diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5b98c8e9..dd2225e91 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,22 +2,19 @@ name: Build on: push: branches: - - sprint # or the name of your main branch + - sprint pull_request: types: [opened, synchronize, reopened] jobs: - build: - name: Build + sonarcloud: + name: SonarCloud runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: - fetch-depth: 0 - - name: Install dependencies - run: yarn - # - name: Test and coverage - # run: yarn jest --coverage - - uses: sonarsource/sonarqube-scan-action@master + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 441c695aa..02db64b75 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true diff --git a/App.tsx b/App.tsx index db5f4d5d4..ea1fb11a7 100644 --- a/App.tsx +++ b/App.tsx @@ -1,8 +1,6 @@ import * as Sentry from '@sentry/react-native'; - import { LogBox, Platform, UIManager } from 'react-native'; import React, { ReactElement, useEffect } from 'react'; - import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { NativeBaseProvider, StatusBar } from 'native-base'; import { PersistGate } from 'redux-persist/integration/react'; @@ -15,7 +13,6 @@ import Navigator from './src/navigation/Navigator'; import { persistor, store } from './src/store/store'; import { LocalizationProvider } from 'src/context/Localization/LocContext'; import { AppContextProvider } from 'src/context/AppContext'; -import { sentryConfig } from 'src/services/sentry'; LogBox.ignoreLogs([ "[react-native-gesture-handler] Seems like you're using an old API with gesture components, check out new Gestures system!", @@ -37,7 +34,6 @@ function AndroidProvider({ children }: { children: ReactElement }) { function App() { useEffect(() => { initConnection(); - Sentry.init(sentryConfig); return () => { endConnection(); }; diff --git a/Gemfile.lock b/Gemfile.lock index 8a92cf8a7..b05e1b92f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,28 +9,28 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) artifactory (3.0.15) atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.826.0) - aws-sdk-core (3.183.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-eventstream (1.3.0) + aws-partitions (1.885.0) + aws-sdk-core (3.191.0) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.71.0) - aws-sdk-core (~> 3, >= 3.177.0) + aws-sdk-kms (1.77.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.135.0) - aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-s3 (1.143.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.6.0) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) claide (1.1.0) @@ -79,14 +79,13 @@ GEM declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.103.0) + excon (0.109.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -115,8 +114,8 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.7) - fastlane (2.216.0) + fastimage (2.3.0) + fastlane (2.219.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -135,6 +134,7 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -143,7 +143,7 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) @@ -160,9 +160,9 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.49.0) + google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.1) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -170,24 +170,23 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.19.0) - google-apis-core (>= 0.9.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.3.1) - google-cloud-storage (1.44.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -204,7 +203,7 @@ GEM i18n (1.12.0) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.6.3) + json (2.7.1) jwt (2.7.1) mini_magick (4.12.0) mini_mime (1.1.5) @@ -216,11 +215,11 @@ GEM nap (1.1.0) naturally (2.2.1) netrc (0.11.0) - optparse (0.1.1) + optparse (0.4.0) os (1.1.4) - plist (3.7.0) + plist (3.7.1) public_suffix (4.0.7) - rake (13.0.6) + rake (13.1.0) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -245,7 +244,7 @@ GEM unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) typhoeus (1.4.0) @@ -253,13 +252,9 @@ GEM tzinfo (2.0.5) concurrent-ruby (~> 1.0) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (2.4.2) - webrick (1.8.1) + unicode-display_width (2.5.0) word_wrap (1.0.0) - xcodeproj (1.22.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/Readme.md b/Readme.md index a12d2ab52..ac220271c 100644 --- a/Readme.md +++ b/Readme.md @@ -6,6 +6,8 @@ Affordable and easy-to-use, security for all your sats, BIP-85, Multisig, Own No [![Playstore](https://bitcoinkeeper.app/wp-content/uploads/2023/05/gpbtn.png)](https://play.google.com/store/apps/details?id=io.hexawallet.bitcoinkeeper) [![Appstore](https://bitcoinkeeper.app/wp-content/uploads/2023/05/applebtn.png)](https://apps.apple.com/us/app/bitcoin-keeper/id1545535925) +[![PGP_APK](https://github.com/bithyve/bitcoin-keeper/assets/50690016/67693cf0-a059-4391-8b48-a9d46a55e71c)](https://github.com/bithyve/bitcoin-keeper/releases) + ## Prerequisites diff --git a/android/app/build.gradle b/android/app/build.gradle index 522dc100d..86d6a1880 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -85,8 +85,8 @@ android { applicationId "io.hexawallet.keeper" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 283 - versionName "1.1.10" + versionCode 297 + versionName "1.1.11" missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'store', 'play' multiDexEnabled true diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 1a48f0ae0..71efb8cb6 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -47,9 +47,7 @@ platform :android do key_password = ENV["KEY_PASSWORD"] key_alias = ENV["KEY_ALIAS"] releaseFilePath = File.join(Dir.pwd, "../app", "release.keystore") - gradle(task: 'clean') - gradle( task: 'bundle', build_type: 'productionRelease', @@ -64,7 +62,18 @@ platform :android do upload_to_play_store( track: 'internal' ) - upload_to_slack({file_path: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH]}) + gradle( + task: 'assemble', + build_type: 'productionRelease', + print_command: false, + properties: { + "android.injected.signing.store.file" => releaseFilePath, + "android.injected.signing.store.password" => store_password, + "android.injected.signing.key.alias" => key_alias, + "android.injected.signing.key.password" => key_password, + } + ) + upload_to_slack({file_path: lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]}) end end diff --git a/flows/newapp.yaml b/flows/newapp.yaml index 4a18862a8..dd790ef48 100644 --- a/flows/newapp.yaml +++ b/flows/newapp.yaml @@ -29,7 +29,7 @@ appId: ${APPID} - tapOn: id: 'btn_Vault' - assertVisible: - text: 'Add Signing Device to activate your Vault' + text: 'Add signer to activate your Vault' index: 1 - assertVisible: id: 'btn_Inheritance_Tools' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f3190a73d..eebfe278f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -472,6 +472,8 @@ PODS: - React-Core - react-native-randombytes (3.6.1): - React-Core + - react-native-rsa-native (2.0.5): + - React - react-native-safe-area-context (4.7.2): - React-Core - react-native-tcp-socket (5.6.2): @@ -604,7 +606,8 @@ PODS: - RNFBApp - RNFS (2.20.0): - React-Core - - RNGestureHandler (2.12.1): + - RNGestureHandler (2.14.0): + - RCT-Folly (= 2021.07.22.00) - React-Core - RNIap (12.10.5): - React-Core @@ -612,35 +615,10 @@ PODS: - React-Core - RNLocalize (2.2.2): - React-Core - - RNReanimated (3.4.2): - - DoubleConversion - - FBLazyVector - - glog - - hermes-engine - - RCT-Folly - - RCTRequired - - RCTTypeSafety - - React-callinvoker + - RNReanimated (3.6.1): + - RCT-Folly (= 2021.07.22.00) - React-Core - - React-Core/DevSupport - - React-Core/RCTWebSocket - - React-CoreModules - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-RCTActionSheet - - React-RCTAnimation - - React-RCTAppDelegate - - React-RCTBlob - - React-RCTImage - - React-RCTLinking - - React-RCTNetwork - - React-RCTSettings - - React-RCTText - ReactCommon/turbomodule/core - - Yoga - RNScreens (3.25.0): - React-Core - React-RCTImage @@ -720,6 +698,7 @@ DEPENDENCIES: - react-native-nfc-manager (from `../node_modules/react-native-nfc-manager`) - react-native-pdf (from `../node_modules/react-native-pdf`) - react-native-randombytes (from `../node_modules/react-native-randombytes`) + - react-native-rsa-native (from `../node_modules/react-native-rsa-native`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-tcp-socket (from `../node_modules/react-native-tcp-socket`) - react-native-tor (from `../node_modules/react-native-tor`) @@ -860,6 +839,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-pdf" react-native-randombytes: :path: "../node_modules/react-native-randombytes" + react-native-rsa-native: + :path: "../node_modules/react-native-rsa-native" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" react-native-tcp-socket: @@ -993,6 +974,7 @@ SPEC CHECKSUMS: react-native-nfc-manager: 42ebc22a04f32c0c2bc63a016ab30fa6486de450 react-native-pdf: 7c0e91ada997bac8bac3bb5bea5b6b81f5a3caae react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 + react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a react-native-safe-area-context: 7aa8e6d9d0f3100a820efb1a98af68aa747f9284 react-native-tcp-socket: c1b7297619616b4c9caae6889bcb0aba78086989 react-native-tor: 3b14e9160b2eb7fa3f310921b2dee71a5171e5b7 @@ -1019,11 +1001,11 @@ SPEC CHECKSUMS: RNFBApp: 9646e09d041ea159b84584865212e4cf33acd179 RNFBMessaging: 3e2682ea5e15fe86da24d16d16019395a881f33c RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: c0d04458598fcb26052494ae23dda8f8f5162b13 + RNGestureHandler: 32a01c29ecc9bb0b5bf7bc0a33547f61b4dc2741 RNIap: c397f49db45af3b10dca64b2325f21bb8078ad21 RNKeychain: a65256b6ca6ba6976132cc4124b238a5b13b3d9c RNLocalize: 95a43f85e41a966be7bc9cff2437128911c52da0 - RNReanimated: 726395a2fa2f04cea340274ba57a4e659bc0d9c1 + RNReanimated: fdbaa9c964bbab7fac50c90862b6cc5f041679b9 RNScreens: 85d3880b52d34db7b8eeebe2f1a0e807c05e69fa RNSentry: 1cd4360f7c668f4d7e8f860e41da884f00e7d814 RNShare: da6d90b6dc332f51f86498041d6e34211f96b630 diff --git a/ios/hexa_keeper.xcodeproj/project.pbxproj b/ios/hexa_keeper.xcodeproj/project.pbxproj index 0d7c04cb2..d722d7936 100644 --- a/ios/hexa_keeper.xcodeproj/project.pbxproj +++ b/ios/hexa_keeper.xcodeproj/project.pbxproj @@ -768,8 +768,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "d7f32c91-22ec-44dd-98e0-8028f77d3ab5"; - PROVISIONING_PROFILE_SPECIFIER = "io.hexawallet.hexakeeper.dev AppStore"; + PROVISIONING_PROFILE = "24a4926e-8b09-47ba-9a7a-303fd6c15cae"; + PROVISIONING_PROFILE_SPECIFIER = "Keeper Distribution"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hexa_keeper.app/hexa_keeper"; }; name = Release; @@ -783,7 +783,7 @@ CODE_SIGN_ENTITLEMENTS = hexa_keeper/hexa_keeper.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 297; DEVELOPMENT_TEAM = Y5TCB759QL; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -891,7 +891,7 @@ "$(inherited)", "\"$(SRCROOT)\"", ); - MARKETING_VERSION = 1.1.10; + MARKETING_VERSION = 1.1.11; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -915,8 +915,9 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = hexa_keeper/hexa_keeper.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: Bithyve UK Ltd (Y5TCB759QL)"; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 297; DEVELOPMENT_TEAM = Y5TCB759QL; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Y5TCB759QL; HEADER_SEARCH_PATHS = ( "$(inherited)", "\"${PODS_ROOT}/Headers/Public\"", @@ -1022,7 +1023,7 @@ "$(inherited)", "\"$(SRCROOT)\"", ); - MARKETING_VERSION = 1.1.10; + MARKETING_VERSION = 1.1.11; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1030,8 +1031,9 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.hexawallet.keeper; PRODUCT_NAME = hexa_keeper; - PROVISIONING_PROFILE = "d7f32c91-22ec-44dd-98e0-8028f77d3ab5"; - PROVISIONING_PROFILE_SPECIFIER = "io.hexawallet.hexakeeper.dev AppStore"; + PROVISIONING_PROFILE = "24a4926e-8b09-47ba-9a7a-303fd6c15cae"; + PROVISIONING_PROFILE_SPECIFIER = "Keeper Distribution"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Keeper Distribution"; SWIFT_OBJC_BRIDGING_HEADER = "whirlpool/hexa_keeper-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -1176,7 +1178,7 @@ CODE_SIGN_ENTITLEMENTS = hexa_keeper_dev.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 297; DEVELOPMENT_TEAM = Y5TCB759QL; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -1285,7 +1287,7 @@ "$(PROJECT_DIR)", "\"$(SRCROOT)\"", ); - MARKETING_VERSION = 1.1.10; + MARKETING_VERSION = 1.1.11; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1310,8 +1312,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = hexa_keeper_dev.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution"; - CURRENT_PROJECT_VERSION = 283; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CURRENT_PROJECT_VERSION = 297; DEVELOPMENT_TEAM = Y5TCB759QL; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Y5TCB759QL; HEADER_SEARCH_PATHS = ( "$(inherited)", "\"${PODS_ROOT}/Headers/Public\"", @@ -1418,7 +1422,7 @@ "$(PROJECT_DIR)", "\"$(SRCROOT)\"", ); - MARKETING_VERSION = 1.1.10; + MARKETING_VERSION = 1.1.11; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1426,8 +1430,9 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.hexawallet.hexakeeper.dev; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "d7f32c91-22ec-44dd-98e0-8028f77d3ab5"; - PROVISIONING_PROFILE_SPECIFIER = "io.hexawallet.hexakeeper.dev AppStore"; + PROVISIONING_PROFILE = "24a4926e-8b09-47ba-9a7a-303fd6c15cae"; + PROVISIONING_PROFILE_SPECIFIER = "Keeper Distribution"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Keeper Development"; SWIFT_OBJC_BRIDGING_HEADER = "whirlpool/hexa_keeper-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "hexa_keeper-Swift.h"; SWIFT_VERSION = 5.0; diff --git a/ios/hexa_keeper/Info.plist b/ios/hexa_keeper/Info.plist index ce85e612e..9f778c68b 100644 --- a/ios/hexa_keeper/Info.plist +++ b/ios/hexa_keeper/Info.plist @@ -36,7 +36,7 @@ CFBundleVersion - 283 + 297 LSRequiresIPhoneOS NFCReaderUsageDescription diff --git a/ios/hexa_keeperTests/Info.plist b/ios/hexa_keeperTests/Info.plist index 6923fb13e..68b736510 100644 --- a/ios/hexa_keeperTests/Info.plist +++ b/ios/hexa_keeperTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 283 + 297 diff --git a/ios/hexa_keeper_dev-Info.plist b/ios/hexa_keeper_dev-Info.plist index 62ee4ce1b..c89723a4b 100644 --- a/ios/hexa_keeper_dev-Info.plist +++ b/ios/hexa_keeper_dev-Info.plist @@ -36,7 +36,7 @@ CFBundleVersion - 283 + 297 LSRequiresIPhoneOS NFCReaderUsageDescription diff --git a/package.json b/package.json index b82fd97dc..49fc0d1bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hexa_keeper", - "version": "1.1.10", + "version": "1.1.11", "private": true, "scripts": { "ios": "react-native run-ios", @@ -29,6 +29,7 @@ "@react-native-firebase/app": "^14.11.1", "@react-native-firebase/messaging": "^14.11.1", "@react-navigation/bottom-tabs": "^6.5.7", + "@react-navigation/drawer": "^6.6.6", "@react-navigation/native": "^6.0.8", "@react-navigation/native-stack": "^6.5.0", "@realm/react": "^0.5.2", @@ -49,6 +50,7 @@ "cktap-protocol-react-native": "git+https://github.com/bithyve/cktap-protocol-react-native.git#main", "coinselect": "3.1.13", "constants": "^0.0.2", + "crypto": "^1.0.1", "crypto-js": "4.1.1", "deprecated-react-native-prop-types": "^4.2.1", "ecpair": "2.0.1", @@ -74,7 +76,7 @@ "react-native-device-info": "10.0.2", "react-native-document-picker": "^8.1.3", "react-native-fs": "^2.20.0", - "react-native-gesture-handler": "^2.12.0", + "react-native-gesture-handler": "^2.14.0", "react-native-get-random-values": "^1.7.2", "react-native-hce": "^0.2.0", "react-native-html-to-pdf": "^0.12.0", @@ -89,8 +91,9 @@ "react-native-qr-decode-image-camera": "^1.1.1", "react-native-qrcode-svg": "^6.1.2", "react-native-randombytes": "^3.0.0", - "react-native-reanimated": "^3.3.0", + "react-native-reanimated": "^3.6.1", "react-native-responsive-screen": "^1.4.2", + "react-native-rsa-native": "^2.0.5", "react-native-safe-area-context": "^4.5.3", "react-native-screens": "^3.21.0", "react-native-send-intent": "^1.3.0", @@ -143,6 +146,7 @@ "lint-staged": "^13.0.4", "metro-react-native-babel-preset": "^0.76.5", "prettier": "^2.4.1", + "react-native-codegen": "^0.70.7", "react-native-flipper": "^0.145.0", "react-native-svg-transformer": "^1.1.0", "react-test-renderer": "18.2.0", diff --git a/sonar-project.properties b/sonar-project.properties index dc30868dc..f7cea55e6 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,3 +1,8 @@ -# must be unique in a given SonarQube instance -sonar.projectKey=bithyve_bitcoin-keeper_AYia6EJSyra2oRERv2Gu -sonar.exclusions=.github/**, .vscode/**, android/**, assets/**, build/**, ios/**, node_modules/**, scripts/**, src/hardware/ledger/client/**, src/core/services/qr/bc-ur-registry/**, tests/** +sonar.projectKey=bithyve_bitcoin-keeper +sonar.organization=bithyve + +sonar.coverage.exclusions=**/android/**/*.*,**/ios/**/*.* +sonar.exclusions=**/android/**/*.*,**/ios/**/*.* + +sonar.javascript.file.suffixes=.js,.jsx +sonar.typescript.file.suffixes=.ts,.tsx \ No newline at end of file diff --git a/src/assets/images/add_white.svg b/src/assets/images/add_white.svg new file mode 100644 index 000000000..ade067e9c --- /dev/null +++ b/src/assets/images/add_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/advanced.svg b/src/assets/images/advanced.svg new file mode 100644 index 000000000..6bb47c60f --- /dev/null +++ b/src/assets/images/advanced.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/images/advanced_green.svg b/src/assets/images/advanced_green.svg new file mode 100644 index 000000000..6d934e400 --- /dev/null +++ b/src/assets/images/advanced_green.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/images/align_right.svg b/src/assets/images/align_right.svg new file mode 100644 index 000000000..bac7ab884 --- /dev/null +++ b/src/assets/images/align_right.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/images/app_backup.svg b/src/assets/images/app_backup.svg new file mode 100644 index 000000000..52cc1215e --- /dev/null +++ b/src/assets/images/app_backup.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/images/calendar.svg b/src/assets/images/calendar.svg new file mode 100644 index 000000000..6387c819b --- /dev/null +++ b/src/assets/images/calendar.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/calendar_disabled.svg b/src/assets/images/calendar_disabled.svg new file mode 100644 index 000000000..8483a00b8 --- /dev/null +++ b/src/assets/images/calendar_disabled.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/change.svg b/src/assets/images/change.svg index 5eaadce34..38319fd7c 100644 --- a/src/assets/images/change.svg +++ b/src/assets/images/change.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/src/assets/images/check.svg b/src/assets/images/check.svg new file mode 100644 index 000000000..f6a66e99e --- /dev/null +++ b/src/assets/images/check.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/checkmark.svg b/src/assets/images/checkmark.svg new file mode 100644 index 000000000..8c1b01538 --- /dev/null +++ b/src/assets/images/checkmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/coinhat.svg b/src/assets/images/coinhat.svg new file mode 100644 index 000000000..3cbd54bc3 --- /dev/null +++ b/src/assets/images/coinhat.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/images/coins.svg b/src/assets/images/coins.svg new file mode 100644 index 000000000..b7cb7c52d --- /dev/null +++ b/src/assets/images/coins.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/images/collaborative_vault.svg b/src/assets/images/collaborative_vault.svg new file mode 100644 index 000000000..9f83763e9 --- /dev/null +++ b/src/assets/images/collaborative_vault.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/collaborative_vault_white.svg b/src/assets/images/collaborative_vault_white.svg new file mode 100644 index 000000000..97e570853 --- /dev/null +++ b/src/assets/images/collaborative_vault_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/copy.svg b/src/assets/images/copy.svg new file mode 100644 index 000000000..f273262dc --- /dev/null +++ b/src/assets/images/copy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/copy_new.svg b/src/assets/images/copy_new.svg new file mode 100644 index 000000000..f273262dc --- /dev/null +++ b/src/assets/images/copy_new.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/cross.svg b/src/assets/images/cross.svg new file mode 100644 index 000000000..83de0c423 --- /dev/null +++ b/src/assets/images/cross.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/daily_wallet.svg b/src/assets/images/daily_wallet.svg new file mode 100644 index 000000000..83a0a5348 --- /dev/null +++ b/src/assets/images/daily_wallet.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/degrading multisig.svg b/src/assets/images/degrading multisig.svg new file mode 100644 index 000000000..6c873d717 --- /dev/null +++ b/src/assets/images/degrading multisig.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/images/degrading_multisig_disabled.svg b/src/assets/images/degrading_multisig_disabled.svg new file mode 100644 index 000000000..908d40962 --- /dev/null +++ b/src/assets/images/degrading_multisig_disabled.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/images/delete_phone.svg b/src/assets/images/delete_phone.svg new file mode 100644 index 000000000..b1731f553 --- /dev/null +++ b/src/assets/images/delete_phone.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/diamond_hands.svg b/src/assets/images/diamond_hands.svg new file mode 100644 index 000000000..9f9abf68b --- /dev/null +++ b/src/assets/images/diamond_hands.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/empty_wallet_illustration.svg b/src/assets/images/empty_wallet_illustration.svg new file mode 100644 index 000000000..10c83b733 --- /dev/null +++ b/src/assets/images/empty_wallet_illustration.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/eye_folder.svg b/src/assets/images/eye_folder.svg new file mode 100644 index 000000000..480327933 --- /dev/null +++ b/src/assets/images/eye_folder.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/images/faq.svg b/src/assets/images/faq.svg new file mode 100644 index 000000000..aba511a47 --- /dev/null +++ b/src/assets/images/faq.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/file.svg b/src/assets/images/file.svg new file mode 100644 index 000000000..6a581bd92 --- /dev/null +++ b/src/assets/images/file.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/healthcheck_light.svg b/src/assets/images/healthcheck_light.svg new file mode 100644 index 000000000..c819813e8 --- /dev/null +++ b/src/assets/images/healthcheck_light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/hexagontile_wallet.svg b/src/assets/images/hexagontile_wallet.svg new file mode 100644 index 000000000..b9f2631a8 --- /dev/null +++ b/src/assets/images/hexagontile_wallet.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/hide_wallet.svg b/src/assets/images/hide_wallet.svg new file mode 100644 index 000000000..4e023af55 --- /dev/null +++ b/src/assets/images/hide_wallet.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/hodler.svg b/src/assets/images/hodler.svg new file mode 100644 index 000000000..f63d442af --- /dev/null +++ b/src/assets/images/hodler.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/icon_received_footer.svg b/src/assets/images/icon_received_footer.svg new file mode 100644 index 000000000..40f8092e7 --- /dev/null +++ b/src/assets/images/icon_received_footer.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/icon_recieved_red.svg b/src/assets/images/icon_recieved_red.svg new file mode 100644 index 000000000..a5264e743 --- /dev/null +++ b/src/assets/images/icon_recieved_red.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/icon_sent_footer.svg b/src/assets/images/icon_sent_footer.svg new file mode 100644 index 000000000..c010e3e64 --- /dev/null +++ b/src/assets/images/icon_sent_footer.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/icon_sent_red.svg b/src/assets/images/icon_sent_red.svg new file mode 100644 index 000000000..a34be402a --- /dev/null +++ b/src/assets/images/icon_sent_red.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/illustration_8.svg b/src/assets/images/illustration_8.svg new file mode 100644 index 000000000..586181573 --- /dev/null +++ b/src/assets/images/illustration_8.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/illustration_spectre.svg b/src/assets/images/illustration_spectre.svg new file mode 100644 index 000000000..18dd9a39f --- /dev/null +++ b/src/assets/images/illustration_spectre.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/import.svg b/src/assets/images/import.svg new file mode 100644 index 000000000..fae8295b5 --- /dev/null +++ b/src/assets/images/import.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/import_green.svg b/src/assets/images/import_green.svg new file mode 100644 index 000000000..5149cb833 --- /dev/null +++ b/src/assets/images/import_green.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/import_wallet.svg b/src/assets/images/import_wallet.svg new file mode 100644 index 000000000..d9ff15020 --- /dev/null +++ b/src/assets/images/import_wallet.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/inheri.svg b/src/assets/images/inheri.svg new file mode 100644 index 000000000..818b9b6c0 --- /dev/null +++ b/src/assets/images/inheri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/inheritanceTitleKey.svg b/src/assets/images/inheritanceTitleKey.svg new file mode 100644 index 000000000..6e73cdd20 --- /dev/null +++ b/src/assets/images/inheritanceTitleKey.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/lock_green.svg b/src/assets/images/lock_green.svg new file mode 100644 index 000000000..561a44b81 --- /dev/null +++ b/src/assets/images/lock_green.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/menu-hor.svg b/src/assets/images/menu-hor.svg new file mode 100644 index 000000000..e15cd289a --- /dev/null +++ b/src/assets/images/menu-hor.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/noTransaction.svg b/src/assets/images/noTransaction.svg index dc1b015da..393a85aa4 100644 --- a/src/assets/images/noTransaction.svg +++ b/src/assets/images/noTransaction.svg @@ -1,58 +1,43 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + + + + + + + + - - + + + + + + + - - - - - - - - - - - - - - + + + + diff --git a/src/assets/images/passwordlock.svg b/src/assets/images/passwordlock.svg new file mode 100644 index 000000000..90d523aca --- /dev/null +++ b/src/assets/images/passwordlock.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/phoneemail.svg b/src/assets/images/phoneemail.svg new file mode 100644 index 000000000..89572b328 --- /dev/null +++ b/src/assets/images/phoneemail.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/pleb_white.svg b/src/assets/images/pleb_white.svg new file mode 100644 index 000000000..b3e3b16e0 --- /dev/null +++ b/src/assets/images/pleb_white.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/recover_white.svg b/src/assets/images/recover_white.svg new file mode 100644 index 000000000..838d03cf1 --- /dev/null +++ b/src/assets/images/recover_white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/settings_footer.svg b/src/assets/images/settings_footer.svg new file mode 100644 index 000000000..232dc724b --- /dev/null +++ b/src/assets/images/settings_footer.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/settings_white.svg b/src/assets/images/settings_white.svg new file mode 100644 index 000000000..34d2af03a --- /dev/null +++ b/src/assets/images/settings_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/shield.svg b/src/assets/images/shield.svg new file mode 100644 index 000000000..bd4eeeeb3 --- /dev/null +++ b/src/assets/images/shield.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/images/shield_white.svg b/src/assets/images/shield_white.svg new file mode 100644 index 000000000..12a81a1b6 --- /dev/null +++ b/src/assets/images/shield_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/show.svg b/src/assets/images/show.svg new file mode 100644 index 000000000..99ffba16f --- /dev/null +++ b/src/assets/images/show.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/signer.svg b/src/assets/images/signer.svg new file mode 100644 index 000000000..ef0fb23dd --- /dev/null +++ b/src/assets/images/signer.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/signer_white.svg b/src/assets/images/signer_white.svg new file mode 100644 index 000000000..02e6f6abe --- /dev/null +++ b/src/assets/images/signer_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/specter_icon.svg b/src/assets/images/specter_icon.svg new file mode 100644 index 000000000..bffbb2912 --- /dev/null +++ b/src/assets/images/specter_icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/images/specter_icon_light.svg b/src/assets/images/specter_icon_light.svg new file mode 100644 index 000000000..d697e506e --- /dev/null +++ b/src/assets/images/specter_icon_light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/images/specterlogo.svg b/src/assets/images/specterlogo.svg new file mode 100644 index 000000000..5a04d7788 --- /dev/null +++ b/src/assets/images/specterlogo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/transaction_pending.svg b/src/assets/images/transaction_pending.svg new file mode 100644 index 000000000..28a662b12 --- /dev/null +++ b/src/assets/images/transaction_pending.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/images/vault_green.svg b/src/assets/images/vault_green.svg new file mode 100644 index 000000000..6221d12ec --- /dev/null +++ b/src/assets/images/vault_green.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/vault_icon.svg b/src/assets/images/vault_icon.svg new file mode 100644 index 000000000..d2d501a27 --- /dev/null +++ b/src/assets/images/vault_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/walleTabFilled.svg b/src/assets/images/walleTabFilled.svg index af5843509..57ff6a446 100644 --- a/src/assets/images/walleTabFilled.svg +++ b/src/assets/images/walleTabFilled.svg @@ -1,3 +1,3 @@ - + diff --git a/src/assets/images/wallet_green.svg b/src/assets/images/wallet_green.svg new file mode 100644 index 000000000..ce823d94e --- /dev/null +++ b/src/assets/images/wallet_green.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/wallet_vault.svg b/src/assets/images/wallet_vault.svg new file mode 100644 index 000000000..0dc79da3f --- /dev/null +++ b/src/assets/images/wallet_vault.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/watch_only.svg b/src/assets/images/watch_only.svg new file mode 100644 index 000000000..58a4f65af --- /dev/null +++ b/src/assets/images/watch_only.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/images/watch_only_wallet.svg b/src/assets/images/watch_only_wallet.svg new file mode 100644 index 000000000..663261613 --- /dev/null +++ b/src/assets/images/watch_only_wallet.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/images/whirlpool.svg b/src/assets/images/whirlpool.svg new file mode 100644 index 000000000..f2454499c --- /dev/null +++ b/src/assets/images/whirlpool.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/components/ActionCard.tsx b/src/components/ActionCard.tsx new file mode 100644 index 000000000..1e44e7bbf --- /dev/null +++ b/src/components/ActionCard.tsx @@ -0,0 +1,89 @@ +import { Box, useColorMode } from 'native-base'; +import DeviceInfo from 'react-native-device-info'; +import { StyleSheet, TouchableOpacity, ViewStyle } from 'react-native'; + +import Text from './KeeperText'; +import { hp, wp } from 'src/constants/responsive'; +import { useEffect, useState } from 'react'; +import useIsSmallDevices from 'src/hooks/useSmallDevices'; + +type ActionCardProps = { + cardName: string; + description?: string; + icon?: Element; + callback: () => void; + customStyle?: ViewStyle; + dottedBorder?: boolean; +}; + +function ActionCard({ + cardName, + icon, + description, + customStyle, + callback, + dottedBorder = false, +}: ActionCardProps) { + const { colorMode } = useColorMode(); + const isSmallDevice = useIsSmallDevices(); + return ( + + + + {dottedBorder && ( + + )} + {icon && icon} + + + {cardName} + + {description && ( + + {description} + + )} + + + ); +} + +const styles = StyleSheet.create({ + cardContainer: { + width: wp(114), + paddingVertical: hp(10), + paddingLeft: 10, + paddingRight: 6, + borderRadius: 10, + }, + circle: { + width: 34, + height: 34, + borderRadius: 34 / 2, + alignItems: 'center', + justifyContent: 'center', + marginTop: hp(25), + marginBottom: hp(10), + marginLeft: 2, + }, + dottedBorder: { + position: 'absolute', + width: '85%', + height: '85%', + borderRadius: 14, + borderWidth: 1, + borderStyle: 'dotted', + }, + cardName: { + fontSize: 12, + }, +}); + +export default ActionCard; diff --git a/src/components/AddCard.tsx b/src/components/AddCard.tsx new file mode 100644 index 000000000..89f0f978b --- /dev/null +++ b/src/components/AddCard.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { Box, useColorMode } from 'native-base'; +import { StyleSheet, TouchableOpacity, ViewStyle } from 'react-native'; +import AddCardIcon from 'src/assets/images/add_white.svg'; +import Text from './KeeperText'; +import Colors from 'src/theme/Colors'; +import HexagonIcon from './HexagonIcon'; + +type AddSignerCardProps = { + name: string; + callback?: (param: any) => void; + cardStyles?: ViewStyle; + iconWidth?: number; + iconHeight?: number; +}; + +function AddCard({ + name, + callback, + cardStyles, + iconWidth = 40, + iconHeight = 34, +}: AddSignerCardProps) { + const { colorMode } = useColorMode(); + return ( + callback(name)}> + + + } + /> + + {name} + + + + + ); +} + +const styles = StyleSheet.create({ + AddCardContainer: { + width: 114, + padding: 10, + height: 125, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 10, + borderWidth: 1, + borderStyle: 'dashed', + }, + nameStyle: { + fontSize: 12, + fontWeight: '400', + }, + + detailContainer: { + gap: 2, + marginTop: 15, + alignItems: 'center', + }, + iconWrapper: { + width: 34, + height: 34, + borderRadius: 30, + justifyContent: 'center', + alignItems: 'center', + }, +}); + +export default AddCard; diff --git a/src/components/Backup/BackupHealthCheckList.tsx b/src/components/Backup/BackupHealthCheckList.tsx index 702c3c969..c136d263b 100644 --- a/src/components/Backup/BackupHealthCheckList.tsx +++ b/src/components/Backup/BackupHealthCheckList.tsx @@ -13,12 +13,17 @@ import ModalWrapper from 'src/components/Modal/ModalWrapper'; import { seedBackedConfirmed } from 'src/store/sagaActions/bhr'; import { setSeedConfirmed } from 'src/store/reducers/bhr'; import { hp, wp } from 'src/constants/responsive'; -import { useNavigation } from '@react-navigation/native'; +import { CommonActions, useNavigation } from '@react-navigation/native'; import HealthCheckComponent from './HealthCheckComponent'; import BackupSuccessful from 'src/components/SeedWordBackup/BackupSuccessful'; import DotView from 'src/components/DotView'; import Buttons from 'src/components/Buttons'; import { useQuery } from '@realm/react'; +import SigningDeviceChecklist from 'src/screens/Vault/SigningDeviceChecklist'; +import KeeperFooter from '../KeeperFooter'; + +import HealthCheck from 'src/assets/images/healthcheck_light.svg'; +import AdvnaceOptions from 'src/assets/images/settings.svg'; function BackupHealthCheckList() { const { colorMode } = useColorMode(); @@ -53,62 +58,77 @@ function BackupHealthCheckList() { }; }, [seedConfirmed]); + function FooterIcon({ Icon }) { + return ( + + + + ); + } + + const footerItems = [ + { + text: 'Health Check', + Icon: () => , + onPress: () => { + onPressConfirm(); + }, + }, + { + text: 'Settings', + Icon: () => , + onPress: () => { + navigtaion.dispatch(CommonActions.navigate('AppBackupSettings', {})); + }, + }, + ]; + return ( - + ( - + - + - + + {item?.title} + + {moment.unix(item.date).format('DD MMM YYYY, HH:mmA')} - - - {strings[item.title]} - - {item.subtitle !== '' && ( - - {item.subtitle} - - )} - )} /> - - - + + + {heading} + + + ); +} + +const styles = StyleSheet.create({ + pillContainer: { + paddingHorizontal: '6%', + height: 17, + borderRadius: 15, + alignItems: 'center', + justifyContent: 'center', + }, + heading: { + fontSize: 8, + lineHeight: 17, + }, +}); + +export default CardPill; diff --git a/src/components/Carousel/ChoosePlanCarousel.tsx b/src/components/Carousel/ChoosePlanCarousel.tsx index 104475cd4..df09304d9 100644 --- a/src/components/Carousel/ChoosePlanCarousel.tsx +++ b/src/components/Carousel/ChoosePlanCarousel.tsx @@ -1,13 +1,13 @@ import { Box } from 'native-base'; -import { FlatList, StyleSheet, Dimensions } from 'react-native'; +import { FlatList, Dimensions } from 'react-native'; import React, { useState } from 'react'; -import { hp, wp } from 'src/constants/responsive'; +import { hp } from 'src/constants/responsive'; import { KeeperApp } from 'src/models/interfaces/KeeperApp'; import { RealmSchema } from 'src/storage/realm/enum'; import { SubscriptionTier } from 'src/models/enums/SubscriptionTier'; import { SubScriptionPlan } from 'src/models/interfaces/Subscription'; -import ChoosePlanCarouselItem from './ChoosePlanCarouselItem'; import { useQuery } from '@realm/react'; +import ChoosePlanCarouselItem from './ChoosePlanCarouselItem'; const { width } = Dimensions.get('window'); const itemWidth = width / 3.5 - 10; @@ -32,22 +32,6 @@ function ChoosePlanCarousel(props: Props) { props.onChange(index); }; - const getBtnTitle = (item: SubScriptionPlan) => { - if (!item.isActive) { - return 'Coming soon'; - } - if (item.productIds.includes(SubscriptionTier.L1)) { - return 'Select'; - } - if ( - item.name.split(' ')[0] === SubscriptionTier.L2 && - subscription.name === SubscriptionTier.L3 - ) { - return 'Select'; - } - return 'Select'; - }; - return ( ( ); } -const styles = StyleSheet.create({ - wrapperView: { - borderRadius: 20, - marginHorizontal: wp(4), - position: 'relative', - }, -}); + export default ChoosePlanCarousel; diff --git a/src/components/Carousel/ChoosePlanCarouselItem.tsx b/src/components/Carousel/ChoosePlanCarouselItem.tsx index 5ecff67e2..f56dd2150 100644 --- a/src/components/Carousel/ChoosePlanCarouselItem.tsx +++ b/src/components/Carousel/ChoosePlanCarouselItem.tsx @@ -5,6 +5,9 @@ import { hp, wp } from 'src/constants/responsive'; import { SubscriptionTier } from 'src/models/enums/SubscriptionTier'; import Text from 'src/components/KeeperText'; import SubScription, { SubScriptionPlan } from 'src/models/interfaces/Subscription'; +import PlebIcon from 'src/assets/images/pleb_white.svg'; +import HodlerIcon from 'src/assets/images/hodler.svg'; +import DiamondIcon from 'src/assets/images/diamond_hands.svg'; import CustomYellowButton from '../CustomButton/CustomYellowButton'; const styles = StyleSheet.create({ @@ -14,6 +17,14 @@ const styles = StyleSheet.create({ position: 'relative', paddingBottom: 20, }, + circle: { + width: 40, + height: 40, + borderRadius: 40 / 2, + marginTop: 10, + alignItems: 'center', + justifyContent: 'center', + }, }); interface Props { @@ -40,6 +51,7 @@ function ChoosePlanCarouselItem({ requesting, }: Props) { const { colorMode } = useColorMode(); + const isSelected = currentPosition === index; const getFreeTrail = useMemo(() => { if (item.monthlyPlanDetails || item.yearlyPlanDetails) { if (isMonthly) return item.monthlyPlanDetails.trailPeriod; @@ -77,7 +89,7 @@ function ChoosePlanCarouselItem({ }, [item, isMonthly]); const canSelectPlan = useMemo(() => { - if (currentPosition === index) { + if (isSelected) { if (isMonthly) { return !item.monthlyPlanDetails?.productId.includes(subscription.productId.toLowerCase()); } @@ -89,11 +101,12 @@ function ChoosePlanCarouselItem({ return ( onPress(index)} testID="btn_selectPlan"> @@ -107,7 +120,7 @@ function ChoosePlanCarouselItem({ py={0.5} px={2} > - + Current @@ -116,36 +129,41 @@ function ChoosePlanCarouselItem({ )} - {/* {currentPosition === index ? : } */} - + + {item.name === 'Pleb' && } + {item.name === 'Hodler' && } + {item.name === 'Diamond Hands' && } + + {item.name} - + {item.subTitle} - - {getFreeTrail} - - + {getAmt} {item.productType !== 'free' && item.isActive ? (isMonthly ? '/month' : '/year') : ''} - {/* + {getFreeTrail} - */} + {canSelectPlan === true ? ( onSelect(item, index)} value={getBtnTitle} disabled={!item.isActive || requesting} - titleColor={`${colorMode}.yellowButtonTextColor`} + titleColor={`${colorMode}.pantoneGreen`} + backgroundColor={`${colorMode}.seashellWhite`} /> ) : null} diff --git a/src/components/CircleIconWrapper.tsx b/src/components/CircleIconWrapper.tsx new file mode 100644 index 000000000..14c874aec --- /dev/null +++ b/src/components/CircleIconWrapper.tsx @@ -0,0 +1,31 @@ +import { Box } from 'native-base'; +import { StyleSheet } from 'react-native'; + +type Props = { + icon: Element; + width?: number; + backgroundColor?: string; +}; + +function CircleIconWrapper({ icon, width = 50, backgroundColor }: Props) { + return ( + + {icon} + + ); +} + +const styles = StyleSheet.create({ + alignItems: { + alignItems: 'center', + justifyContent: 'center', + }, +}); + +export default CircleIconWrapper; diff --git a/src/components/CustomButton/CustomYellowButton.tsx b/src/components/CustomButton/CustomYellowButton.tsx index c2fd169c2..ad92f9c7f 100644 --- a/src/components/CustomButton/CustomYellowButton.tsx +++ b/src/components/CustomButton/CustomYellowButton.tsx @@ -1,13 +1,14 @@ import React from 'react'; import { TouchableHighlight, StyleSheet } from 'react-native'; import { Box, useColorMode } from 'native-base'; - import Text from 'src/components/KeeperText'; + export interface Props { value: string; onPress?: Function; disabled?: boolean; titleColor?: string; + backgroundColor?: string; } function CustomYellowButton(props: Props) { const { colorMode } = useColorMode(); @@ -20,7 +21,10 @@ function CustomYellowButton(props: Props) { }} testID="btn_customYellowButton" > - + {props.value} diff --git a/src/components/EmptyView/EmptyStateView.tsx b/src/components/EmptyView/EmptyStateView.tsx index 085e5c22c..dcb57864e 100644 --- a/src/components/EmptyView/EmptyStateView.tsx +++ b/src/components/EmptyView/EmptyStateView.tsx @@ -7,22 +7,27 @@ import { hp, windowHeight } from 'src/constants/responsive'; function EmptyStateView({ IllustartionImage, title, - subTitle, + subTitle = '', }: { IllustartionImage: any; title: string; - subTitle: string; + subTitle?: string; }) { const { colorMode } = useColorMode(); return ( - {windowHeight > 812 ? : } - - {title} - - - {subTitle} - + + + {title} + + {subTitle && ( + + {subTitle} + + )} + + {/* {windowHeight > 812 ? : } */} + ); } @@ -31,13 +36,13 @@ const styles = StyleSheet.create({ marginTop: windowHeight > 800 ? hp(20) : hp(12), alignItems: 'center', justifyContent: 'flex-end', + gap: 20, }, noTransactionTitle: { - fontSize: 12, + fontSize: 14, letterSpacing: 0.6, opacity: 0.85, fontWeight: '400', - marginTop: hp(20), }, noTransactionSubTitle: { fontSize: 12, diff --git a/src/components/HexagonIcon.tsx b/src/components/HexagonIcon.tsx new file mode 100644 index 000000000..458fc6d53 --- /dev/null +++ b/src/components/HexagonIcon.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { Box } from 'native-base'; +import Svg, { Path } from 'react-native-svg'; + +type Props = { + width: number; + height: number; + backgroundColor: string; + icon: Element; +}; + +function HexagonIcon({ width, height, backgroundColor, icon }: Props) { + return ( + + + + + {icon} + + ); +} + +const styles = StyleSheet.create({ + container: { + position: 'relative', + alignItems: 'center', + justifyContent: 'center', + }, + icon: { + position: 'absolute', + }, +}); + +export default HexagonIcon; diff --git a/src/components/Instruction.tsx b/src/components/Instruction.tsx index b9ec24072..4db9a99d6 100644 --- a/src/components/Instruction.tsx +++ b/src/components/Instruction.tsx @@ -1,15 +1,18 @@ import { StyleSheet } from 'react-native'; import React from 'react'; import { Box, useColorMode } from 'native-base'; -import { hp, wp } from 'src/constants/responsive'; +import { wp } from 'src/constants/responsive'; import Text from './KeeperText'; export function Instruction({ text }: { text: string }) { const { colorMode } = useColorMode(); return ( - - + + + • + + {text} @@ -22,13 +25,9 @@ const styles = StyleSheet.create({ bulletContainer: { marginTop: 4, flexDirection: 'row', - }, - bulletPoint: { - marginRight: wp(5), - height: hp(5), - width: hp(5), - borderRadius: 10, - top: 12, + alignItems: 'center', + justifyContent: 'center', + gap: 5, }, infoText: { letterSpacing: 0.65, diff --git a/src/components/KeeperFooter.tsx b/src/components/KeeperFooter.tsx index 7a7afb443..dc698a037 100644 --- a/src/components/KeeperFooter.tsx +++ b/src/components/KeeperFooter.tsx @@ -11,13 +11,15 @@ type FooterItem = { disabled?: boolean; hideItem?: boolean; }; -export const KeeperFooter = ({ +export function KeeperFooter({ items, wrappedScreen = true, + marginX = 10, }: { items: FooterItem[]; + marginX?: number; wrappedScreen?: boolean; -}) => { +}) { const { colorMode } = useColorMode(); const footerItemsToRender = items.filter((item) => !item.hideItem); return ( @@ -26,9 +28,9 @@ export const KeeperFooter = ({ 2 ? 'space-between' : 'space-around'} - marginX={10} + marginX={marginX} marginTop={3} - alignItems={'flex-start'} + alignItems="flex-start" > {footerItemsToRender.map((item) => { return ( @@ -39,7 +41,9 @@ export const KeeperFooter = ({ onPress={item.onPress} disabled={item.disabled} > - + + + ); -}; +} export default KeeperFooter; @@ -65,11 +69,19 @@ const styles = StyleSheet.create({ paddingHorizontal: 5, }, IconWrapper: { - justifyContent: 'center', + justifyContent: 'space-around', alignItems: 'center', + gap: 10, }, border: { borderWidth: 0.5, opacity: 0.2, }, + circle: { + width: 38, + height: 38, + borderRadius: 38 / 2, + alignItems: 'center', + justifyContent: 'center', + }, }); diff --git a/src/components/KeeperHeader.tsx b/src/components/KeeperHeader.tsx index 2c7fc64ca..54d2dc51b 100644 --- a/src/components/KeeperHeader.tsx +++ b/src/components/KeeperHeader.tsx @@ -9,7 +9,9 @@ import Text from 'src/components/KeeperText'; type Props = { title?: string; + titleColor?: string; subtitle?: string; + subTitleColor?: string; onPressHandler?: () => void; enableBack?: boolean; learnMore?: boolean; @@ -18,25 +20,32 @@ type Props = { learnTextColor?: string; rightComponent?: Element; contrastScreen?: boolean; + marginLeft?: boolean; + icon?: Element; }; function KeeperHeader({ title = '', subtitle = '', + titleColor, + subTitleColor, onPressHandler, enableBack = true, learnMore = false, learnMorePressed = () => {}, - learnBackgroundColor = 'light.lightAccent', + learnBackgroundColor = 'light.RussetBrown', learnTextColor = 'light.learnMoreBorder', rightComponent = null, contrastScreen = false, + marginLeft = true, + icon = null, }: Props) { const { colorMode } = useColorMode(); const navigation = useNavigation(); + const styles = getStyles(marginLeft); return ( {enableBack && ( - + )} - - {title && ( - - {title} - - )} - {subtitle && ( - - {subtitle} - - )} + + {icon && icon} + + {title && ( + + {title} + + )} + {subtitle && ( + + {subtitle} + + )} + {rightComponent} @@ -89,50 +100,57 @@ function KeeperHeader({ ); } -const styles = StyleSheet.create({ - container: { - backgroundColor: 'transparent', - }, - addWalletText: { - lineHeight: 26, - letterSpacing: 0.8, - }, - addWalletDescription: { - fontSize: 12, - lineHeight: 20, - letterSpacing: 0.5, - }, - backContainer: { - justifyContent: 'space-between', - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 5, - paddingVertical: windowHeight > 680 ? 15 : 7, - }, - backButton: { - height: 20, - width: 20, - justifyContent: 'center', - alignItems: 'center', - padding: 20, - }, - learnMoreContainer: { - borderWidth: 0.5, - borderRadius: 5, - paddingHorizontal: 5, - justifyContent: 'center', - alignItems: 'center', - }, - learnMoreText: { - fontSize: 12, - letterSpacing: 0.6, - alignSelf: 'center', - }, - headerContainer: { - width: windowWidth * 0.85, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, -}); +const getStyles = (marginLeft: boolean) => + StyleSheet.create({ + container: { + backgroundColor: 'transparent', + }, + addWalletText: { + lineHeight: 26, + letterSpacing: 0.8, + fontSize: 18, + }, + addWalletDescription: { + fontSize: 14, + lineHeight: 20, + letterSpacing: 0.5, + }, + backContainer: { + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 5, + paddingVertical: windowHeight > 680 ? 15 : 7, + }, + backButton: { + height: 20, + width: 20, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + learnMoreContainer: { + borderWidth: 0.5, + borderRadius: 5, + paddingHorizontal: 5, + justifyContent: 'center', + alignItems: 'center', + }, + learnMoreText: { + fontSize: 12, + letterSpacing: 0.6, + alignSelf: 'center', + }, + headerContainer: { + width: windowWidth * 0.85, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + headerInfo: { + paddingLeft: marginLeft ? '10%' : 0, + flexDirection: 'row', + gap: 10, + }, + }); export default KeeperHeader; diff --git a/src/components/NavButton.tsx b/src/components/NavButton.tsx new file mode 100644 index 000000000..414197bc5 --- /dev/null +++ b/src/components/NavButton.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Box, useColorMode } from 'native-base'; +import LinkIcon from 'src/assets/images/link.svg'; +import openLink from 'src/utils/OpenLink'; +import { StyleSheet, TouchableOpacity } from 'react-native'; +import { hp, wp } from 'src/constants/responsive'; +import Text from './KeeperText'; + +type NavButtonProps = { + icon: Element; + heading: string; + link: string; +}; + +function NavButton({ icon, heading, link }: NavButtonProps) { + const { colorMode } = useColorMode(); + + return ( + openLink(link)}> + + + {icon} + + + {heading} + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + NavButtonContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingHorizontal: 15, + height: hp(45), + width: wp(166), + borderRadius: 8, + marginBottom: hp(8), + alignItems: 'center', + }, + headingWrapper: { + flexDirection: 'row', + alignItems: 'center', + gap: 7, + }, + heading: { + fontWeight: '400', + fontSize: 13, + letterSpacing: 0.79, + }, + link: { + justifyContent: 'center', + alignItems: 'center', + }, +}); + +export default NavButton; diff --git a/src/components/Note/Note.tsx b/src/components/Note/Note.tsx index c2929f6bf..efdd35a56 100644 --- a/src/components/Note/Note.tsx +++ b/src/components/Note/Note.tsx @@ -16,7 +16,7 @@ function Note({ title = 'Note', subtitle, subtitleColor = 'GreyText', width = '1 return ( - + {title} diff --git a/src/components/NotificationStack.tsx b/src/components/NotificationStack.tsx new file mode 100644 index 000000000..e48284e9a --- /dev/null +++ b/src/components/NotificationStack.tsx @@ -0,0 +1,306 @@ +import React, { useEffect, useState } from 'react'; +import { Alert, Dimensions, StyleSheet, View } from 'react-native'; +import { + Directions, + Gesture, + GestureDetector, + GestureHandlerRootView, +} from 'react-native-gesture-handler'; +import Animated, { + SharedValue, + interpolate, + runOnJS, + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated'; +import { Box, useColorMode } from 'native-base'; +import { uaiType } from 'src/models/interfaces/Uai'; +import useUaiStack from 'src/hooks/useUaiStack'; +import { useDispatch } from 'react-redux'; +import { uaiActioned } from 'src/store/sagaActions/uai'; +import { CommonActions, useNavigation } from '@react-navigation/native'; +import useVault from 'src/hooks/useVault'; +import useToastMessage from 'src/hooks/useToastMessage'; +import ToastErrorIcon from 'src/assets/images/toast_error.svg'; +import InheritanceKeyServer from 'src/services/operations/InheritanceKey'; +import Text from './KeeperText'; +import KeeperModal from './KeeperModal'; +import ActivityIndicatorView from './AppActivityIndicator/ActivityIndicatorView'; +import UAIView from 'src/screens/Home/components/HeaderDetails/components/UAIView'; +import { windowHeight } from 'src/constants/responsive'; +import { TransferType } from 'src/models/enums/TransferType'; + +const { width } = Dimensions.get('window'); + +const _size = width * 0.95; +const layout = { + borderRadius: 16, + width: _size, + height: 90, + spacing: 12, + cardsGap: 10, +}; +const maxVisibleItems = 3; + +const nonSkippableUAIs = [uaiType.DEFAULT, uaiType.SECURE_VAULT]; + +type CardProps = { + totalLength: number; + index: number; + info: any; + activeIndex: SharedValue; +}; + +function Card({ info, index, totalLength, activeIndex }: CardProps) { + const { colorMode } = useColorMode(); + + const dispatch = useDispatch(); + const navigtaion = useNavigation(); + const { showToast } = useToastMessage(); + + const [showModal, setShowModal] = useState(false); + const [modalActionLoader, setmodalActionLoader] = useState(false); + const [uaiConfig, setUaiConfig] = useState({}); + + const { activeVault } = useVault({ getFirst: true }); + + const animations = useAnimatedStyle(() => { + return { + position: 'absolute', + zIndex: totalLength - index, + opacity: interpolate( + activeIndex.value, + [index - 1, index, index + 1], + [1 - 1 / maxVisibleItems, 1, 1] + ), + transform: [ + { + translateY: interpolate( + activeIndex.value, + [index - 1, index, index + 1], + [layout.cardsGap, 0, layout.height + layout.cardsGap] + ), + }, + { + scale: interpolate(activeIndex.value, [index - 1, index, index + 1], [0.96, 1, 1]), + }, + ], + }; + }); + + const getUaiTypeDefinations = (type: string, entityId?: string) => { + switch (type) { + case uaiType.RELEASE_MESSAGE: + return { + modalDetails: { + heading: 'Update application', + btnText: 'Update', + }, + cta: () => { + setShowModal(false); + uaiSetActionFalse(); + }, + }; + case uaiType.VAULT_TRANSFER: + return { + modalDetails: { + heading: 'Trasfer to Vault', + subTitle: + 'Your Auto-transfer policy has triggered a transaction that needs your approval', + btnText: ' Transfer Now', + }, + heading: 'Trasfer to Vault', + cta: (info) => { + activeVault + ? navigtaion.navigate('SendConfirmation', { + uaiSetActionFalse, + walletId: info.entityId, + transferType: TransferType.WALLET_TO_VAULT, + }) + : showToast('No vaults found', ); + + setShowModal(false); + }, + }; + case uaiType.SECURE_VAULT: + return { + heading: 'Secure you vault', + cta: () => { + navigtaion.dispatch( + CommonActions.navigate({ name: 'VaultSetup', merge: true, params: {} }) + ); + }, + }; + case uaiType.SIGNING_DEVICES_HEALTH_CHECK: + return { + heading: 'Pending healthcheck', + cta: () => { + navigtaion.navigate('VaultDetails', { vaultId: activeVault.id }); + }, + }; + case uaiType.IKS_REQUEST: + return { + modalDetails: { + heading: 'Inheritance Key request', + subTitle: `Request:${entityId}`, + displayText: + 'There is a request by someone for accessing the Inheritance Key you have set up using this app', + btnText: 'Decline', + }, + heading: 'Inheritance Key request', + cta: async (entityId) => { + try { + setmodalActionLoader(true); + if (entityId) { + const res = await InheritanceKeyServer.declineInheritanceKeyRequest(entityId); + if (res?.declined) { + showToast('IKS declined'); + uaiSetActionFalse(); + setShowModal(false); + } else { + Alert.alert('Something went Wrong!'); + } + } + } catch (err) { + Alert.alert('Something went Wrong!'); + console.log('Error in declining request'); + } + setShowModal(false); + setmodalActionLoader(false); + }, + }; + case uaiType.DEFAULT: + return { + heading: 'Secure you vault', + cta: () => { + navigtaion.navigate('ManageSigners'); + }, + }; + default: + return { + cta: () => { + activeVault + ? navigtaion.navigate('VaultDetails', { vaultId: activeVault.id }) + : showToast('No vaults found', ); + }, + }; + } + }; + + useEffect(() => { + setUaiConfig(getUaiTypeDefinations(info?.uaiType, info?.entityId)); + }, [info]); + + const uaiSetActionFalse = () => { + dispatch(uaiActioned(info.id)); + }; + + const pressHandler = () => { + if (info?.isDisplay) { + setShowModal(true); + } else { + uaiConfig?.cta(info); + } + }; + + return ( + <> + + + + + + + setShowModal(false)} + title={uaiConfig?.modalDetails?.heading} + subTitle={uaiConfig?.modalDetails?.subTitle} + buttonText={uaiConfig?.modalDetails?.btnText} + buttonTextColor="light.white" + buttonCallback={() => uaiConfig?.cta(info?.entityId)} + Content={() => {info?.displayText}} + /> + + + ); +} + +export default function NotificationStack() { + const activeIndex = useSharedValue(0); + const { uaiStack } = useUaiStack(); + + const removeCard = () => {}; + + const flingUp = Gesture.Fling() + .direction(Directions.UP) + .onStart(() => { + runOnJS(removeCard)(); + }); + + return ( + + + + {(uaiStack || []).map((c, index) => { + return ( + + ); + })} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + position: 'relative', + top: windowHeight / 8, + }, + viewWrapper: { + alignItems: 'center', + flex: 1, + justifyContent: 'flex-end', + }, + card: { + borderRadius: layout.borderRadius, + width: layout.width, + height: layout.height, + justifyContent: 'center', + }, + title: { + fontSize: 12, + fontWeight: '600', + }, + content: { + fontSize: 14, + fontWeight: '600', + width: '50%', + }, + contentContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + skip: { + fontSize: 12, + }, + buttonContainer: { + flexDirection: 'row', + gap: 20, + }, +}); diff --git a/src/components/OptionCard.tsx b/src/components/OptionCard.tsx index b1113428b..e1ec6b054 100644 --- a/src/components/OptionCard.tsx +++ b/src/components/OptionCard.tsx @@ -3,49 +3,80 @@ import { Box, HStack, Pressable, VStack, useColorMode } from 'native-base'; import React from 'react'; import RightArrowIcon from 'src/assets/images/icon_arrow.svg'; import { windowWidth } from 'src/constants/responsive'; +import { StyleSheet } from 'react-native'; type OptionProps = { title: string; description: string; - Icon?: any; - callback: () => void; + callback?: () => void; + titleColor?: string; + descriptionColor?: string; + Icon?: Element; + LeftIcon?: Element; + disabled?: boolean; + CardPill?: Element; }; -export const OptionCard = ({ title, description, Icon, callback }: OptionProps) => { +export function OptionCard({ + title, + description, + Icon, + callback = null, + titleColor, + descriptionColor, + LeftIcon, + disabled = false, + CardPill, +}: OptionProps) { const { colorMode } = useColorMode(); return ( - + - - - {title} - - {description && ( + + {LeftIcon && LeftIcon} + - {description} + {title} - )} - - - {Icon ? Icon : } - + {description && ( + + {description} + + )} + + + {CardPill ? ( + CardPill + ) : ( + + {Icon || } + + )} ); -}; +} + +const styles = StyleSheet.create({ + iconContainer: { + alignItems: 'center', + justifyContent: 'center', + gap: 5, + }, +}); export default OptionCard; diff --git a/src/components/SettingComponent/CountryCard.tsx b/src/components/SettingComponent/CountryCard.tsx deleted file mode 100644 index 7b3d21a3a..000000000 --- a/src/components/SettingComponent/CountryCard.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import Text from 'src/components/KeeperText'; -import { Box, useColorMode } from 'native-base'; -import Switch from 'src/components/Switch/Switch'; - -function CountryCard(props) { - const { colorMode } = useColorMode(); - - return ( - - - - {props.title} - - - {props.description} - - - - props.onSwitchToggle(value)} value={props.value} /> - - - ); -} - -export default CountryCard; diff --git a/src/components/SettingComponent/CountrySwitchCard.tsx b/src/components/SettingComponent/CountrySwitchCard.tsx index 2449c9215..7c28f3a00 100644 --- a/src/components/SettingComponent/CountrySwitchCard.tsx +++ b/src/components/SettingComponent/CountrySwitchCard.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import Text from 'src/components/KeeperText'; import { Box, useColorMode } from 'native-base'; +import Text from 'src/components/KeeperText'; +import { StyleSheet } from 'react-native'; function CountrySwitchCard(props) { const { colorMode } = useColorMode(); @@ -8,16 +9,14 @@ function CountrySwitchCard(props) { - + {props.title} - + {props.description} @@ -25,4 +24,15 @@ function CountrySwitchCard(props) { ); } +const styles = StyleSheet.create({ + titleText: { + fontSize: 13, + letterSpacing: 0.13, + }, + descriptionText: { + fontSize: 12, + letterSpacing: 0.12, + }, +}); + export default CountrySwitchCard; diff --git a/src/components/SignerCard.tsx b/src/components/SignerCard.tsx new file mode 100644 index 000000000..522a96772 --- /dev/null +++ b/src/components/SignerCard.tsx @@ -0,0 +1,88 @@ +import { Box, Pressable, useColorMode } from 'native-base'; +import Text from './KeeperText'; +import { StyleSheet } from 'react-native'; + +type SignerCardProps = { + walletName: string; + walletDescription: string; + icon: Element; + selectedCard: string; + onCardSelect: (cardName: string) => void; +}; + +function SignerCard({ + walletName, + walletDescription, + icon, + selectedCard, + onCardSelect, +}: SignerCardProps) { + const isSelected = selectedCard === walletName; + const { colorMode } = useColorMode(); + return ( + onCardSelect(walletName)} + > + + + + {icon} + + + {walletName} + + + {walletDescription} + + + + ); +} + +const styles = StyleSheet.create({ + walletContainer: { + width: 114, + padding: 10, + height: 125, + alignItems: 'flex-start', + borderRadius: 10, + borderWidth: 0.5, + }, + walletName: { + fontSize: 12, + fontWeight: '400', + }, + walletDescription: { + fontSize: 11, + }, + circle: { + width: 20, + height: 20, + borderRadius: 20 / 2, + alignSelf: 'flex-end', + borderWidth: 1, + }, + detailContainer: { + gap: 2, + marginTop: 15, + }, + iconWrapper: { + width: 34, + height: 34, + borderRadius: 30, + justifyContent: 'center', + alignItems: 'center', + }, +}); + +export default SignerCard; diff --git a/src/components/TransactionElement.tsx b/src/components/TransactionElement.tsx index f42d83fc2..75f73f77c 100644 --- a/src/components/TransactionElement.tsx +++ b/src/components/TransactionElement.tsx @@ -3,16 +3,16 @@ import { Box, useColorMode } from 'native-base'; import { StyleSheet, TouchableOpacity } from 'react-native'; import moment from 'moment'; -import useBalance from 'src/hooks/useBalance'; import { hp, wp } from 'src/constants/responsive'; import { Transaction } from 'src/core/wallets/interfaces'; -import IconRecieve from 'src/assets/images/icon_received.svg'; -import UnconfirmedIcon from 'src/assets/images/pending.svg'; -import IconSent from 'src/assets/images/icon_sent.svg'; +import IconSent from 'src/assets/images/icon_sent_red.svg'; +import IconRecieve from 'src/assets/images/icon_recieved_red.svg'; +import TransactionPendingIcon from 'src/assets/images/transaction_pending.svg'; + import IconArrow from 'src/assets/images/icon_arrow_grey.svg'; import Text from 'src/components/KeeperText'; -import CurrencyInfo from 'src/screens/HomeScreen/components/CurrencyInfo'; +import CurrencyInfo from 'src/screens/Home/components/CurrencyInfo'; function TransactionElement({ transaction, @@ -25,39 +25,33 @@ function TransactionElement({ }) { const { colorMode } = useColorMode(); const date = moment(transaction?.date)?.format('DD MMM YY • HH:mm A'); - const { getSatUnit, getBalance, getCurrencyIcon } = useBalance(); return ( - {transaction?.transactionType === 'Received' ? : } + + {transaction.confirmations === 0 && ( + + + + )} + {transaction?.transactionType === 'Received' ? : } + + + {date} + {transaction?.txid} - - {date} - - {transaction.confirmations > 0 ? null : ( - - - - )} - {/* {getCurrencyIcon(BtcBlack, 'dark')} - - {getBalance(transaction?.amount)} - - {getSatUnit()} - - */} { setEnableSelection(!enableSelection); diff --git a/src/components/UTXOsComponents/UTXOList.tsx b/src/components/UTXOsComponents/UTXOList.tsx index ce47040c9..e4b80eb2a 100644 --- a/src/components/UTXOsComponents/UTXOList.tsx +++ b/src/components/UTXOsComponents/UTXOList.tsx @@ -17,7 +17,7 @@ import ToastErrorIcon from 'src/assets/images/toast_error.svg'; import useToastMessage from 'src/hooks/useToastMessage'; import { useAppSelector } from 'src/store/hooks'; import useLabelsNew from 'src/hooks/useLabelsNew'; -import CurrencyInfo from 'src/screens/HomeScreen/components/CurrencyInfo'; +import CurrencyInfo from 'src/screens/Home/components/CurrencyInfo'; import { LocalizationContext } from 'src/context/Localization/LocContext'; function Label({ @@ -124,10 +124,7 @@ function UTXOElement({ style={styles.utxoCardContainer} onPress={() => { if (enableSelection && !item.confirmed) { - showToast( - walletTranslation.intiatePremixToastMsg, - - ); + showToast(walletTranslation.intiatePremixToastMsg, ); return; } if (allowSelection) { diff --git a/src/components/UploadFile.tsx b/src/components/UploadFile.tsx index 14d4639a8..903ad4917 100644 --- a/src/components/UploadFile.tsx +++ b/src/components/UploadFile.tsx @@ -31,7 +31,7 @@ function UploadFile({ fileHandler }) { > - Import a BSMS or Output Descriptor File + Import a BSMS or Wallet Configuration File diff --git a/src/components/WalletCard.tsx b/src/components/WalletCard.tsx new file mode 100644 index 000000000..a99e11a50 --- /dev/null +++ b/src/components/WalletCard.tsx @@ -0,0 +1,108 @@ +import { Box, Pressable, useColorMode } from 'native-base'; +import React, { StyleSheet, ViewStyle } from 'react-native'; +import Text from './KeeperText'; +import Fonts from 'src/constants/Fonts'; + +type WalletCardProps = { + id: number; + walletName: string; + walletDescription: string; + icon: Element; + selectedIcon: Element; + selectedCard: number; + onCardSelect: (cardName: number) => void; + arrowStyles: ViewStyle; +}; + +function WalletCard({ + id, + walletName, + walletDescription, + icon, + selectedIcon, + selectedCard, + onCardSelect, + arrowStyles, +}: WalletCardProps) { + const { colorMode } = useColorMode(); + const isSelected = selectedCard === id; + + return ( + onCardSelect(id)}> + + + + {isSelected ? selectedIcon : icon} + + + + {walletName} + + + {walletDescription} + + + + + {isSelected && ( + + )} + + ); +} + +const styles = StyleSheet.create({ + walletContainer: { + width: 114, + height: 125, + padding: 10, + borderRadius: 10, + borderWidth: 0.5, + }, + circle: { + width: 34, + height: 34, + borderRadius: 34 / 2, + alignItems: 'center', + justifyContent: 'center', + }, + detailContainer: { + alignItems: 'flex-start', + justifyContent: 'center', + gap: 5, + marginTop: 15, + }, + arrow: { + marginTop: -10, + width: 0, + height: 0, + backgroundColor: 'transparent', + borderStyle: 'solid', + borderTopWidth: 20, + borderLeftWidth: 10, + borderRightWidth: 10, + borderLeftColor: 'transparent', + borderRightColor: 'transparent', + }, + walletName: { + fontSize: 12, + }, +}); + +export default WalletCard; diff --git a/src/components/WalletFingerPrint.tsx b/src/components/WalletFingerPrint.tsx new file mode 100644 index 000000000..747dadac0 --- /dev/null +++ b/src/components/WalletFingerPrint.tsx @@ -0,0 +1,77 @@ +import React, { useContext } from 'react'; +import { Box, Pressable, useColorMode } from 'native-base'; +import CopyIcon from 'src/assets/images/copy.svg'; +import { StyleSheet } from 'react-native'; +import Clipboard from '@react-native-community/clipboard'; +import useToastMessage from 'src/hooks/useToastMessage'; +import TickIcon from 'src/assets/images/icon_tick.svg'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import Text from './KeeperText'; +import { hp } from 'src/constants/responsive'; + +type Props = { + fingerprint: string; + title?: string; +}; + +function WalletFingerprint({ title, fingerprint }: Props) { + const { colorMode } = useColorMode(); + const { showToast } = useToastMessage(); + + const { translations } = useContext(LocalizationContext); + const { wallet: walletTranslation } = translations; + + return ( + + + + {title ? title : 'Wallet Fingerprint'} + + + {fingerprint} + + + { + Clipboard.setString(fingerprint); + showToast(walletTranslation.walletIdCopied, ); + }} + > + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + width: '90%', + borderRadius: 10, + height: 60, + marginVertical: hp(20), + }, + heading: { + fontSize: 14, + }, + value: { + fontSize: 16, + }, + iconContainer: { + borderRadius: 10, + margin: 2, + alignItems: 'center', + justifyContent: 'center', + width: 56, + height: '95%', + }, + textContainer: { + margin: 10, + }, +}); + +export default WalletFingerprint; diff --git a/src/components/XPub/ShowXPub.tsx b/src/components/XPub/ShowXPub.tsx index 622c0ce96..44622d8e9 100644 --- a/src/components/XPub/ShowXPub.tsx +++ b/src/components/XPub/ShowXPub.tsx @@ -9,10 +9,11 @@ import { wp, hp } from 'src/constants/responsive'; import QRCode from 'react-native-qrcode-svg'; import CopyIcon from 'src/assets/images/icon_copy.svg'; -import { KeeperApp } from 'src/models/interfaces/KeeperApp'; import { getCosignerDetails } from 'src/core/wallets/factories/WalletFactory'; import { Wallet } from 'src/core/wallets/interfaces/wallet'; import Note from '../Note/Note'; +import { getKeyExpression } from 'src/core/utils'; +import { XpubTypes } from 'src/core/wallets/enums'; function ShowXPub({ wallet, @@ -22,7 +23,6 @@ function ShowXPub({ noteSubText, copyable = true, cosignerDetails = false, - keeper, }: { data: string; wallet?: Wallet; @@ -31,7 +31,6 @@ function ShowXPub({ noteSubText?: string; copyable: boolean; cosignerDetails?: boolean; - keeper?: KeeperApp; }) { const { colorMode } = useColorMode(); const { translations } = useContext(LocalizationContext); @@ -41,7 +40,15 @@ function ShowXPub({ useEffect(() => { if (cosignerDetails) { setTimeout(() => { - setDetails(JSON.stringify(getCosignerDetails(wallet, keeper.id))); + const details = getCosignerDetails(wallet, true); + setDetails( + getKeyExpression( + details.mfp, + details.xpubDetails[XpubTypes.P2WPKH].derivationPath, + details.xpubDetails[XpubTypes.P2WPKH].xpub, + false + ) + ); }, 200); } else { setDetails(data); diff --git a/src/components/onBoarding/OnboardingSlideComponent.tsx b/src/components/onBoarding/OnboardingSlideComponent.tsx index 320a14941..870b2b828 100644 --- a/src/components/onBoarding/OnboardingSlideComponent.tsx +++ b/src/components/onBoarding/OnboardingSlideComponent.tsx @@ -11,7 +11,7 @@ function OnboardingSlideComponent(props) { return ( - + {props.title} @@ -42,11 +42,12 @@ const styles = StyleSheet.create({ width, alignItems: 'center', justifyContent: 'center', - padding: 5, + paddingHorizontal: 5, + paddingBottom: 5, + paddingTop: 40, flex: 1, }, titleWrapper: { - flex: 0.2, marginHorizontal: hp(20), }, illustartionWrapper: { diff --git a/src/constants/defaultData.tsx b/src/constants/defaultData.tsx index cf0dd12a9..60c097254 100644 --- a/src/constants/defaultData.tsx +++ b/src/constants/defaultData.tsx @@ -7,45 +7,74 @@ export const securityTips = [ { title: 'Introducing Whirlpool', subTitle: - 'Whirlpool gives you forward looking privacy by breaking deterministic links of your future transactions from past ones', + 'Whirlpool gives you forward looking privacy by breaking deterministic links of your future transactions from past ones.', assert: , message: - 'For increased privacy and security, remix sats a few times, then transfer them to the Vault', + 'For increased privacy and security, remix sats a few times, then transfer them to the vault.', }, { - title: 'Connecting to Node', - subTitle: 'Interact with the bitcoin network more privately and securely', + title: 'Connecting to a bitcoin node', + subTitle: 'Interact with the bitcoin network more privately and securely.', assert: , - message: - 'Eliminate reliance on third parties to validate financial transactions and hold your funds.', + message: 'Eliminate reliance on third parties to hold your funds and validating transactions.', }, { title: 'Security Tip', subTitle: - 'You can get a receive address directly from a signing device and do not have to trust the Keeper app', + 'You can get a receive address directly from a signer and do not have to trust the Keeper app', assert: , message: 'This will mean that the funds are received at the correct address', }, { title: 'Introducing Inheritance Tools', subTitle: - 'Use Inheritance documents for your inheritance planning. Inheritance Key is an assisted key that can be availed by your heir', + 'Use Inheritance documents for your inheritance planning. Inheritance Key is an assisted key that can be availed by your heir.', assert: , - message: 'Consult your estate planner for incorporating documents from this app in your will', + message: 'Consult your estate planner for incorporating documents from this app in your will.', }, { - title: 'Keep your signing devices safe', - subTitle: 'Signing devices are what control your funds.', + title: 'Keep your signers safe', + subTitle: 'Signers are what control your funds.', assert: , - message: 'These are generally offline and to keep them secure is your responsibility. ', + message: + 'These are generally offline and to keep them secure is your responsibility. Losing them may lead to permanent loss of your bitcoin.', }, { title: 'Security Tip', subTitle: - 'Recreate the multisig Vault on more coordinators. Receive a small amount and send a part of it. Check whether the balances are appropriately reflected across all the coordinators after each step', + 'Recreate the multisig vault on more coordinators. Receive a small amount and send a part of it. Check whether the balances are appropriately reflected across all the coordinators after each step', assert: , message: 'Testing out your setup before using it is always a good idea', }, + { + title: 'Security Tip', + subTitle: + 'Check the send-to address on a signing device you are going to use to sign the transaction.', + assert: , + message: + 'This ensures that the signed transaction has the intended recipient and the address was not swapped', + }, + { + title: 'Logging in to your Keeper app', + subTitle: 'Shake your device or take a screenshot to send feedback.', + assert: , + message: + 'This feature is *only* for the testnet version of the app. The developers will get your message along with other information from the app.', + }, + { + title: 'Confirming your subscription', + subTitle: 'Unlock inheritance planning at the Diamond Hands tier.', + assert: , + message: + 'You can change your subscription at anytime from within the app or from the subscription details in your profile.', + }, + { + title: 'Keep your signing devices safe', + subTitle: 'Signing devices are what control your funds.', + assert: , + message: + 'These are generally offline and to keep them secure is your responsibility. Losing them may lead to permanent loss of your bitcoin.', + }, ]; export const getSecurityTip = () => { const selected = Math.floor(Math.random() * securityTips.length); // Comment for creating wallet modal WP diff --git a/src/context/Localization/language/en.json b/src/context/Localization/language/en.json index 16b63d81a..4a931e3fd 100644 --- a/src/context/Localization/language/en.json +++ b/src/context/Localization/language/en.json @@ -11,7 +11,7 @@ "add": "Add", "sign": "Sign", "knowMore": "Know more", - "confirmProceed": "Confirm & Proceed", + "confirmProceed": "Confirm & Send", "skip": "Skip", "search": "Search", "share": "Share", @@ -59,7 +59,7 @@ "change": "Change", "save": "Save", "contactCreated": "Contact Created", - "FAQs": "FAQ's", + "FAQs": "FAQ’s & Help", "TermsConditions": "Terms and Conditions", "PrivacyPolicy": "Privacy Policy", "ViewSeed": "View Seed", @@ -82,11 +82,11 @@ "transactions": "Transactions", "viewAll": "View All", "addNewWalletOrImport": "Add a new wallet or import one", - "noTransYet": "No transactions yet.", + "noTransYet": "You have no transactions yet", "pullDownRefresh": "Pull down to refresh", "securityTips": "Security Tip", "emptyStateModalSubtitle": "Recreate the multisig on more coordinators. Receive a small amount and send a part of it. Check the balances are appropriately reflected across all the coordinators after each step.", - "addSignDeviceUpgrade": "Add signing device to upgrade", + "addSignDeviceUpgrade": "Add signer to upgrade", "path": "Path", "purpose": "Purpose", "choosePurpose": "Choose Purpose", @@ -95,7 +95,10 @@ "receiveTestSats": "Receiving test sats", "upgradeNow": "Upgrade Now", "somethingWrong": "Something went wrong", - "showAsQR": "Show as QR" + "showAsQR": "Show as QR", + "addNow": "ADD NOW", + "runHealthCheck": "Run Health Check", + "action": "Action" }, "noInternet": { "no": "No Internet", @@ -204,7 +207,7 @@ "HexaPaywalletsecurity": "You are about the enhance your Hexa Pay wallet's security", "amountdesc": "The QR code will change once an amount is entered", "ConvertedAmount": "Converted Amount", - "reflectSats": "It would take some time for the sats to reflect in your account based on the network conditions", + "reflectSats": "It would take some time for sats to reflect in your wallet based on fees paid by sender", "the_wallet_backup": "The wallet backup is ", "availableToSpend": "Available to spend", "Transactions": "Transactions", @@ -258,17 +261,17 @@ "Transferbitcoincustody": "Transfer bitcoin custody", "ActivateTransfer": "Activate Transfer", "Thebestinheritance": "The best inheritance in history", - "UpgradetoElitetier": "Upgrade to Elite tier and setup the Vault with 5 signing devices", + "UpgradetoElitetier": "Upgrade to Elite tier and setup the vault with 5 signers", "ActivateInheritance": "Activate Inheritance", "Downloadandsafelykeepinheritencedocuments": "Download and safely keep inheritence documents. Safekeeping best practices. Will template for digital assets. Inheritance recovery instructions", "Setupfalserecoveryalert": "Setup false recovery alert (optional)", "tryingtorecoveryourwallet": "If someone is trying to recover your wallet, make sure you get notified to approve or deny the recovery", "IndependentRecovery": "Independent Recovery", - "UnderstandhowyoucanrecoveryourVault": "Understand how you can recover your Vault even without the Keeper app or any service from the company", + "UnderstandhowyoucanrecoveryourVault": "Understand how you can recover your vault even without the Keeper app or any service from the company", "Practicehealthcheck": "Practice health check", - "Makessureyousignersareaccessible": "Make sure you signing devices are accessible. Change them if that is not the case", + "Makessureyousignersareaccessible": "Make sure you signers are accessible. Change them if that is not the case", "inheritanceSupport": "Inheritance Support", - "inheritanceSupportSubTitle": "Keeper provides you with the tips and tools you need to include the Vault in your estate planning", + "inheritanceSupportSubTitle": "Keeper provides you with the tips and tools you need to include the vault in your estate planning", "consultYourEstate": "Contact your estate planning attorney and advisors to ensure the documents provided here are suitable for your needs and are as per your jurisdiction", "secureBequeathBitcoin": "Plan your bitcoin inheritance" }, @@ -277,6 +280,7 @@ "Startstackingsats": "Start stacking sats", "SinglesigWallet": "Single-sig Wallet", "TransferPolicy": "Transfer Policy", + "TransferPolicyDesc": "Trigger transfer from hot wallet to vault", "Foryourdaytodayspends": "For your day to day spends", "MultisigWallet": "Multi-sig Wallet", "Forlongtermholding": "For long term holding", @@ -289,7 +293,7 @@ "BlueWallet": "Blue Wallet", "MunnWallet": "Munn Wallet", "Blockchain": "Blockchain.com", - "AddVault": "Add a Vault", + "AddVault": "Add a vault", "AddWallet": "Add a Wallet", "Setupawalletforyoubitcoin": "Set up a wallet for you bitcoin", "Creatingyourwallet": "Creating your wallet", @@ -311,13 +315,13 @@ "ImportAWallet": "Import a Wallet", "AddNewWalletDescription": "Name and description for the wallet", "WalletNamePlaceHolder": "Enter wallet name", - "WalletDescriptionPlaceholder": "Enter wallet description", + "WalletDescriptionPlaceholder": "Add a description (Optional)", "AutoTransferInitiated": "Auto transfer initiated at (optional)", - "AutoTransferInitiatedDesc": "When the wallet balance crosses this amount, a transfer to the Vault is initiated for user approval", + "AutoTransferInitiatedDesc": "When the wallet balance crosses this amount, a transfer to the vault is initiated for user approval", "WalletDetails": "Wallet Details", "walletDetailsSubTitle": "Name, details and transfer policy", "EditWalletDeatils": "Edit wallet name and description", - "changeWalletDetails": "Change wallet name & description", + "changeWalletDetails": "Wallet Name & Description", "TransferSuccessful": "Transfer Successful!", "initiatetransfer": "Are you sure you want to initiate transfer to", "XPubTitle": "Wallet xPub", @@ -328,12 +332,12 @@ "confirmPassSubTitle": "To show wallet seed words", "SendSuccess": "Send Successful", "ViewDetails": "View Details", - "approveTransVault": "Approve Transfer to Vault", + "approveTransVault": "Approve Transfer to vault", "approveTransVaultSubtitle": "Your auto-transfer policy has triggered a transaction attransaction, at 100,000 sats that needs your approval", "TransNow": "Transfer Now", "AddWalletCosigner": "Add Wallet as co-signer", - "AddWalletCosignerSubTitle": "You are about to add this wallet as a co-signer for a Keeper app Vault.", - "AddWalletCosignerParagraph": "Please back up this wallet. If you want to delete the app from this phone, please change your signing device on the other Keeper app before deleting this one.", + "AddWalletCosignerSubTitle": "You are about to add this wallet as a co-signer for a Keeper app vault.", + "AddWalletCosignerParagraph": "Please back up this wallet. If you want to delete the app from this phone, please change your signer on the other Keeper app before deleting this one.", "whirlpoolUtxoTitle": "Whirlpool & UTXOs", "whirlpoolUtxoSubTitle": "Manage wallet UTXOs and use Whirlpool", "addWallet": "Add Wallet", @@ -345,7 +349,7 @@ "addCollabWalletParagraph": "Please ensure that Keeper is properly backed up to ensure your bitcoin's security", "selectForMix": "Select for Mix", "selectForRemix": "Select for Remix", - "remixVault": "Remix to Vault", + "remixVault": "Remix to vault", "selectToSend": "Select to Send", "noUTXOYet": "No UTXOs yet", "noUTXOYetSubTitle": "UTXOs from all your Tx0s land here.", @@ -368,9 +372,9 @@ "TransPolicyChange": "Transfer Policy Changed", "walletSeedWord": "Wallet Seed Words", "walletSeedWordSubTitle": "Use to link external wallets to Keeper", - "showCoSignerDetails": "Show co-signer Details", - "showCoSignerDetailsSubTitle": "Use this wallet as a co-signer with other vaults", - "actCoSigner": "Act as co-signer", + "showCoSignerDetails": "Show Co-Signer Details", + "showCoSignerDetailsSubTitle": "Use this wallet as a co-signer", + "actCoSigner": "Act as Co-Signer", "recieveTestSats": "Receive Test Sats", "recieveTestSatSubTitle": "Receive Test Sats to this address", "walletSettingNote": "These settings are for your selected wallet only and does not affect other wallets", @@ -378,8 +382,9 @@ "receiveSubTitle": "Native segwit address", "receiveAddress": "Receive Address", "addressCopied": "Address Copied Successfully", + "walletIdCopied": "Fingerprint Copied Successfully", "addSpecificInvoiceAmt": "Add a specific invoice amount", - "addressReceiveDirectly": "You can get a receive address directly from a signing device and do not have to trust the Keeper app", + "addressReceiveDirectly": "You can get a receive address directly from a signer and do not have to trust the Keeper app", "editTransPolicyInfo": " This will trigger a transfer request which you need to approve", "transPolicyCantZero": "Transfer Policy cannot be zero", "updateWalletPath": "Update Wallet Path", @@ -392,46 +397,68 @@ "failToUpdate": "Failed to update", "walletBalanceMsg": "Wallet has balance: changes not allowed", "transactionBroadcasted": "The transaction has been successfully broadcasted", - "sendTransSuccessMsg": " You can view the confirmation status of the transaction on any block explorer or when the Vault transaction list is refreshed" + "sendTransSuccessMsg": " You can view the confirmation status of the transaction on any block explorer or when the vault transaction list is refreshed", + "PRIORITY": "PRIORITY", + "ARRIVALTIME": "ARRIVAL TIME", + "FEE": "FEE", + "transactionPriority": "Transaction Priority", + "addCustomPriority": "Add Custom Priority", + "totalAmount": "Total Amount", + "totalFees": "Total Fees", + "total": "Total", + "highFeeAlert": "High Fee Alert", + "highFeeAlertSubTitle": "Network fee is larger than the amount you are sending", + "networkFee": "Network Fee", + "amtBeingSent": "Amount being sent", + "estimateArrvlTime": "Estimated arrival time", + "sendingFrom": "Sending From", + "sendingFromUtxo": "Sending from selected UTXOs of", + "AvailableToSpend": "Available to spend", + "advanced": "Advanced", + "chooseFromTemplate": "Choose from a template" }, "vault": { - "SetupyourVault": "Setup your Vault", - "VaultDesc": "A signing device is an air gapped device which helps you keep your Vault safe", + "SetupyourVault": "Setup a vault", + "AddCustomMultiSig": "Add Custom MultiSig", + "configureScheme": "Create an m-of-n vault", + "VaultDesc": "A signer is an air gapped device which helps you keep your vault safe", "SetupNow": "Setup Now", - "Addsigner": "Add a signing device", + "Addsigner": "Add a signer", "Vault": "Vault", "Yoursupersecurebitcoin": "Your super secure bitcoin", - "MySigners": "My signing devices", + "MySigners": "My signers", "Usedforsecuringfunds": "Used for securing funds", "Inheritance": "Inheritance", "Setupinheritancetoyoursats": "Set up inheritance to your sats", "Setup": "Setup", "VaultCreated": "Vault Created", - "VaultCreationDesc": "Your Basic Vault has been successfully setup You can start receiving bitcoin in it", - "ViewVault": "View Vault", + "VaultCreationDesc": "Your Basic vault has been successfully setup You can start receiving bitcoin in it", + "ViewVault": "View vault", "AddNow": "Add Now", - "SelectSigner": "Select Signing Device", - "ForVault": "For your Vault", - "VaultInfo": "A signing device can be a hardware wallet or a signing device or an app. Most popular ones are listed above. Want support for more?", + "SelectSigner": "Select a Signer", + "SelectSignerSubtitle": "To store one private key", + "VaultInfo": "A signer can be a hardware wallet or a signer or an app. Most popular ones are listed above. Want support for more?", "CustomPriority": "Custom Priority", "EditDescription": "Edit Description", "Description": "", - "yourVault": "Your Vault", - "toActiveVault": "Add Signing Device to activate your Vault", + "yourVault": "Your vault", + "toActiveVault": "Add signer to activate your vault", "inheritanceTools": "Inheritance Tools", "manageInheritance": "Manage Inheritance key or view documents", - "additionalOptionForSignDevice": "There are also some additional options if you do not have hardware signing devices", - "sendVaultSignDevices": "For sending out of the Vault you will need the signing devices. This means no one can steal your bitcoin in the Vault unless they also have the signing devices", + "additionalOptionForSignDevice": "There are also some additional options if you do not have hardware signers", + "sendVaultSignDevices": "For sending out of the vault you will need the signers. This means no one can steal your bitcoin in the vault unless they also have the signers", "addEmail": "Add Email", + "addEmailPhone": "Add Phone/Email", "addEmailDetails": " Additionally you can provide an email which will be used to notify you when someone tries to access the Inheritance Key", + "addEmailVaultDetail": "Get notified of Key activation and have option to decline use", "walletSetupDetails": "This kind of wallet setup can be used for business partnerships, family funds, or any scenario where joint control of funds is necessary.", - "keeperSupportSigningDevice": "Keeper supports all the popular bitcoin signing devices (Hardware Wallets) that a user can select", - "newVaultCreated": "New Vault Created", + "keeperSupportSigningDevice": "Keeper supports all the popular bitcoin signers (Hardware Wallets) that a user can select", + "newVaultCreated": "New vault Created", "collaborativeWallet": "Collaborative Wallet", - "keeperVault": "Keeper Vault", + "keeperVault": "Keeper vault", "collaborativeWalletMultipleUsers": "Collaborative wallet is designed to enable multiple users to have control over a single wallet, adding a layer of security and efficiency in fund management.", - "signingOldVault": "Old Vaults with the previous signing device configuration will be in the archived list of Vaults", - "cvvSigningServerInfo": "If you lose your authenticator app, use the other Signing Devices to reset the Signing Server" + "signingOldVault": "Old Vaults with the previous signer configuration will be in the archived list of Vaults", + "cvvSigningServerInfo": "If you lose your authenticator app, use the other signers to reset the signer" }, "seed": { "EnterSeed": "Enter Seeds", @@ -445,16 +472,19 @@ "walletRecoverySuccessful": "Wallet Recovery Successful", "desc": "Use these to create any other wallet and that wallet will be linked to Keeper (will show along with other wallets)", "recoveryPhrase": "Recovery Phrase", - "enterRecoveryPhrase": "Enter the Wallet Recovery Phrase", + "backupPhrase": "Recovery Key", + "enterRecoveryPhrase": "Recover an existing Keeper App", + "enterRecoveryPhraseSubTitle": "Enter existing 12-word recovery phrase below", + "enterRecoveryPhraseNote": "Make sure to use the 12-word recovery phrase in private", "recoverYourKeeperApp": "Recovering your Keeper app", - "recoverYourKeeperAppSubTitle": "Recovering your wallets and Vault for the Keeper app", + "recoverYourKeeperAppSubTitle": "Recovering your wallets and vault for the Keeper app", "walletRecoveryPhrase": "Wallet Recovery Phrase", "showXPubNoteSubText": "Losing your Recovery Phrase may result in permanent loss of funds. Store them carefully.", "mobileKeyVerified": "Mobile Key verified successfully", "seedWordVerified": "Seed Words verified successfully" }, "healthcheck": { - "ChangeSigningDevice": "Change signing device", + "ChangeSigningDevice": "Change signer", "HealthCheck": "Perform Health Check", "SkippingHealthCheck": "Skipping Health Check", "EnterCVV": "Enter the CVV", @@ -471,7 +501,7 @@ "Setupandassociate": "Setup and associate", "Associate": "Associate", "CardStatus": "Card Status", - "SetupTitle": "Keep your Coldcard ready", + "SetupTitle": "Setting up Coldcard", "SetupDescription": "Keep your Coldcard ready before proceeding" }, "ledger": { @@ -493,11 +523,11 @@ }, "signingServer": { "choosePolicy": "Choose Policy", - "choosePolicySubTitle": "For the signing server", + "choosePolicySubTitle": "For the signer", "maxNoCheckAmt": "Max no-check amount", - "maxNoCheckAmtSubTitle": "The Signing Server will sign a transaction of this amount or lower, even w/o a 2FA verification code", + "maxNoCheckAmtSubTitle": "The signer will sign a transaction of this amount or lower, even w/o a 2FA verification code", "maxAllowedAmt": "Max allowed amount", - "maxAllowedAmtSubTitle": "If the transaction amount is more than this amount, the Signing Server will not sign it. You will have to use other devices for it" + "maxAllowedAmtSubTitle": "If the transaction amount is more than this amount, the signer will not sign it. You will have to use other devices for it" }, "settings": { "selectCurrency": "Select currency", @@ -509,10 +539,13 @@ "VersionHistorySubTitle": "View the app's version history", "LanguageCountry": "Language & Currency", "LanguageCountrySubTitle": "Select language and currency", + "CurrencyDefaults": "Currency Defaults", + "CurrencyDefaultsSubtitle": "Set preferred defaults", "KeeperCommunityTelegramGroup": "Keeper Community Telegram Group", "Questionsfeedbackandmore": "Questions, feedback and more", "biometricsDesc": "Select app settings", "SatsMode": "Sats Mode", + "SatsModeSubTitle": "Use Sats mode to see balance in sats", "Viewbalancessats": "View your balances in sats", "CountrySettings": "Country Settings", "ChooseKeeperaccesslocation": "Choose Keeper access location", @@ -530,6 +563,8 @@ "walletSettings": "Wallet Settings", "walletSettingsSub": "Your wallet settings & preferences", "walletSettingSubTitle": "Setting for the wallet only", + "BackupSettings": "Backup Settings", + "BackupSettingSubTitle": "Setting your backup", "AppInfo": "App Info", "AppInfoSub": "Hexa app version number and details", "Disconnectyour": "Disconnect your own node and connect to Hexa node", @@ -548,6 +583,8 @@ "ChangeCurrency": "Currency & Language", "Chooseyourcurrency": "Choose your currency & language", "AlternateCurrency": "Alternate Currency", + "FiatCurrency": "Fiat Currency", + "Seebalance": "See balance in your local fiat currency", "LanguageSettings": "Language Settings", "Chooseyourlanguage": "Choose your language preference", "HelpUstranslate": "Help us translate better", @@ -570,6 +607,9 @@ "desc": "It would take some time for the sats to reflect in your account based on the network condition", "nodeSettings": "Node Settings", "nodeSettingsSubtitle": "Configure your node settings", + "SecurityAndLogin": "Security & Login", + "SecurityAndLoginSubtitle": "App improvement and ease of access", + "AppLevelSettings": "App-level settings", "nodeSettingUsedSoFar": "Node settings used so far", "connectToMyNode": "Connect to my node", "connectToMyNodeSubtitle": "Disable to use Keeper's node", @@ -589,8 +629,8 @@ "nodeConnectionFailure": "Unable to connect to Electrum Server. Invalid node details or, not known", "ManageWallets": "Manage Wallets", "ManageWalletsSub": "Hide wallets and unhide them.", - "settingsSubTitle": "Configure your app here", - "appBackup": "App Backup", + "settingsSubTitle": "Customize your app", + "appBackup": "Recovery Key", "torSettingTitle": "Tor Settings", "torSettingSubTitle": "Configure in-app Tor and Orbot", "keeperTelegram": "Keeper Telegram", @@ -605,30 +645,42 @@ "checkStatus": "Check Status", "torSettingsNoteSubTitle": "Some WiFi networks use settings that do not let your device connect to Tor. If you get constant errors, try changing to mobile network or check your network settings", "orbotConnection": "Orbot Connection", - "orbotConnectionSubTitle": "To connect to Tor via Orbot, you need to have the Orbot app installed on your device." + "orbotConnectionSubTitle": "To connect to Tor via Orbot, you need to have the Orbot app installed on your device.", + "PrivacyDisplay": "Privacy & Display", + "PrivacyDisplaySubTitle": "Dark mode and biometrics", + "networkSettings": "Network Settings", + "networkSettingSubTitle": "Node and Tor in Network Setting", + "networkSettingConfigSubTitle": "Configure your node and tor settings", + "satsModeSubTitle": "Enable to see balance in sats", + "shareAnalytics": "Share Analytics & App Data", + "analyticsDescription": "No personal or bitcoin data is shared", + "changePasscode": "Change Passcode", + "changePasscodeDescription": "Easy to remember; hard to guess" }, "onboarding": { "Comprehensive": "Comprehensive", "security": "security", "slide01Title": "for\n your bitcoin keys", "privacy": "privacy", - "slide01Paragraph": "Use trusted signing devices (hardware wallets) for air-gapped signing or multi-sig. Add BIP 85 hot wallets or set up inheritance.", + "slide01Paragraph": "Use trusted signers (hardware or software) for air-gapped signing and setting up multisig vaults. Use health check to verify access to keys", "slide02Title": "Security should not\ncome at the cost of", - "slide02Paragraph": "You do not need to register or provide any PII to use Keeper. Run your own node. Connect via Tor.", + "slide02Paragraph": "No account creation needed. Run your own node. Connect via Tor.", "slide03Title": "Secure bitcoin for you and\nyour loved ones, anywhere", "slide03Paragraph": "Use all the Keeper features, including inheritance, no matter which country you are located in as long as the app is available in your country.", "slide04Title": "All that you will need for\nsecuring your bitcoin", - "slide04Paragraph": "BIP 85 hot wallets, auto-transfer to Vault, buy bitcoin directly in to your cold storage, and much more", + "slide04Paragraph": "BIP 85 hot wallets, auto-transfer to vault, buy bitcoin directly in to your cold storage, and much more", "slide05Title": "Works just with the mobile app,\nno computer needed", - "slide05Paragraph": "More security with NFC and QR code compatibility. Connect directly to the hardware signing devices, without any intermediate step", + "slide05Paragraph": "More security with NFC and QR code compatibility. Connect directly to the hardware signers, without any intermediate step", "slide06Title": "Upgrade Keeper, with your bitcoin journey", - "slide06Paragraph": "Pleb - Hodler - Diamond Hands. Three levels for the Vault with increased multi-sig security. The last tier enables inheritance.", + "slide06Paragraph": "Pleb - Hodler - Diamond Hands. Three levels for the vault with increased multi-sig security. The last tier enables inheritance.", "slide07Title": "Ensure forward looking privacy \nwith", "whirlpool": "Whirlpool", - "slide07Paragraph": "You can remix your sats for free for enhanced privacy before storing them in your Vault" + "slide07Paragraph": "You can remix your sats for free for enhanced privacy before storing them in your vault", + "slide08Title": "Upgrade to Diamond Hands for\n generational hodling", + "slide08Paragraph": "Plan your inheritance. Access assisted keys.\nAvail priority support services." }, "choosePlan": { - "choosePlantitle": "Choose your plan", + "choosePlantitle": "Manage Subscription", "choosePlanSubTitle": "You are currently on the basic plan", "noteSubTitle": "Restore Purchases defaults to your previous plan if issues arise", "confirming": "Confirming your subscription", @@ -640,7 +692,7 @@ "viewSubscription": "View Subsciption" }, "BackupWallet": { - "confirmSeedWord": "Confirm Backup phrase", + "confirmSeedWord": "Confirm Backup Phrase", "enterSeedWord": "Enter the second(02) word", "seedWordNote": "If you don’t have the words written down, you may choose to start over.", "startOver": "Start Over", @@ -667,7 +719,8 @@ "SEED_BACKUP_CONFIRMED": "Recovery Phrase backup is confirmed", "SEED_BACKUP_CONFIRMATION_SKIPPED": "Recovery Phrase health confirmation is skipped", "recoveryPhrase": "Recovery Phrase", - "recoveryPhraseSubTitle": "The QR below comprises of your 12 word Recovery Phrase" + "recoveryPhraseSubTitle": "The QR below comprises of your 12 word Recovery Phrase", + "recoveryPhraseNote": "Write down and store securely. Never share, they're your wallet's key. Loss of words = loss of wallet access." }, "importWallet": { "addDetails": "Add details", @@ -676,9 +729,10 @@ "addDescription": "Add Description", "enterAmount": "Enter Amount", "autoTransfer": "Auto transfer initiated at (optional)", - "walletBalance": "When the wallet balance crosses this amount, a transfer to the Vault is initiated for user approval", + "walletBalance": "When the wallet balance crosses this amount, a transfer to the vault is initiated for user approval", "IWNoteDescription": "Make sure that the QR is well aligned, focused and visible as a whole", "scanSeedWord": "Scan your seed words/Backup Phrase", + "usingWalletConfigurationFile": "Using wallet configuration file", "uploadFromGallery": "Upload from gallery", "AddImportModalTitle": "Add or Import Wallet", "AddImportModalSubTitle": "Create purpose specific wallets having dedicated UTXOs. Manage other app wallets by importing them", @@ -690,5 +744,10 @@ "addressForRamp": "Address for ramp transactions", "buyBitcoinRamp": "Buy bitcoin with Ramp", "buyBitcoinRampSubTitle": "Ramp enables BTC purchases using Apple Pay, Debit/Credit card, Bank Transfer and open banking where available payment methods available may vary based on your country" + }, + "signer": { + "addSigner": "Add Signer", + "addSignerSubTitle": "to Valinor", + "addSigners": "Add Signers" } } diff --git a/src/context/Localization/language/es.json b/src/context/Localization/language/es.json index 9ad54fef7..4e2374492 100644 --- a/src/context/Localization/language/es.json +++ b/src/context/Localization/language/es.json @@ -11,7 +11,7 @@ "add": "Add", "sign": "Sign", "knowMore": "Know more", - "confirmProceed": "Confirm & Proceed", + "confirmProceed": "Confirm & Send", "skip": "Skip", "search": "Search", "share": "Share", @@ -59,7 +59,7 @@ "change": "Change", "save": "Save", "contactCreated": "Contact Created", - "FAQs": "FAQ's", + "FAQs": "FAQ’s & Help", "TermsConditions": "Terms and Conditions", "PrivacyPolicy": "Privacy Policy", "ViewSeed": "View Seed", @@ -82,11 +82,11 @@ "transactions": "Transactions", "viewAll": "View All", "addNewWalletOrImport": "Add a new wallet or import one", - "noTransYet": "No transactions yet.", + "noTransYet": "You have no transactions yet", "pullDownRefresh": "Pull down to refresh", "securityTips": "Security Tip", "emptyStateModalSubtitle": "Recreate the multisig on more coordinators. Receive a small amount and send a part of it. Check the balances are appropriately reflected across all the coordinators after each step.", - "addSignDeviceUpgrade": "Add signing device to upgrade", + "addSignDeviceUpgrade": "Add signer to upgrade", "path": "Path", "purpose": "Purpose", "choosePurpose": "Choose Purpose", @@ -95,7 +95,9 @@ "receiveTestSats": "Receiving test sats", "upgradeNow": "Upgrade Now", "somethingWrong": "Something went wrong", - "showAsQR": "Show as QR" + "showAsQR": "Show as QR", + "addNow": "ADD NOW", + "runHealthCheck": "Run Health Check" }, "noInternet": { "no": "No Internet", @@ -105,7 +107,8 @@ "some": "Some of the features will not work as expected in your Hexa app, including:", "fetching": "fetching your balance and transactions", "sending": "sending sats", - "contact": "contact requests" + "contact": "contact requests", + "action": "Action" }, "login": { "login": "Login", @@ -204,7 +207,7 @@ "HexaPaywalletsecurity": "You are about the enhance your Hexa Pay wallet's security", "amountdesc": "The QR code will change once an amount is entered", "ConvertedAmount": "Converted Amount", - "reflectSats": "It would take some time for the sats to reflect in your account based on the network conditions", + "reflectSats": "It would take some time for sats to reflect in your wallet based on fees paid by sender", "the_wallet_backup": "The wallet backup is ", "availableToSpend": "Available to spend", "Transactions": "Transactions", @@ -258,17 +261,17 @@ "Transferbitcoincustody": "Transfer bitcoin custody", "ActivateTransfer": "Activate Transfer", "Thebestinheritance": "The best inheritance in history", - "UpgradetoElitetier": "Upgrade to Elite tier and setup the Vault with 5 signing devices", + "UpgradetoElitetier": "Upgrade to Elite tier and setup the vault with 5 signers", "ActivateInheritance": "Activate Inheritance", "Downloadandsafelykeepinheritencedocuments": "Download and safely keep inheritence documents. Safekeeping best practices. Will template for digital assets. Inheritance recovery instructions", "Setupfalserecoveryalert": "Setup false recovery alert (optional)", "tryingtorecoveryourwallet": "If someone is trying to recover your wallet, make sure you get notified to approve or deny the recovery", "IndependentRecovery": "Independent Recovery", - "UnderstandhowyoucanrecoveryourVault": "Understand how you can recover your Vault even without the Keeper app or any service from the company", + "UnderstandhowyoucanrecoveryourVault": "Understand how you can recover your vault even without the Keeper app or any service from the company", "Practicehealthcheck": "Practice health check", - "Makessureyousignersareaccessible": "Make sure you signing devices are accessible. Change them if that is not the case", + "Makessureyousignersareaccessible": "Make sure you signers are accessible. Change them if that is not the case", "inheritanceSupport": "Inheritance Support", - "inheritanceSupportSubTitle": "Keeper provides you with the tips and tools you need to include the Vault in your estate planning", + "inheritanceSupportSubTitle": "Keeper provides you with the tips and tools you need to include the vault in your estate planning", "consultYourEstate": "Contact your estate planning attorney and advisors to ensure the documents provided here are suitable for your needs and are as per your jurisdiction", "secureBequeathBitcoin": "Plan your bitcoin inheritance" }, @@ -277,6 +280,7 @@ "Startstackingsats": "Start stacking sats", "SinglesigWallet": "Single-sig Wallet", "TransferPolicy": "Transfer Policy", + "TransferPolicyDesc": "Trigger transfer from hot wallet to vault", "Foryourdaytodayspends": "For your day to day spends", "MultisigWallet": "Multi-sig Wallet", "Forlongtermholding": "For long term holding", @@ -289,7 +293,7 @@ "BlueWallet": "Blue Wallet", "MunnWallet": "Munn Wallet", "Blockchain": "Blockchain.com", - "AddVault": "Add a Vault", + "AddVault": "Add a vault", "AddWallet": "Add a Wallet", "Setupawalletforyoubitcoin": "Set up a wallet for you bitcoin", "Creatingyourwallet": "Creating your wallet", @@ -310,14 +314,14 @@ "ImportAWallet": "Import a Wallet", "AddNewWalletDescription": "Name and description for the wallet", "WalletNamePlaceHolder": "Enter wallet name", - "WalletDescriptionPlaceholder": "Enter wallet description", + "WalletDescriptionPlaceholder": "Add a description (Optional)", "AutoTransferInitiated": "Auto transfer initiated at (optional)", - "AutoTransferInitiatedDesc": "When the wallet balance crosses the amount specified here, a transaction for transferring sats from the wallet to the Vault is initiated for user approval", + "AutoTransferInitiatedDesc": "When the wallet balance crosses the amount specified here, a transaction for transferring sats from the wallet to the vault is initiated for user approval", "TransferSuccessful": "Transfer Successful!", "WalletDetails": "Wallet Details", "walletDetailsSubTitle": "Name, details and transfer policy", "EditWalletDeatils": "Edit wallet name and descrition", - "changeWalletDetails": "Change wallet name & description", + "changeWalletDetails": "Wallet Name & Description", "initiatetransfer": "Are you sure you want to initiate transfer to", "XPubTitle": "Wallet xPub", "XPubSubTitle": "Scan or copy paste the xPub in another app for generating new addresses and fetching balances", @@ -327,11 +331,11 @@ "confirmPassSubTitle": "To show wallet seed words", "SendSuccess": "Send Successful", "ViewDetails": "View Details", - "approveTransVault": "Approve Transfer to Vault", + "approveTransVault": "Approve Transfer to vault", "approveTransVaultSubtitle": "Your auto-transfer policy has triggered a transaction attransaction, at 100,000 sats that needs your approval", "AddWalletCosigner": "Add Wallet as co-signer", - "AddWalletCosignerSubTitle": "You are about to add this wallet as a co-signer for a Keeper app Vault.", - "AddWalletCosignerParagraph": "Please back up this wallet. If you want to delete the app from this phone, please change your signing device on the other Keeper app before deleting this one.", + "AddWalletCosignerSubTitle": "You are about to add this wallet as a co-signer for a Keeper app vault.", + "AddWalletCosignerParagraph": "Please back up this wallet. If you want to delete the app from this phone, please change your signer on the other Keeper app before deleting this one.", "whirlpoolUtxoTitle": "Whirlpool & UTXOs", "whirlpoolUtxoSubTitle": "Manage wallet UTXOs and use Whirlpool", "addWallet": "Add Wallet", @@ -343,7 +347,7 @@ "addCollabWalletParagraph": "Please ensure that Keeper is properly backed up to ensure your bitcoin's security", "selectForMix": "Select for Mix", "selectForRemix": "Select for Remix", - "remixVault": "Remix to Vault", + "remixVault": "Remix to vault", "selectToSend": "Select to Send", "noUTXOYet": "No UTXOs yet", "noUTXOYetSubTitle": "UTXOs from all your Tx0s land here.", @@ -366,9 +370,9 @@ "TransPolicyChange": "Transfer Policy Changed", "walletSeedWord": "Wallet Seed Words", "walletSeedWordSubTitle": "Use to link external wallets to Keeper", - "showCoSignerDetails": "Show co-signer Details", - "showCoSignerDetailsSubTitle": "Use this wallet as a co-signer with other vaults", - "actCoSigner": "Act as co-signer", + "showCoSignerDetails": "Show Co-Signer Details", + "showCoSignerDetailsSubTitle": "Use this wallet as a co-signer", + "actCoSigner": "Act as Co-Signer", "recieveTestSats": "Receive Test Sats", "recieveTestSatSubTitle": "Receive Test Sats to this address", "walletSettingNote": "These settings are for your selected wallet only and does not affect other wallets", @@ -376,8 +380,9 @@ "receiveSubTitle": "Native segwit address", "receiveAddress": "Receive Address", "addressCopied": "Address Copied Successfully", + "walletIdCopied": "Fingerprint Copied Successfully", "addSpecificInvoiceAmt": "Add a specific invoice amount", - "addressReceiveDirectly": "You can get a receive address directly from a signing device and do not have to trust the Keeper app", + "addressReceiveDirectly": "You can get a receive address directly from a signer and do not have to trust the Keeper app", "editTransPolicyInfo": " This will trigger a transfer request which you need to approve", "transPolicyCantZero": "Transfer Policy cannot be zero", "updateWalletPath": "Update Wallet Path", @@ -390,46 +395,68 @@ "failToUpdate": "Failed to update", "walletBalanceMsg": "Wallet has balance: changes not allowed", "transactionBroadcasted": "The transaction has been successfully broadcasted", - "sendTransSuccessMsg": " You can view the confirmation status of the transaction on any block explorer or when the Vault transaction list is refreshed" + "sendTransSuccessMsg": " You can view the confirmation status of the transaction on any block explorer or when the vault transaction list is refreshed", + "PRIORITY": "PRIORITY", + "ARRIVALTIME": "ARRIVAL TIME", + "FEE": "FEE", + "transactionPriority": "Transaction Priority", + "addCustomPriority": "Add Custom Priority", + "totalAmount": "Total Amount", + "totalFees": "Total Fees", + "total": "Total", + "highFeeAlert": "High Fee Alert", + "highFeeAlertSubTitle": "Network fee is larger than the amount you are sending", + "networkFee": "Network Fee", + "amtBeingSent": "Amount being sent", + "estimateArrvlTime": "Estimated arrival time", + "sendingFrom": "Sending From", + "sendingFromUtxo": "Sending from selected UTXOs of", + "AvailableToSpend": "Available to spend", + "advanced": "Advanced", + "chooseFromTemplate": "Choose from a template" }, "vault": { - "SetupyourVault": "Setup your Vault", - "VaultDesc": "A signing device is an an air gapped device which helps you keep your Vault safe", + "SetupyourVault": "Setup a vault", + "AddCustomMultiSig": "Add Custom MultiSig", + "configureScheme": "Create an m-of-n vault", + "VaultDesc": "A signer is an an air gapped device which helps you keep your vault safe", "SetupNow": "Setup Now", - "Addsigner": "Add a signing device", + "Addsigner": "Add a signer", "Vault": "Vault", "Yoursupersecurebitcoin": "Your super secure bitcoin", - "MySigners": "My signing devices", + "MySigners": "My signers", "Usedforsecuringfunds": "Used for securing funds", "Inheritance": "Inheritance", "Setupinheritancetoyoursats": "Set up inheritance to your sats", "Setup": "Setup", "VaultCreated": "Vault created", - "VaultCreationDesc": "Your Basic Vault has been successfully setup You can start receiving bitcoin in it", - "ViewVault": "View Vault", + "VaultCreationDesc": "Your Basic vault has been successfully setup You can start receiving bitcoin in it", + "ViewVault": "View vault", "AddNow": "Add Now", - "SelectSigner": "Select a Signing Devices", - "ForVault": "For your Vault", - "VaultInfo": "A signing device can be a hardware wallet or a signing device or an app. Most popular ones are listed above. Want support for more?", + "SelectSigner": "Select a Signer", + "SelectSignerSubtitle": "To store one private key", + "VaultInfo": "A signer can be a hardware wallet or a signer or an app. Most popular ones are listed above. Want support for more?", "CustomPriority": "Custom Priority", "EditDescription": "Edit Description", "Description": "", - "yourVault": "Your Vault", - "toActiveVault": "Add Signing Device to activate your Vault", + "yourVault": "Your vault", + "toActiveVault": "Add signer to activate your vault", "inheritanceTools": "Inheritance Tools", "manageInheritance": "Manage Inheritance key or view documents", - "additionalOptionForSignDevice": "There are also some additional options if you do not have hardware signing devices", - "sendVaultSignDevices": "For sending out of the Vault you will need the signing devices. This means no one can steal your bitcoin in the Vault unless they also have the signing devices", + "additionalOptionForSignDevice": "There are also some additional options if you do not have hardware signers", + "sendVaultSignDevices": "For sending out of the vault you will need the signers. This means no one can steal your bitcoin in the vault unless they also have the signers", "addEmail": "Add Email", + "addEmailPhone": "Add Phone/Email", "addEmailDetails": "Additionally you can provide an email which will be used to notify you when someone tries to access the Inheritance Key", + "addEmailVaultDetail": "Get notified of Key activation and have option to decline use", "walletSetupDetails": "This kind of wallet setup can be used for business partnerships, family funds, or any scenario where joint control of funds is necessary.", - "keeperSupportSigningDevice": "Keeper supports all the popular bitcoin signing devices (Hardware Wallets) that a user can select", - "newVaultCreated": "New Vault Created", + "keeperSupportSigningDevice": "Keeper supports all the popular bitcoin signers (Hardware Wallets) that a user can select", + "newVaultCreated": "New vault Created", "collaborativeWallet": "Collaborative Wallet", - "keeperVault": "Keeper Vault", + "keeperVault": "Keeper vault", "collaborativeWalletMultipleUsers": "Collaborative wallet is designed to enable multiple users to have control over a single wallet, adding a layer of security and efficiency in fund management.", - "signingOldVault": "Old Vaults with the previous signing device configuration will be in the archived list of Vaults", - "cvvSigningServerInfo": "If you lose your authenticator app, use the other Signing Devices to reset the Signing Server" + "signingOldVault": "Old Vaults with the previous signer configuration will be in the archived list of Vaults", + "cvvSigningServerInfo": "If you lose your authenticator app, use the other signers to reset the signer" }, "seed": { "EnterSeed": "Enter Seeds", @@ -443,14 +470,17 @@ "walletRecoverySuccessful": "Wallet Recovery Successful", "desc": "Use these to create any other wallet and that wallet will be linked to Keeper (will show along with other wallets)", "recoveryPhrase": "Backup Phrase", - "enterRecoveryPhrase": "Enter the Wallet Recovery Phrase", + "backupPhrase": "Recovery Key", + "enterRecoveryPhrase": "Recover an existing Keeper App", + "enterRecoveryPhraseSubTitle": "Enter existing 12-word recovery phrase below", + "enterRecoveryPhraseNote": "Make sure to use the 12-word recovery phrase in private", "walletRecoveryPhrase": "Wallet Recovery Phrase", "showXPubNoteSubText": "Losing your Recovery Phrase may result in permanent loss of funds. Store them carefully.", "mobileKeyVerified": "Mobile Key verified successfully", "seedWordVerified": "Seed Words verified successfully" }, "healthcheck": { - "ChangeSigningDevice": "Change signing device", + "ChangeSigningDevice": "Change signer", "HealthCheck": "Health Check", "SkippingHealthCheck": "Skipping Health Check", "EnterCVV": "Enter the CVV", @@ -467,7 +497,7 @@ "Setupandassociate": "Setup and associate", "Associate": "Associate", "CardStatus": "Card Status", - "SetupTitle": "Keep your Coldcard ready", + "SetupTitle": "Setting up Coldcard", "SetupDescription": "Keep your Coldcard ready before proceeding" }, "ledger": { @@ -489,11 +519,11 @@ }, "signingServer": { "choosePolicy": "Choose Policy", - "choosePolicySubTitle": "For the signing server", + "choosePolicySubTitle": "For the signer", "maxNoCheckAmt": "Max no-check amount", - "maxNoCheckAmtSubTitle": "The Signing Server will sign a transaction of this amount or lower, even w/o a 2FA verification code", + "maxNoCheckAmtSubTitle": "The signer will sign a transaction of this amount or lower, even w/o a 2FA verification code", "maxAllowedAmt": "Max allowed amount", - "maxAllowedAmtSubTitle": "If the transaction amount is more than this amount, the Signing Server will not sign it. You will have to use other devices for it" + "maxAllowedAmtSubTitle": "If the transaction amount is more than this amount, the signer will not sign it. You will have to use other devices for it" }, "settings": { "selectCurrency": "Select currency", @@ -505,10 +535,13 @@ "VersionHistorySubTitle": "View the app's version history", "LanguageCountry": "Language & Currency", "LanguageCountrySubTitle": "Select language and currency", + "CurrencyDefaults": "Currency Defaults", + "CurrencyDefaultsSubtitle": "Set preferred defaults", "KeeperCommunityTelegramGroup": "Keeper Community Telegram Group", "Questionsfeedbackandmore": "Questions, feedback and more", "biometricsDesc": "Select app settings", "SatsMode": "Sats Mode", + "SatsModeSubTitle": "Use Sats mode to see balance in sats", "Viewbalancessats": "View your balances in sats", "CountrySettings": "Country Settings", "ChooseKeeperaccesslocation": "Choose Keeper access location", @@ -526,6 +559,8 @@ "walletSettings": "Wallet Settings", "walletSettingsSub": "Your wallet settings & preferences", "walletSettingSubTitle": "Setting for the wallet only", + "BackupSettings": "Backup Settings", + "BackupSettingSubTitle": "Setting your backup", "AppInfo": "App Info", "AppInfoSub": "Hexa app version number and details", "Disconnectyour": "Disconnect your own node and connect to Hexa node", @@ -544,6 +579,8 @@ "ChangeCurrency": "Currency & Language", "Chooseyourcurrency": "Choose your currency & language", "AlternateCurrency": "Alternate Currency", + "FiatCurrency": "Fiat Currency", + "Seebalance": "See balance in your local fiat currency", "LanguageSettings": "Language Settings", "Chooseyourlanguage": "Choose your language preference", "HelpUstranslate": "Help us translate better", @@ -566,6 +603,9 @@ "desc": "It would take some time for the sats to reflect in your account based on the network condition", "nodeSettings": "Node Settings", "nodeSettingsSubtitle": "Configure your node settings", + "SecurityAndLogin": "Security & Login", + "SecurityAndLoginSubtitle": "App improvement and ease of access", + "AppLevelSettings": "App-level settings", "nodeSettingUsedSoFar": "Node settings used so far", "connectToMyNode": "Connect to my node", "connectToMyNodeSubtitle": "Disable to use Keeper's node", @@ -585,8 +625,8 @@ "nodeConnectionFailure": "Unable to connect to Electrum Server. Invalid node details or, not known", "ManageWallets": "Manage Wallets", "ManageWalletsSub": "Hide wallets and unhide them.", - "settingsSubTitle": "Configure your app here", - "appBackup": "App Backup", + "settingsSubTitle": "Customize your app", + "appBackup": "Recovery Key", "torSettingTitle": "Tor Settings", "torSettingSubTitle": "Configure in-app Tor and Orbot", "keeperTelegram": "Keeper Telegram", @@ -601,30 +641,42 @@ "checkStatus": "Check Status", "torSettingsNoteSubTitle": "Some WiFi networks use settings that do not let your device connect to Tor. If you get constant errors, try changing to mobile network or check your network settings", "orbotConnection": "Orbot Connection", - "orbotConnectionSubTitle": "To connect to Tor via Orbot, you need to have the Orbot app installed on your device." + "orbotConnectionSubTitle": "To connect to Tor via Orbot, you need to have the Orbot app installed on your device.", + "PrivacyDisplay": "Privacy & Display", + "PrivacyDisplaySubTitle": "Dark mode and biometrics", + "networkSettings": "Network Settings", + "networkSettingSubTitle": "Node and Tor in Network Setting", + "networkSettingConfigSubTitle": "Configure your node and tor settings", + "satsModeSubTitle": "Enable to see balance in sats", + "shareAnalytics": "Share Analytics & App Data", + "analyticsDescription": "No personal or bitcoin data is shared", + "changePasscode": "Change Passcode", + "changePasscodeDescription": "Easy to remember; hard to guess" }, "onboarding": { "Comprehensive": "Comprehensive", "security": "security", "privacy": "privacy", "slide01Title": "for\n your bitcoin keys", - "slide01Paragraph": "Keeper works with most of the trusted hardware signing devices which hold your keys in an air-gapped and/ or multisig manner", + "slide01Paragraph": "Use trusted signers for air-gapped signing and setting up vaults. Use health check to verify access to keys", "slide02Title": "Security should not\ncome at the cost of", "slide02Paragraph": "Use all the Keeper features, including inheritance, no matter which country you are located in as long as the app is available in your country.", "slide03Title": "Secure bitcoin for you and\nyour loved ones, anywhere", "slide03Paragraph": "Use all the Keeper features, including inheritance, no matter which country you are located in as long as the app is available in your country.", "slide04Title": "All that you will need for\nsecuring your bitcoin", - "slide04Paragraph": "BIP 85 hot wallets, auto-transfer to Vault, buy bitcoin directly in to your cold storage, and much more", + "slide04Paragraph": "BIP 85 hot wallets, auto-transfer to vault, buy bitcoin directly in to your cold storage, and much more", "slide05Title": "Works just with the mobile app,\nno computer needed", - "slide05Paragraph": "More security with NFC and QR code compatibility. Connect directly to the hardware signing devices, without any intermediate step", + "slide05Paragraph": "More security with NFC and QR code compatibility. Connect directly to the hardware signers, without any intermediate step", "slide06Title": "Affordable plans\nand a great offer", - "slide06Paragraph": "And now for a limited time, all tiers for the Keeper are free for the first 6 months. Download Keeper and setup your Vault now.", + "slide06Paragraph": "And now for a limited time, all tiers for the Keeper are free for the first 6 months. Download Keeper and setup your vault now.", "slide07Title": "Ensure forward looking privacy \nwith", "whirlpool": "Whirlpool", - "slide07Paragraph": "You can remix your sats for free for enhanced privacy before storing them in your Vault" + "slide07Paragraph": "You can remix your sats for free for enhanced privacy before storing them in your vault", + "slide08Title": "Upgrade to Diamond Hands for\n generational hodling", + "slide08Paragraph": "Plan your inheritance. Access assisted keys.\nAvail priority support services." }, "choosePlan": { - "choosePlantitle": "Choose your plan", + "choosePlantitle": "Manage Subscription", "choosePlanSubTitle": "You are currently on the basic plan", "noteSubTitle": "Restore Purchases defaults to your previous plan if issues arise", "confirming": "Confirming your subscription", @@ -636,7 +688,7 @@ "viewSubscription": "View Subsciption" }, "BackupWallet": { - "confirmSeedWord": "Confirm Backup phrase", + "confirmSeedWord": "Confirm Backup Phrase", "enterSeedWord": "Enter the second(02) word", "seedWordNote": "If you don’t have the words written down, you may choose to start over.", "startOver": "Start Over", @@ -663,7 +715,8 @@ "SEED_BACKUP_CONFIRMED": "Recovery Phrase backup is confirmed", "SEED_BACKUP_CONFIRMATION_SKIPPED": "Recovery Phrase health confirmation is skipped", "recoveryPhrase": "Recovery Phrase", - "recoveryPhraseSubTitle": "The QR below comprises of your 12 word Recovery Phrase" + "recoveryPhraseSubTitle": "The QR below comprises of your 12 word Recovery Phrase", + "recoveryPhraseNote": "Write down and store securely. Never share, they're your wallet's key. Loss of words = loss of wallet access." }, "importWallet": { "addDetails": "Add details", @@ -672,9 +725,10 @@ "addDescription": "Add Description", "enterAmount": "Enter Amount", "autoTransfer": "Auto transfer initiated at (optional)", - "walletBalance": "When the wallet balance crosses this amount, a transfer to the Vault is initiated for user approval", + "walletBalance": "When the wallet balance crosses this amount, a transfer to the vault is initiated for user approval", "IWNoteDescription": "Make sure that the QR is well aligned, focused and visible as a whole", "scanSeedWord": "Scan your seed words/Backup Phrase", + "usingWalletConfigurationFile": "Using wallet configuration file", "uploadFromGallery": "Upload from gallery", "AddImportModalTitle": "Add or Import Wallet", "AddImportModalSubTitle": "Create purpose specific wallets having dedicated UTXOs. Manage other app wallets by importing them", @@ -686,5 +740,10 @@ "addressForRamp": "Address for ramp transactions", "buyBitcoinRamp": "Buy bitcoin with Ramp", "buyBitcoinRampSubTitle": "Ramp enables BTC purchases using Apple Pay, Debit/Credit card, Bank Transfer and open banking where available payment methods available may vary based on your country" + }, + "signer": { + "addSigner": "Add Signer", + "addSignerSubTitle": "to Valinor", + "addSigners": "Add Signers" } } diff --git a/src/core/config.ts b/src/core/config.ts index 0a4472006..b2717ace5 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -33,8 +33,10 @@ const DEFAULT_CONFIG = { ENVIRONMENT: APP_STAGE.DEVELOPMENT, CHANNEL_URL: 'https://keeper-channel.herokuapp.com/', KEEPER_HWI: 'https://connect.bitcoinkeeper.app/', - RAMP_BASE_URL: 'https://buy.ramp.network/', + RAMP_BASE_URL: 'https://app.ramp.network/', RAMP_REFERRAL_CODE: 'ku67r7oh5juc27bmb3h5pek8y5heyb5bdtfa66pr', + SIGNING_SERVER_RSA_PUBKEY: + '-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/gXW+ITXpSfp8tu9Ujw gDfcPSfFLuIDovPFRBdMP9uJ7baHqhYO1WqvrafLHZ/akhdk9XSR18Oqb7pvUfvQ Y/40QO6Qlx6zxyGoM1FOTaXY2NxCv5U+kTi2NJpMi2C7/h63ykiLD9dkO0qBCBjd /tFsv8e5GTOQXZQvIEsAyeBNsNQxNX5AY7HQI0nMjGrxKYGMaBQKFqtaJIQwESlo DSkrd5yQJQR50KwL0+/e5znemVhxS08NgjxGTVKTuiJhsJa+PWMZhlmHjcLaFrZz QjDuhqycRCwXk7tuZHOVRSI9LC+L5LfayL6Mj7N1NdmkRWRY/feXU9GlaFX8KQqq fwIDAQAB -----END PUBLIC KEY-----', }; class Configuration { @@ -94,6 +96,9 @@ class Configuration { baseURL: this.SIGNING_SERVER, timeout: this.REQUEST_TIMEOUT, }); + public SIGNING_SERVER_RSA_PUBKEY: string = config.SIGNING_SERVER_RSA_PUBKEY + ? config.SIGNING_SERVER_RSA_PUBKEY.trim() + : DEFAULT_CONFIG.SIGNING_SERVER_RSA_PUBKEY; public NETWORK: bitcoinJS.Network; diff --git a/src/core/utils.ts b/src/core/utils.ts index b4546e1d2..eed69a1e3 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -2,9 +2,7 @@ import { EntityKind } from './wallets/enums'; import { Vault, VaultScheme, VaultSigner } from './wallets/interfaces/vault'; import { Wallet } from './wallets/interfaces/wallet'; import WalletOperations from './wallets/operations'; -const cryptoJS = require('crypto'); - -// GENRATOR +const crypto = require('crypto'); export const getDerivationPath = (derivationPath: string) => derivationPath.substring(2).split("'").join('h'); @@ -16,8 +14,15 @@ export const getMultiKeyExpressions = (signers: VaultSigner[]) => { return keyExpressions.join(); }; -export const getKeyExpression = (masterFingerprint: string, derivationPath: string, xpub: string) => - `[${masterFingerprint}/${getDerivationPath(derivationPath)}]${xpub}/**`; +export const getKeyExpression = ( + masterFingerprint: string, + derivationPath: string, + xpub: string, + withPathRestrictions: boolean = true +) => + `[${masterFingerprint}/${getDerivationPath(derivationPath)}]${xpub}${ + withPathRestrictions ? '/**' : '' + }`; export const genrateOutputDescriptors = ( wallet: Vault | Wallet, @@ -178,7 +183,7 @@ export const parseTextforVaultConfig = (secret: string) => { throw Error('Unsupported format!'); }; -export const urlParamsToObj = (url: string): object => { +export const urlParamsToObj = (url: string): any => { try { const regex = /[?&]([^=#]+)=([^&#]*)/g; const params = {}; @@ -195,17 +200,11 @@ export const urlParamsToObj = (url: string): object => { export const createCipheriv = (data: string, password: string) => { const algorithm = 'aes-256-cbc'; - const iv = cryptoJS.randomBytes(16); - // Creating Cipheriv with its parameter - const cipher = cryptoJS.createCipheriv(algorithm, Buffer.from(password, 'hex'), iv); - - // Updating text + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(algorithm, Buffer.from(password, 'hex'), iv); let encrypted = cipher.update(data); - - // Using concatenation encrypted = Buffer.concat([encrypted, cipher.final()]); - // Returning iv and encrypted data return { iv: iv.toString('hex'), encryptedData: encrypted.toString('hex') }; }; @@ -213,7 +212,7 @@ export const createDecipheriv = (data: { iv: string; encryptedData: string }, pa const algorithm = 'aes-256-cbc'; const encryptedText = Buffer.from(data.encryptedData, 'hex'); // Creating Decipher - const decipher = cryptoJS.createDecipheriv( + const decipher = crypto.createDecipheriv( algorithm, Buffer.from(password, 'hex'), Buffer.from(data.iv, 'hex') diff --git a/src/core/wallets/enums/index.ts b/src/core/wallets/enums/index.ts index 9b2f573c3..b012d486a 100644 --- a/src/core/wallets/enums/index.ts +++ b/src/core/wallets/enums/index.ts @@ -104,6 +104,8 @@ export enum SignerType { BITBOX02 = 'BITBOX02', OTHER_SD = 'OTHER_SD', INHERITANCEKEY = 'INHERITANCEKEY', + UNKOWN_SIGNER = 'UNKNOWN_SIGNER', + SPECTER = 'SPECTER', } export enum PaymentInfoKind { @@ -149,3 +151,27 @@ export enum LabelRefType { PUBKEY = 'PUBKEY', XPUB = 'XPUB', } + +export enum ImportedKeyType { + MNEMONIC = 'mnemonic', + + // Extended Public Keys - MAINNET + XPUB = 'xpub', + YPUB = 'ypub', + ZPUB = 'zpub', + + // Extended Private Keys - MAINNET + XPRV = 'xprv', + YPRV = 'yprv', + ZPRV = 'zprv', + + // Extended Public Keys - TESTNET + TPUB = 'tpub', + UPUB = 'upub', + VPUB = 'vpub', + + // Extended Private Keys - TESTNET + TPRV = 'tprv', + UPRV = 'uprv', + VPRV = 'vprv', +} diff --git a/src/core/wallets/factories/VaultFactory.ts b/src/core/wallets/factories/VaultFactory.ts index b54c52c4b..483e41aee 100644 --- a/src/core/wallets/factories/VaultFactory.ts +++ b/src/core/wallets/factories/VaultFactory.ts @@ -11,6 +11,7 @@ import { VisibilityType, } from '../enums'; import { + Signer, Vault, VaultPresentationData, VaultScheme, @@ -38,13 +39,16 @@ const STANDARD_VAULT_SCHEME = [ { m: 3, n: 5 }, ]; -export const generateVaultId = (signers, networkType, scheme) => { - const network = WalletUtilities.getNetworkByType(networkType); +export const generateVaultId = (signers: VaultSigner[], scheme) => { const xpubs = signers.map((signer) => signer.xpub).sort(); - const fingerprints = []; - xpubs.forEach((xpub) => - fingerprints.push(WalletUtilities.getFingerprintFromExtendedKey(xpub, network)) - ); + const xpubMap = {}; + signers.forEach((signer) => { + xpubMap[signer.xpub] = signer; + }); + const fingerprints = xpubs.map((xpub) => { + const signer = xpubMap[xpub]; + return signer.xfp; + }); STANDARD_VAULT_SCHEME.forEach((s) => { if (s.m !== scheme.m || s.n !== scheme.n) { fingerprints.push(JSON.stringify(scheme)); @@ -64,6 +68,7 @@ export const generateVault = async ({ networkType, vaultShellId, collaborativeWalletId, + signerMap, }: { type: VaultType; vaultName: string; @@ -73,8 +78,9 @@ export const generateVault = async ({ networkType: NetworkType; vaultShellId?: string; collaborativeWalletId?: string; + signerMap: { [key: string]: Signer }; }): Promise => { - const id = generateVaultId(signers, networkType, scheme); + const id = generateVaultId(signers, scheme); const xpubs = signers.map((signer) => signer.xpub); const shellId = vaultShellId || generateKey(12); const defaultShell = 1; @@ -125,7 +131,7 @@ export const generateVault = async ({ vault.specs.receivingAddress = WalletOperations.getNextFreeAddress(vault); // update cosigners map(if one of the signers is an assisted key) - await updateCosignersMapForAssistedKeys(signers); + await updateCosignersMapForAssistedKeys(signers, signerMap); return vault; }; @@ -227,10 +233,14 @@ export const generateMockExtendedKey = ( return { ...extendedKeys, derivationPath: xDerivationPath, masterFingerprint }; }; -export const generateCosignerMapIds = (signers: VaultSigner[], except: SignerType) => { +export const generateCosignerMapIds = ( + signerMap: { [key: string]: Signer }, + keys: VaultSigner[], + except: SignerType +) => { const cosignerIds = []; - signers.forEach((signer) => { - if (signer.type !== except) cosignerIds.push(signer.signerId); + keys.forEach((signer) => { + if (signerMap[signer.masterFingerprint].type !== except) cosignerIds.push(signer.xfp); }); cosignerIds.sort(); @@ -245,28 +255,30 @@ export const generateCosignerMapIds = (signers: VaultSigner[], except: SignerTyp }; export const generateCosignerMapUpdates = ( - signers: VaultSigner[], + signerMap: { [key: string]: Signer }, + keys: VaultSigner[], assistedKey: VaultSigner ): IKSCosignersMapUpdate[] | CosignersMapUpdate[] => { - const cosignersMapIds = generateCosignerMapIds(signers, assistedKey.type); + const assistedKeyType = signerMap[assistedKey.masterFingerprint].type; + const cosignersMapIds = generateCosignerMapIds(signerMap, keys, assistedKeyType); - if (assistedKey.type === SignerType.POLICY_SERVER) { + if (assistedKeyType === SignerType.POLICY_SERVER) { const cosignersMapUpdates: CosignersMapUpdate[] = []; for (let id of cosignersMapIds) { cosignersMapUpdates.push({ cosignersId: id, - signerId: assistedKey.signerId, + signerId: assistedKey.xfp, action: CosignersMapUpdateAction.ADD, }); } return cosignersMapUpdates; - } else if (assistedKey.type === SignerType.INHERITANCEKEY) { + } else if (assistedKeyType === SignerType.INHERITANCEKEY) { const cosignersMapUpdates: IKSCosignersMapUpdate[] = []; for (let id of cosignersMapIds) { cosignersMapUpdates.push({ cosignersId: id, - inheritanceKeyId: assistedKey.signerId, + inheritanceKeyId: assistedKey.xfp, action: IKSCosignersMapUpdateAction.ADD, }); } @@ -275,20 +287,26 @@ export const generateCosignerMapUpdates = ( } else throw new Error('Non-supported signer type'); }; -const updateCosignersMapForAssistedKeys = async (signers) => { - for (let signer of signers) { - if (signer.type === SignerType.POLICY_SERVER || signer.type === SignerType.INHERITANCEKEY) { - const cosignersMapUpdates = generateCosignerMapUpdates(signers, signer); - - if (signer.type === SignerType.POLICY_SERVER) { +const updateCosignersMapForAssistedKeys = async (keys: VaultSigner[], signerMap) => { + for (let key of keys) { + const assistedKeyType = signerMap[key.masterFingerprint]?.type; + if ( + assistedKeyType === SignerType.POLICY_SERVER || + assistedKeyType === SignerType.INHERITANCEKEY + ) { + // creates maps per signer type + const cosignersMapUpdates = generateCosignerMapUpdates(signerMap, keys, key); + + // updates our backend with the cosigners map + if (assistedKeyType === SignerType.POLICY_SERVER) { const { updated } = await SigningServer.updateCosignersToSignerMap( - signer.signerId, + key.xfp, cosignersMapUpdates as CosignersMapUpdate[] ); if (!updated) throw new Error('Failed to update cosigners-map for SS Assisted Keys'); - } else if (signer.type === SignerType.INHERITANCEKEY) { + } else if (assistedKeyType === SignerType.INHERITANCEKEY) { const { updated } = await InheritanceKeyServer.updateCosignersToSignerMapIKS( - signer.signerId, + key.xfp, cosignersMapUpdates as IKSCosignersMapUpdate[] ); if (!updated) throw new Error('Failed to update cosigners-map for IKS Assisted Keys'); @@ -316,6 +334,8 @@ export const MOCK_SD_MNEMONIC_MAP = { 'equal gospel mirror humor early liberty finger breeze super celery invite proof', [SignerType.BITBOX02]: 'journey gospel position invite winter pattern inquiry scrub sorry early enable badge', + [SignerType.SPECTER]: + 'journey invite inquiry day among poverty inquiry affair keen pave nasty position', }; export const generateMockExtendedKeyForSigner = ( @@ -324,6 +344,9 @@ export const generateMockExtendedKeyForSigner = ( networkType = NetworkType.TESTNET ) => { const mockMnemonic = MOCK_SD_MNEMONIC_MAP[signer]; + if (!mockMnemonic) { + throw new Error(`We don't support mock flow for soft keys`); + } const seed = bip39.mnemonicToSeedSync(mockMnemonic); const masterFingerprint = WalletUtilities.getFingerprintFromSeed(seed); const xDerivationPath = WalletUtilities.getDerivationPath(entity, networkType, 123); diff --git a/src/core/wallets/factories/WalletFactory.ts b/src/core/wallets/factories/WalletFactory.ts index b6cc1ff31..a4e07468a 100644 --- a/src/core/wallets/factories/WalletFactory.ts +++ b/src/core/wallets/factories/WalletFactory.ts @@ -5,6 +5,7 @@ import { hash256 } from 'src/services/operations/encryption'; import config from 'src/core/config'; import { EntityKind, + ImportedKeyType, NetworkType, ScriptTypes, VisibilityType, @@ -14,6 +15,7 @@ import { import { TransferPolicy, Wallet, + WalletDerivationDetails, WalletImportDetails, WalletPresentationData, WalletSpecs, @@ -27,7 +29,7 @@ import { XpubDetailsType } from '../interfaces/vault'; export const whirlPoolWalletTypes = [WalletType.PRE_MIX, WalletType.POST_MIX, WalletType.BAD_BANK]; -export const generateWalletSpecs = ( +export const generateWalletSpecsFromMnemonic = ( mnemonic: string, network: bitcoinJS.Network, xDerivationPath: string @@ -62,6 +64,42 @@ export const generateWalletSpecs = ( return specs; }; +export const generateWalletSpecsFromExtendedKeys = ( + extendedKey: string, + extendedKeyType: ImportedKeyType +) => { + let xpriv: string; + let xpub: string; + + if (WalletUtilities.isExtendedPrvKey(extendedKeyType)) { + xpriv = WalletUtilities.getXprivFromExtendedKey(extendedKey, config.NETWORK); + xpub = WalletUtilities.getPublicExtendedKeyFromPriv(xpriv); + } else if (WalletUtilities.isExtendedPubKey(extendedKeyType)) { + xpub = WalletUtilities.getXpubFromExtendedKey(extendedKey, config.NETWORK); + } else { + throw new Error('Invalid key'); + } + + const specs: WalletSpecs = { + xpub, + xpriv, + nextFreeAddressIndex: 0, + nextFreeChangeAddressIndex: 0, + confirmedUTXOs: [], + unconfirmedUTXOs: [], + balances: { + confirmed: 0, + unconfirmed: 0, + }, + transactions: [], + txNote: {}, + hasNewUpdates: false, + lastSynched: 0, + receivingAddress: '', + }; + return specs; +}; + export const generateWallet = async ({ type, instanceNum, @@ -87,43 +125,80 @@ export const generateWallet = async ({ }): Promise => { const network = WalletUtilities.getNetworkByType(networkType); - let mnemonic: string; - let xDerivationPath: string; let bip85Config: BIP85Config; let depositWalletId: string; + let id: string; + let derivationDetails: WalletDerivationDetails; + let specs: WalletSpecs; if (type === WalletType.IMPORTED) { + // case: adding imported wallet if (!importDetails) throw new Error('Import details are missing'); - mnemonic = importDetails.mnemonic; + const { importedKey, importedKeyDetails, derivationConfig } = importDetails; + + let mnemonic; + if (importedKeyDetails.importedKeyType === ImportedKeyType.MNEMONIC) { + // case: import wallet via mnemonic + mnemonic = importedKey; + id = WalletUtilities.getMasterFingerprintFromMnemonic(mnemonic); // case: wallets(non-whirlpool) have master-fingerprints as their id + derivationDetails = { + instanceNum, + mnemonic, + bip85Config, + xDerivationPath: derivationConfig.path, + }; + + specs = generateWalletSpecsFromMnemonic(mnemonic, network, derivationDetails.xDerivationPath); + } else { + // case: import wallet via extended keys + + derivationDetails = { + instanceNum, // null + mnemonic, // null + bip85Config, // null + xDerivationPath: derivationConfig.path, + }; + + specs = generateWalletSpecsFromExtendedKeys(importedKey, importedKeyDetails.importedKeyType); + + id = WalletUtilities.getFingerprintFromExtendedKey(specs.xpriv || specs.xpub, config.NETWORK); // case: extended key imported wallets have xfp as their id + } } else if (whirlPoolWalletTypes.includes(type)) { - mnemonic = parentMnemonic; + // case: adding whirlpool wallet + const mnemonic = parentMnemonic; + depositWalletId = WalletUtilities.getMasterFingerprintFromMnemonic(mnemonic); // case: whirlpool wallets have master-fingerprints as their deposit id + id = hash256(`${id}${type}`); + + derivationDetails = { + instanceNum, + mnemonic, + bip85Config, + xDerivationPath: derivationConfig + ? derivationConfig.path + : WalletUtilities.getDerivationPath(EntityKind.WALLET, networkType), + }; + specs = generateWalletSpecsFromMnemonic(mnemonic, network, derivationDetails.xDerivationPath); } else { + // case: adding new wallet if (!primaryMnemonic) throw new Error('Primary mnemonic missing'); // BIP85 derivation: primary mnemonic to bip85-child mnemonic bip85Config = BIP85.generateBIP85Configuration(type, instanceNum); const entropy = await BIP85.bip39MnemonicToEntropy(bip85Config.derivationPath, primaryMnemonic); - mnemonic = BIP85.entropyToBIP39(entropy, bip85Config.words); - } - if (derivationConfig) xDerivationPath = derivationConfig.path; - else if (importDetails && importDetails.derivationConfig) - xDerivationPath = importDetails.derivationConfig.path; - else xDerivationPath = WalletUtilities.getDerivationPath(EntityKind.WALLET, networkType); + const mnemonic = BIP85.entropyToBIP39(entropy, bip85Config.words); + id = WalletUtilities.getMasterFingerprintFromMnemonic(mnemonic); // case: wallets(non-whirlpool) have master-fingerprints as their id - let id = WalletUtilities.getMasterFingerprintFromMnemonic(mnemonic); // case: wallets(non-whirlpool) have master-fingerprints as their id - - if (whirlPoolWalletTypes.includes(type)) { - depositWalletId = id; // case: whirlpool wallets have master-fingerprints as their deposit id - id = hash256(`${id}${type}`); + derivationDetails = { + instanceNum, + mnemonic, + bip85Config, + xDerivationPath: derivationConfig + ? derivationConfig.path + : WalletUtilities.getDerivationPath(EntityKind.WALLET, networkType), + }; + specs = generateWalletSpecsFromMnemonic(mnemonic, network, derivationDetails.xDerivationPath); } - const derivationDetails = { - instanceNum, - mnemonic, - bip85Config, - xDerivationPath, - }; - const defaultShell = 1; const presentationData: WalletPresentationData = { name: walletName, @@ -132,8 +207,6 @@ export const generateWallet = async ({ shell: defaultShell, }; - const specs: WalletSpecs = generateWalletSpecs(mnemonic, network, xDerivationPath); - const wallet: Wallet = { id, entityKind: EntityKind.WALLET, @@ -167,27 +240,28 @@ const generateExtendedKeysForCosigner = ( return { extendedKeys, xDerivationPath }; }; -export const getCosignerDetails = (wallet: Wallet, appId: string) => { - const deviceId = appId; +export const getCosignerDetails = (wallet: Wallet, singleSig: boolean = false) => { const masterFingerprint = wallet.id; - const { extendedKeys: multiSigExtendedKeys, xDerivationPath: multiSigXderivationPath } = - generateExtendedKeysForCosigner(wallet); - const { extendedKeys: singleSigExtendedKeys, xDerivationPath: singleSigXderivationPath } = - generateExtendedKeysForCosigner(wallet, EntityKind.WALLET); + const { extendedKeys, xDerivationPath } = generateExtendedKeysForCosigner( + wallet, + singleSig ? EntityKind.WALLET : EntityKind.VAULT + ); const xpubDetails: XpubDetailsType = {}; - xpubDetails[XpubTypes.P2WPKH] = { - xpub: singleSigExtendedKeys.xpub, - derivationPath: singleSigXderivationPath, - }; - xpubDetails[XpubTypes.P2WSH] = { - xpub: multiSigExtendedKeys.xpub, - derivationPath: multiSigXderivationPath, - }; + if (singleSig) { + xpubDetails[XpubTypes.P2WPKH] = { + xpub: extendedKeys.xpub, + derivationPath: xDerivationPath, + }; + } else { + xpubDetails[XpubTypes.P2WSH] = { + xpub: extendedKeys.xpub, + derivationPath: xDerivationPath, + }; + } return { - deviceId, mfp: masterFingerprint, xpubDetails, }; diff --git a/src/core/wallets/interfaces/index.ts b/src/core/wallets/interfaces/index.ts index fa04690cb..92d5af4b7 100644 --- a/src/core/wallets/interfaces/index.ts +++ b/src/core/wallets/interfaces/index.ts @@ -180,7 +180,7 @@ export interface SigningPayload { } export interface SerializedPSBTEnvelop { - signerId: string; + xfp: string; signerType: SignerType; serializedPSBT: string; signingPayload?: SigningPayload[]; diff --git a/src/core/wallets/interfaces/vault.ts b/src/core/wallets/interfaces/vault.ts index 451ca740d..7bd4db0b3 100644 --- a/src/core/wallets/interfaces/vault.ts +++ b/src/core/wallets/interfaces/vault.ts @@ -40,31 +40,49 @@ export type XpubDetailsType = { [key in XpubTypes as string]: { xpub: string; derivationPath: string; xpriv?: string }; }; -export type DeviceInfo = { - registeredWallet?: string; +export type signerXpubs = { + [key in XpubTypes as string]: { xpub: string; derivationPath: string; xpriv?: string }[]; }; -export interface VaultSigner { - signerId: string; + +export interface Signer { + // Represents a h/w or s/w wallet(Signer) + // Rel: Signer hosts multiple VaultSigners(key), diff derivation paths + // Note: Assisted Keys(IKS and SS) can only have one key(VaultSigner) per Signer type: SignerType; storageType: SignerStorage; isMock?: boolean; - xpub: string; - xpriv?: string; + masterFingerprint: string; + signerXpubs: signerXpubs; signerName?: string; signerDescription?: string; - bip85Config?: BIP85Config; lastHealthCheck: Date; addedOn: Date; + bip85Config?: BIP85Config; + signerPolicy?: SignerPolicy; // Signing Server's Signer Policy + inheritanceKeyInfo?: InheritanceKeyInfo; // IKS config and policy + hidden: boolean; +} + +export type RegisteredVaultInfo = { + vaultId: string; registered: boolean; + registrationInfo?: string; +}; + +export interface VaultSigner { + // Represents xpub(Extended Key) belonging to one of the Signers, + // Rel: VaultSigner(Extended Key) could only belong to one Signer, and is an active part of a Vault(s) masterFingerprint: string; + xpub: string; + xpriv?: string; + xfp: string; derivationPath: string; - xpubDetails: XpubDetailsType; - signerPolicy?: SignerPolicy; - inheritanceKeyInfo?: InheritanceKeyInfo; - deviceInfo?: DeviceInfo; + registeredVaults?: RegisteredVaultInfo[]; } export interface Vault { + // Represents a Vault + // Rel: Created using multiple VaultSigners(Extended Keys) id: string; // vault identifier(derived from xpub) shellId: string; entityKind: EntityKind; // Vault vs Wallet identifier @@ -73,7 +91,7 @@ export interface Vault { isUsable: boolean; // true if vault is usable isMultiSig: boolean; // true scheme: VaultScheme; // scheme of vault(m of n) - signers: VaultSigner[]; + signers: VaultSigner[]; // signers of the vault presentationData: VaultPresentationData; specs: VaultSpecs; archived: boolean; diff --git a/src/core/wallets/interfaces/wallet.ts b/src/core/wallets/interfaces/wallet.ts index 8151dbf9c..4f0e57697 100644 --- a/src/core/wallets/interfaces/wallet.ts +++ b/src/core/wallets/interfaces/wallet.ts @@ -1,20 +1,28 @@ import { DerivationConfig } from 'src/store/sagas/wallets'; import { Balances, BIP85Config, UTXO, Transaction } from '.'; -import { NetworkType, WalletType, VisibilityType, EntityKind, ScriptTypes } from '../enums'; +import { + NetworkType, + WalletType, + VisibilityType, + EntityKind, + ScriptTypes, + ImportedKeyType, + DerivationPurpose, +} from '../enums'; export interface WalletImportDetails { - // importing via mnemonic - mnemonic?: string; - - // importing via xpriv/xpub - // extendedKey?: string; - + importedKey: string; + importedKeyDetails: { + importedKeyType: ImportedKeyType; + watchOnly: Boolean; + purpose: DerivationPurpose; + }; derivationConfig: DerivationConfig; } export interface WalletDerivationDetails { instanceNum: number; // instance number of this particular walletType - mnemonic: string; // mnemonic of the wallet + mnemonic?: string; // mnemonic of the wallet bip85Config?: BIP85Config; // bip85 configuration leading to the derivation path for the corresponding entropy xDerivationPath: string; // derivation path of the extended keys belonging to this wallet } diff --git a/src/core/wallets/operations/index.ts b/src/core/wallets/operations/index.ts index 1fa9fb929..398962f8a 100644 --- a/src/core/wallets/operations/index.ts +++ b/src/core/wallets/operations/index.ts @@ -45,17 +45,17 @@ import { TransactionType, TxPriority, } from '../enums'; -import { Vault, VaultScheme, VaultSigner, VaultSpecs } from '../interfaces/vault'; +import { Signer, Vault, VaultSigner, VaultSpecs } from '../interfaces/vault'; import { AddressCache, AddressPubs, Wallet, WalletSpecs } from '../interfaces/wallet'; import WalletUtilities from './utils'; -import RestClient from 'src/services/rest/RestClient'; +import RestClient, { TorStatus } from 'src/services/rest/RestClient'; const ECPair = ECPairFactory(ecc); const validator = (pubkey: Buffer, msghash: Buffer, signature: Buffer): boolean => ECPair.fromPublicKey(pubkey).verify(msghash, signature); -const feeSurcharge = (wallet: Wallet | Vault) => +const testnetFeeSurcharge = (wallet: Wallet | Vault) => /* !! TESTNET ONLY !! as the redeem script for vault is heavy(esp. 3-of-5/3-of-6), the nodes reject the tx if the overall fee for the tx is low(which is the case w/ electrum) @@ -470,8 +470,8 @@ export default class WalletOperations { estimatedBlocks: lowFeeBlockEstimate, }; - const feeRatesByPriority = { high, medium, low }; - return feeRatesByPriority; + const feeRatesByPriority = { high, medium, low }; + return feeRatesByPriority; } catch (err) { console.log('Failed to fetch fee via Fulcrum', { err }); throw new Error('Failed to fetch fee'); @@ -485,7 +485,12 @@ export default class WalletOperations { return WalletOperations.mockFeeRatesForTestnet(); try { - const res = await RestClient.get(`https://mempool.space/api/v1/fees/recommended`); + const endpoint = + RestClient.getTorStatus() === TorStatus.CONNECTED + ? 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/fees/recommended' + : 'https://mempool.space/api/v1/fees/recommended'; + const res = await RestClient.get(endpoint); + const mempoolFee: { economyFee: number; fastestFee: number; @@ -579,7 +584,11 @@ export default class WalletOperations { }).address, }); } - const { fee } = coinselectSplit(inputUTXOs, outputUTXOs, feePerByte + feeSurcharge(wallet)); + const { fee } = coinselectSplit( + inputUTXOs, + outputUTXOs, + feePerByte + testnetFeeSurcharge(wallet) + ); return { fee, @@ -593,7 +602,7 @@ export default class WalletOperations { amount: number; }[], averageTxFees: AverageTxFees, - selectedUTXOs?: any + selectedUTXOs?: UTXO[] ): | { fee: number; @@ -624,7 +633,11 @@ export default class WalletOperations { const defaultFeePerByte = averageTxFees[defaultTxPriority].feePerByte; const defaultEstimatedBlocks = averageTxFees[defaultTxPriority].estimatedBlocks; - const assets = coinselect(inputUTXOs, outputUTXOs, defaultFeePerByte + feeSurcharge(wallet)); + const assets = coinselect( + inputUTXOs, + outputUTXOs, + defaultFeePerByte + testnetFeeSurcharge(wallet) + ); const defaultPriorityInputs = assets.inputs; const defaultPriorityOutputs = assets.outputs; const defaultPriorityFee = assets.fee; @@ -657,7 +670,7 @@ export default class WalletOperations { const { inputs, outputs, fee } = coinselect( inputUTXOs, outputUTXOs, - averageTxFees[priority].feePerByte + feeSurcharge(wallet) + averageTxFees[priority].feePerByte + testnetFeeSurcharge(wallet) ); const debitedAmount = netAmount + fee; if (!inputs || debitedAmount > confirmedBalance) { @@ -688,21 +701,25 @@ export default class WalletOperations { address: string; value: number; }[], - customTxFeePerByte: number - ): TransactionPrerequisiteElements => { - const inputUTXOs = wallet.specs.confirmedUTXOs; + customTxFeePerByte: number, + selectedUTXOs?: UTXO[] + ): TransactionPrerequisite => { + const inputUTXOs = + selectedUTXOs && selectedUTXOs.length ? selectedUTXOs : wallet.specs.confirmedUTXOs; const { inputs, outputs, fee } = coinselect( inputUTXOs, outputUTXOs, - customTxFeePerByte + feeSurcharge(wallet) + customTxFeePerByte + testnetFeeSurcharge(wallet) ); if (!inputs) return { fee }; return { - inputs, - outputs, - fee, + [TxPriority.CUSTOM]: { + inputs, + outputs, + fee, + }, }; }; @@ -747,7 +764,7 @@ export default class WalletOperations { const bip32Derivation = [ { masterFingerprint: Buffer.from(masterFingerprint, 'hex'), - path, + path: path.replaceAll('h', "'"), pubkey: publicKey, }, ]; @@ -852,7 +869,7 @@ export default class WalletOperations { wallet: Wallet | Vault, txPrerequisites: TransactionPrerequisite, txnPriority: string, - customTxPrerequisites?: TransactionPrerequisiteElements, + customTxPrerequisites?: TransactionPrerequisite, derivationPurpose?: DerivationPurpose, scriptType?: BIP48ScriptTypes ): Promise<{ @@ -864,9 +881,10 @@ export default class WalletOperations { try { let inputs; let outputs; - if (txnPriority === TxPriority.CUSTOM && customTxPrerequisites) { - inputs = customTxPrerequisites.inputs; - outputs = customTxPrerequisites.outputs; + if (txnPriority === TxPriority.CUSTOM) { + if (!customTxPrerequisites) throw new Error('Tx-prerequisites missing for custom fee'); + inputs = customTxPrerequisites[txnPriority].inputs; + outputs = customTxPrerequisites[txnPriority].outputs; } else { inputs = txPrerequisites[txnPriority].inputs; outputs = txPrerequisites[txnPriority].outputs; @@ -1024,10 +1042,11 @@ export default class WalletOperations { wallet: Vault, inputs: InputUTXOs[], PSBT: bitcoinJS.Psbt, - signer: VaultSigner, + vaultKey: VaultSigner, outgoing: number, outputs: OutputUTXOs[], - change: string + change: string, + signerMap?: { [key: string]: Signer } ): | { signedPSBT: bitcoinJS.Psbt; @@ -1038,15 +1057,16 @@ export default class WalletOperations { serializedPSBTEnvelop: SerializedPSBTEnvelop; } => { const signingPayload: SigningPayload[] = []; + const signer = signerMap[vaultKey.masterFingerprint]; const payloadTarget = signer.type; let isSigned = false; - if (signer.isMock && signer.xpriv) { + if (signer.isMock && vaultKey.xpriv) { // case: if the signer is mock and has an xpriv attached to it, we'll sign the PSBT right away const { signedSerializedPSBT } = WalletOperations.internallySignVaultPSBT( wallet, inputs, PSBT.toBase64(), - signer.xpriv + vaultKey.xpriv ); PSBT = bitcoinJS.Psbt.fromBase64(signedSerializedPSBT, { network: config.NETWORK }); isSigned = true; @@ -1065,7 +1085,7 @@ export default class WalletOperations { inputs[inputIndex].address, wallet ); - publicKey = multisigAddress.signerPubkeyMap.get(signer.xpub); + publicKey = multisigAddress.signerPubkeyMap.get(vaultKey.xpub); subPath = multisigAddress.subPath; } else { const singlesigAddress = WalletUtilities.addressToKey( @@ -1120,7 +1140,7 @@ export default class WalletOperations { const serializedPSBT = PSBT.toBase64(); const serializedPSBTEnvelop: SerializedPSBTEnvelop = { - signerId: signer.signerId, + xfp: vaultKey.xfp, signerType: signer.type, serializedPSBT, signingPayload, @@ -1209,12 +1229,12 @@ export default class WalletOperations { wallet: Wallet | Vault, txPrerequisites: TransactionPrerequisite, txnPriority: TxPriority, - network: bitcoinJS.networks.Network, recipients: { address: string; amount: number; }[], - customTxPrerequisites?: TransactionPrerequisiteElements + customTxPrerequisites?: TransactionPrerequisite, + signerMap?: { [key: string]: Signer } ): Promise< | { serializedPSBTEnvelops: SerializedPSBTEnvelop[]; @@ -1236,22 +1256,23 @@ export default class WalletOperations { if (wallet.entityKind === EntityKind.VAULT) { // case: vault(single/multi-sig) - const { signers } = wallet as Vault; + const { signers: vaultKeys } = wallet as Vault; const serializedPSBTEnvelops: SerializedPSBTEnvelop[] = []; let outgoing = 0; recipients.forEach((recipient) => { outgoing += recipient.amount; }); - for (const signer of signers) { + for (const vaultKey of vaultKeys) { const { serializedPSBTEnvelop } = WalletOperations.signVaultTransaction( wallet as Vault, inputs, PSBT, - signer, + vaultKey, outgoing, outputs, - change + change, + signerMap ); serializedPSBTEnvelops.push(serializedPSBTEnvelop); } @@ -1284,12 +1305,18 @@ export default class WalletOperations { serializedPSBTEnvelops: SerializedPSBTEnvelop[], txPrerequisites: TransactionPrerequisite, txnPriority: TxPriority, + customTxPrerequisites?: TransactionPrerequisite, txHex?: string ): Promise<{ txid: string; finalOutputs: bitcoinJS.TxOutput[]; }> => { - const { inputs } = txPrerequisites[txnPriority]; + let inputs; + if (txnPriority === TxPriority.CUSTOM) { + if (!customTxPrerequisites) throw new Error('Tx-prerequisites missing for custom fee'); + inputs = customTxPrerequisites[txnPriority].inputs; + } else inputs = txPrerequisites[txnPriority].inputs; + let combinedPSBT: bitcoinJS.Psbt = null; let finalOutputs: bitcoinJS.TxOutput[]; diff --git a/src/core/wallets/operations/utils.ts b/src/core/wallets/operations/utils.ts index eb7c0a333..1b12ae127 100644 --- a/src/core/wallets/operations/utils.ts +++ b/src/core/wallets/operations/utils.ts @@ -24,9 +24,11 @@ import { BIP48ScriptTypes, DerivationPurpose, EntityKind, + ImportedKeyType, NetworkType, PaymentInfoKind, ScriptTypes, + XpubTypes, } from '../enums'; import { OutputUTXOs } from '../interfaces'; import config from 'src/core/config'; @@ -305,6 +307,11 @@ export default class WalletUtilities { return childXKey.toBase58(); }; + static getPublicExtendedKeyFromPriv = (extendedKey: string): string => { + const xKey = bip32.fromBase58(extendedKey, config.NETWORK); + return xKey.neutered().toBase58(); + }; + static getNetworkFromXpub = (xpub: string) => { if (xpub) { return xpub.startsWith('xpub') || xpub.startsWith('ypub') || xpub.startsWith('zpub') @@ -313,25 +320,6 @@ export default class WalletUtilities { } }; - static getNetworkFromPrefix = (prefix) => { - switch (prefix) { - case 'xpub': // 0x0488b21e - case 'ypub': // 0x049d7cb2 - case 'Ypub': // 0x0295b43f - case 'zpub': // 0x04b24746 - case 'Zpub': // 0x02aa7ed3 - return NetworkType.MAINNET; - case 'tpub': // 0x043587cf - case 'upub': // 0x044a5262 - case 'Upub': // 0x024289ef - case 'vpub': // 0x045f1cf6 - case 'Vpub': // 0x02575483 - return NetworkType.TESTNET; - default: - return null; - } - }; - static generateYpub = (xpub: string, network: bitcoinJS.Network): string => { // generates ypub corresponding to supplied xpub || upub corresponding to tpub let data = bs58check.decode(xpub); @@ -340,9 +328,17 @@ export default class WalletUtilities { return bs58check.encode(data); }; - static generateXpubFromYpub = (ypub: string, network: bitcoinJS.Network): string => { - // generates xpub corresponding to supplied ypub || tpub corresponding to upub - let data = bs58check.decode(ypub); + static getXprivFromExtendedKey = (extendedKey: string, network: bitcoinJS.Network) => { + // case: xprv corresponding to supplied yprv/zprv or tprv corresponding to supplied uprv/vprv + let data = bs58check.decode(extendedKey); + const versionBytes = bitcoinJS.networks.bitcoin === network ? '0488ade4' : '04358394'; + data = Buffer.concat([Buffer.from(versionBytes, 'hex'), data.slice(4)]); + return bs58check.encode(data); + }; + + static getXpubFromExtendedKey = (extendedKey: string, network: bitcoinJS.Network) => { + // case: xpub corresponding to supplied ypub/zpub or tpub corresponding to supplied upub/vpub + let data = bs58check.decode(extendedKey); const versionBytes = bitcoinJS.networks.bitcoin === network ? '0488b21e' : '043587cf'; data = Buffer.concat([Buffer.from(versionBytes, 'hex'), data.slice(4)]); return bs58check.encode(data); @@ -781,4 +777,156 @@ export default class WalletUtilities { } return null; }; + + static getScriptTypeFromDerivationPath = (derivationPath: string): XpubTypes => { + const purpose = WalletUtilities.getPurpose(derivationPath); + switch (purpose) { + case DerivationPurpose.BIP48: + return XpubTypes.P2WSH; + case DerivationPurpose.BIP84: + return XpubTypes.P2WPKH; + case DerivationPurpose.BIP86: + return XpubTypes.P2TR; + case DerivationPurpose.BIP49: + return XpubTypes['P2SH-P2WPKH']; + case DerivationPurpose.BIP44: + return XpubTypes.P2PKH; + default: + return XpubTypes.P2WSH; + } + }; + + static isExtendedPrvKey = (keyType: ImportedKeyType) => { + if (config.NETWORK === bitcoinJS.networks.bitcoin) + return [ImportedKeyType.XPRV, ImportedKeyType.YPRV, ImportedKeyType.ZPRV].includes(keyType); + else + return [ImportedKeyType.TPRV, ImportedKeyType.UPRV, ImportedKeyType.VPRV].includes(keyType); + }; + + static isExtendedPubKey = (keyType: ImportedKeyType) => { + if (config.NETWORK === bitcoinJS.networks.bitcoin) + return [ImportedKeyType.XPUB, ImportedKeyType.YPUB, ImportedKeyType.ZPUB].includes(keyType); + else + return [ImportedKeyType.TPUB, ImportedKeyType.UPUB, ImportedKeyType.VPUB].includes(keyType); + }; + + static getImportedKeyDetails = ( + input: string + ): { importedKeyType: ImportedKeyType; watchOnly: Boolean; purpose: DerivationPurpose } => { + try { + // case: mnemonic + bip39.mnemonicToEntropy(input); + return { importedKeyType: ImportedKeyType.MNEMONIC, watchOnly: false, purpose: null }; + } catch (err) { + try { + // case: extended keys + bs58check.decode(input); + + // attempt to create an extended key from the input + if (config.NETWORK === bitcoinJS.networks.bitcoin) { + // extended public keys (mainnet) + if (input.startsWith(ImportedKeyType.XPUB)) + return { + importedKeyType: ImportedKeyType.XPUB, + watchOnly: true, + purpose: DerivationPurpose.BIP44, + }; + if (input.startsWith(ImportedKeyType.YPUB)) + return { + importedKeyType: ImportedKeyType.YPUB, + watchOnly: true, + purpose: DerivationPurpose.BIP49, + }; + if (input.startsWith(ImportedKeyType.ZPUB)) + return { + importedKeyType: ImportedKeyType.ZPUB, + watchOnly: true, + purpose: DerivationPurpose.BIP84, + }; + + // extended private keys (mainnet) + if (input.startsWith(ImportedKeyType.XPRV)) + return { + importedKeyType: ImportedKeyType.XPRV, + watchOnly: false, + purpose: DerivationPurpose.BIP44, + }; + if (input.startsWith(ImportedKeyType.YPRV)) + return { + importedKeyType: ImportedKeyType.YPRV, + watchOnly: false, + purpose: DerivationPurpose.BIP49, + }; + if (input.startsWith(ImportedKeyType.ZPRV)) + return { + importedKeyType: ImportedKeyType.ZPRV, + watchOnly: false, + purpose: DerivationPurpose.BIP84, + }; + } else { + // extended public keys (testnet) + if (input.startsWith(ImportedKeyType.TPUB)) + return { + importedKeyType: ImportedKeyType.TPUB, + watchOnly: true, + purpose: DerivationPurpose.BIP44, + }; + if (input.startsWith(ImportedKeyType.UPUB)) + return { + importedKeyType: ImportedKeyType.UPUB, + watchOnly: true, + purpose: DerivationPurpose.BIP49, + }; + if (input.startsWith(ImportedKeyType.VPUB)) + return { + importedKeyType: ImportedKeyType.VPUB, + watchOnly: true, + purpose: DerivationPurpose.BIP84, + }; + + // extended private keys (testnet) + if (input.startsWith(ImportedKeyType.TPRV)) + return { + importedKeyType: ImportedKeyType.TPRV, + watchOnly: false, + purpose: DerivationPurpose.BIP44, + }; + if (input.startsWith(ImportedKeyType.UPRV)) + return { + importedKeyType: ImportedKeyType.UPRV, + watchOnly: false, + purpose: DerivationPurpose.BIP49, + }; + if (input.startsWith(ImportedKeyType.VPRV)) + return { + importedKeyType: ImportedKeyType.VPRV, + watchOnly: false, + purpose: DerivationPurpose.BIP84, + }; + } + } catch (err) { + // if neither mnemonic nor extended key, consider it an invalid input + throw new Error('Invalid Import Key'); + } + } + }; + + static getNetworkFromPrefix = (prefix) => { + switch (prefix) { + case 'xpub': // 0x0488b21e + case 'ypub': // 0x049d7cb2 + case 'Ypub': // 0x0295b43f + case 'zpub': // 0x04b24746 + case 'Zpub': // 0x02aa7ed3 + return NetworkType.MAINNET; + case 'tpub': // 0x043587cf + case 'upub': // 0x044a5262 + case 'Upub': // 0x024289ef + case 'vpub': // 0x045f1cf6 + case 'Vpub': // 0x02575483 + return NetworkType.TESTNET; + default: + return null; + } + }; } diff --git a/src/hardware/bitbox/index.ts b/src/hardware/bitbox/index.ts index f4ddb5527..a7756e9d8 100644 --- a/src/hardware/bitbox/index.ts +++ b/src/hardware/bitbox/index.ts @@ -1,6 +1,6 @@ /* eslint-disable no-await-in-loop */ import { SignerType, XpubTypes } from 'src/core/wallets/enums'; -import { Vault, VaultSigner, XpubDetailsType } from 'src/core/wallets/interfaces/vault'; +import { Signer, Vault, VaultSigner, XpubDetailsType } from 'src/core/wallets/interfaces/vault'; import { HWErrorType } from 'src/models/enums/Hardware'; import WalletUtilities from 'src/core/wallets/operations/utils'; import config from 'src/core/config'; @@ -25,7 +25,7 @@ export const getBitbox02Details = (data, isMultisig) => { return { xpub, derivationPath, - xfp: WalletUtilities.getFingerprintFromExtendedKey(xpub, network), + masterFingerprint: WalletUtilities.getFingerprintFromExtendedKey(xpub, network), xpubDetails, }; } catch (_) { @@ -33,10 +33,14 @@ export const getBitbox02Details = (data, isMultisig) => { } }; -export const getWalletConfigForBitBox02 = ({ vault }: { vault: Vault }) => { - const ourXPubIndex = vault.signers.findIndex((signer) => signer.type === SignerType.BITBOX02); +export const getWalletConfigForBitBox02 = ({ vault, signer }: { vault: Vault; signer: Signer }) => { + const ourXPubIndex = vault.signers.findIndex( + (vaultKey) => + signer.type === SignerType.BITBOX02 && signer.masterFingerprint === vaultKey.masterFingerprint + ); const keypathAccountDerivation = vault.signers.find( - (signer) => signer.type === SignerType.BITBOX02 + (vaultKey) => + signer.type === SignerType.BITBOX02 && signer.masterFingerprint === vaultKey.masterFingerprint ).derivationPath; return { ourXPubIndex, @@ -49,9 +53,10 @@ export const getWalletConfigForBitBox02 = ({ vault }: { vault: Vault }) => { export const getTxForBitBox02 = async ( serializedPSBT: string, signingPayload: SigningPayload[], - signer: VaultSigner, + vaultKey: VaultSigner, isMultisig: boolean, - vault: Vault + vault: Vault, + signer: Signer ) => { try { const payload = signingPayload[0]; @@ -62,7 +67,7 @@ export const getTxForBitBox02 = async ( change: changeAddress, inputsToSign, } = payload; - const keypathAccount = getKeypathFromString(signer.derivationPath); + const keypathAccount = getKeypathFromString(vaultKey.derivationPath); const inputs = []; let index = 0; const { version, locktime } = psbt; @@ -111,7 +116,7 @@ export const getTxForBitBox02 = async ( payload: WalletUtilities.getPubkeyHashFromScript(output.address, output.script), }; }); - const walletConfig = isMultisig ? getWalletConfigForBitBox02({ vault }) : null; + const walletConfig = isMultisig ? getWalletConfigForBitBox02({ vault, signer }) : null; return { inputs, outputs, @@ -119,7 +124,7 @@ export const getTxForBitBox02 = async ( walletConfig, version, locktime, - derivationPath: signer.derivationPath, + derivationPath: vaultKey.derivationPath, }; } catch (error) { captureError(error); diff --git a/src/hardware/coldcard/index.ts b/src/hardware/coldcard/index.ts index b153dfbf9..b89ce1343 100644 --- a/src/hardware/coldcard/index.ts +++ b/src/hardware/coldcard/index.ts @@ -23,7 +23,12 @@ export const extractColdCardExport = (data, isMultisig) => { xpubDetails[XpubTypes.P2WSH] = { xpub: multiSigXpub, derivationPath: multiSigPath }; const xpub = isMultisig ? multiSigXpub : singleSigXpub; const derivationPath = isMultisig ? multiSigPath : singleSigPath; - return { xpub, derivationPath, xfp: data.xfp, xpubDetails }; + return { xpub, derivationPath, masterFingerprint: data.xfp, xpubDetails }; +}; + +export const getConfigDetails = async () => { + const { data } = (await NFC.read(NfcTech.NfcV))[0]; + return data; }; export const getColdcardDetails = async (isMultisig: boolean) => { diff --git a/src/hardware/index.ts b/src/hardware/index.ts index a0032a680..2076bdd11 100644 --- a/src/hardware/index.ts +++ b/src/hardware/index.ts @@ -1,8 +1,10 @@ import { + Signer, Vault, VaultScheme, VaultSigner, XpubDetailsType, + signerXpubs, } from 'src/core/wallets/interfaces/vault'; import { @@ -11,7 +13,6 @@ import { NetworkType, SignerStorage, SignerType, - XpubTypes, } from 'src/core/wallets/enums'; import WalletUtilities from 'src/core/wallets/operations/utils'; import config, { APP_STAGE } from 'src/core/config'; @@ -19,6 +20,7 @@ import { HWErrorType } from 'src/models/enums/Hardware'; import { generateMockExtendedKeyForSigner } from 'src/core/wallets/factories/VaultFactory'; import idx from 'idx'; import HWError from './HWErrorState'; +import { SubscriptionTier } from 'src/models/enums/SubscriptionTier'; export const UNVERIFYING_SIGNERS = [ SignerType.JADE, @@ -33,17 +35,18 @@ export const UNVERIFYING_SIGNERS = [ export const generateSignerFromMetaData = ({ xpub, derivationPath, - xfp, + masterFingerprint, signerType, storageType, isMultisig, xpriv = null, isMock = false, - xpubDetails = {} as XpubDetailsType, - signerId = null, + xpubDetails = null as XpubDetailsType, + xfp = null, signerPolicy = null, inheritanceKeyInfo = null, -}): VaultSigner => { + isAmf = false, +}): { signer: Signer; key: VaultSigner } => { const networkType = WalletUtilities.getNetworkFromPrefix(xpub.slice(0, 4)); const network = WalletUtilities.getNetworkByType(config.NETWORK_TYPE); if ( @@ -54,29 +57,43 @@ export const generateSignerFromMetaData = ({ ) { throw new HWError(HWErrorType.INCORRECT_NETWORK); } - xpub = WalletUtilities.generateXpubFromYpub(xpub, network); - xpubDetails = Object.keys(xpubDetails).length - ? xpubDetails - : { [isMultisig ? XpubTypes.P2WSH : XpubTypes.P2WPKH]: { xpub, derivationPath, xpriv } }; - signerId = signerId || WalletUtilities.getFingerprintFromExtendedKey(xpub, network); - const signer: VaultSigner = { - signerId, + xpub = WalletUtilities.getXpubFromExtendedKey(xpub, network); + + const signerXpubs: signerXpubs = {}; + if (!xpubDetails) { + const scriptType = WalletUtilities.getScriptTypeFromDerivationPath(derivationPath); + signerXpubs[scriptType] = [{ xpub, xpriv, derivationPath }]; + } else { + Object.entries(xpubDetails).forEach(([key, xpubDetail]) => { + const { xpub, xpriv, derivationPath } = xpubDetail; + signerXpubs[key] = signerXpubs[key] || []; + signerXpubs[key].push({ xpub, xpriv, derivationPath }); + }); + } + + const signer: Signer = { type: signerType, - signerName: getSignerNameFromType(signerType, isMock, !!xpubDetails[XpubTypes.AMF]), - xpub, - xpriv, - derivationPath, - masterFingerprint: xfp, + storageType, isMock, + signerName: getSignerNameFromType(signerType, isMock, isAmf), lastHealthCheck: new Date(), addedOn: new Date(), - storageType, - registered: UNVERIFYING_SIGNERS.includes(signerType) || isMock, - xpubDetails, + masterFingerprint, signerPolicy, inheritanceKeyInfo, + signerXpubs, + hidden: false, }; - return signer; + + const key: VaultSigner = { + xfp: xfp || WalletUtilities.getFingerprintFromExtendedKey(xpub, network), + derivationPath, + xpub, + xpriv, + masterFingerprint, + }; + + return { signer, key }; }; export const getSignerNameFromType = (type: SignerType, isMock = false, isAmf = false) => { @@ -89,7 +106,7 @@ export const getSignerNameFromType = (type: SignerType, isMock = false, isAmf = name = 'Jade'; break; case SignerType.KEEPER: - name = 'Keeper Signing Device'; + name = 'Collaborative Key'; break; case SignerType.KEYSTONE: name = 'Keystone'; @@ -118,11 +135,17 @@ export const getSignerNameFromType = (type: SignerType, isMock = false, isAmf = case SignerType.SEEDSIGNER: name = 'SeedSigner'; break; + case SignerType.SPECTER: + name = 'Specter'; + break; case SignerType.BITBOX02: name = 'BitBox02'; break; case SignerType.OTHER_SD: - name = 'Other Signing Device'; + name = 'Other signer'; + break; + case SignerType.UNKOWN_SIGNER: + name = 'Unknown Signer'; break; case SignerType.INHERITANCEKEY: name = 'Inheritance Key'; @@ -142,10 +165,10 @@ export const getSignerNameFromType = (type: SignerType, isMock = false, isAmf = export const getWalletConfig = ({ vault }: { vault: Vault }) => { let line = '# Multisig setup file (exported from Keeper)\n'; - line += `Name: Keeper Vault\n`; + line += 'Name: Keeper vault\n'; line += `Policy: ${vault.scheme.m} of ${vault.scheme.n}\n`; - line += `Format: P2WSH\n`; - line += `\n`; + line += 'Format: P2WSH\n'; + line += '\n'; vault.signers.forEach((signer) => { line += `Derivation: ${signer.derivationPath}\n`; line += `${signer.masterFingerprint}: ${signer.xpub}\n\n`; @@ -153,8 +176,8 @@ export const getWalletConfig = ({ vault }: { vault: Vault }) => { return line; }; -export const getSignerSigTypeInfo = (signer: VaultSigner) => { - const purpose = WalletUtilities.getSignerPurposeFromPath(signer.derivationPath); +export const getSignerSigTypeInfo = (key: VaultSigner, signer: Signer) => { + const purpose = WalletUtilities.getSignerPurposeFromPath(key.derivationPath); if ( signer.isMock || (signer.type === SignerType.TAPSIGNER && config.NETWORK_TYPE === NetworkType.TESTNET) // amf flow @@ -175,23 +198,22 @@ export const getMockSigner = (signerType: SignerType) => { signerType, networkType ); - const signer: VaultSigner = generateSignerFromMetaData({ + const { signer, key } = generateSignerFromMetaData({ xpub, xpriv, derivationPath, - xfp: masterFingerprint, + masterFingerprint, signerType, storageType: SignerStorage.COLD, isMock: true, isMultisig: true, }); - return signer; + return { signer, key }; } return null; }; -export const isSignerAMF = (signer: VaultSigner) => - !!idx(signer, (_) => _.xpubDetails[XpubTypes.AMF].xpub); +export const isSignerAMF = (signer: Signer) => !!idx(signer, (_) => _.signerName.includes('*')); const HARDENED = 0x80000000; export const getKeypathFromString = (keypathString: string): number[] => { @@ -208,44 +230,14 @@ export const getKeypathFromString = (keypathString: string): number[] => { }); }; -const SIGNLE_ALLOWED_SIGNERS = [SignerType.POLICY_SERVER, SignerType.MOBILE_KEY]; - -const allowSingleKey = (type, vaultSigners) => { - if (vaultSigners.find((s) => s.type === type)) { - if (SIGNLE_ALLOWED_SIGNERS.includes(type)) { - return true; - } - return false; - } - return false; -}; - -const getDisabled = (type: SignerType, isOnL1, vaultSigners, scheme) => { - // Keys Incase of level 1 we have level 1 - if (isOnL1) { - return { disabled: true, message: 'Upgrade tier to use as key' }; - } - - if (type === SignerType.POLICY_SERVER && (scheme.n < 3 || scheme.m < 2)) { - return { - disabled: true, - message: 'Please create a vault with a minimum of 3 signers and 2 required signers', - }; - } - // Keys Incase of already added - if (allowSingleKey(type, vaultSigners)) { - return { disabled: true, message: 'Key already added to the Vault' }; - } - - return { disabled: false, message: '' }; -}; - export const getDeviceStatus = ( type: SignerType, - isNfcSupported, - vaultSigners, - isOnL1, - scheme: VaultScheme + isNfcSupported: boolean, + isOnL1: boolean, + isOnL2: boolean, + scheme: VaultScheme, + existingSigners: Signer[], + addSignerFlow: boolean = false ) => { switch (type) { case SignerType.COLDCARD: @@ -255,37 +247,86 @@ export const getDeviceStatus = ( disabled: config.ENVIRONMENT !== APP_STAGE.DEVELOPMENT && !isNfcSupported, }; case SignerType.MOBILE_KEY: - return allowSingleKey(type, vaultSigners) - ? { disabled: true, message: 'Key already added to the Vault' } - : { - message: '', - disabled: false, - }; - case SignerType.POLICY_SERVER: - return { - message: getDisabled(type, isOnL1, vaultSigners, scheme).message, - disabled: getDisabled(type, isOnL1, vaultSigners, scheme).disabled, - }; + if (existingSigners.find((s) => s.type === SignerType.MOBILE_KEY)) { + return { message: `${getSignerNameFromType(type)} has been already added`, disabled: true }; + } else { + return { message: '', disabled: false }; + } + case SignerType.KEEPER: + return addSignerFlow || scheme?.n < 2 + ? { + message: `You can add a ${getSignerNameFromType( + type + )} in a multisig configuration only`, + disabled: true, + } + : { message: '', disabled: false }; case SignerType.TREZOR: - return scheme.n > 1 + return addSignerFlow || scheme?.n > 1 ? { disabled: true, message: 'Multisig with trezor is coming soon!' } - : { - message: '', - disabled: false, - }; - case SignerType.KEEPER: - case SignerType.SEED_WORDS: - case SignerType.JADE: - case SignerType.BITBOX02: - case SignerType.PASSPORT: - case SignerType.SEEDSIGNER: - case SignerType.LEDGER: - case SignerType.KEYSTONE: + : { message: '', disabled: false }; + case SignerType.POLICY_SERVER: + return getPolicyServerStatus(type, isOnL1, scheme, addSignerFlow, existingSigners); + case SignerType.INHERITANCEKEY: + return getInheritanceKeyStatus(type, isOnL1, isOnL2, scheme, addSignerFlow, existingSigners); default: - return { - message: '', - disabled: false, - }; + return { message: '', disabled: false }; + } +}; + +const getPolicyServerStatus = ( + type: SignerType, + isOnL1: boolean, + scheme: VaultScheme, + addSignerFlow: boolean, + existingSigners +) => { + if (addSignerFlow) { + return { + message: `Please add ${getSignerNameFromType(type)} from the vault creation flow`, + disabled: true, + }; + } else if (isOnL1) { + return { disabled: true, message: 'Upgrade tier to use as key' }; + } else if (existingSigners.find((s) => s.type === SignerType.POLICY_SERVER)) { + return { message: `${getSignerNameFromType(type)} has been already added`, disabled: true }; + } else if (type === SignerType.POLICY_SERVER && (scheme.n < 3 || scheme.m < 2)) { + return { + disabled: true, + message: 'Please create a vault with a minimum of 3 signers and 2 required signers', + }; + } else { + return { disabled: false, message: '' }; + } +}; + +const getInheritanceKeyStatus = ( + type: SignerType, + isOnL1: boolean, + isOnL2: boolean, + scheme: VaultScheme, + addSignerFlow: boolean, + existingSigners +) => { + if (addSignerFlow) { + return { + disabled: true, + message: `Please add ${getSignerNameFromType(type)} from the vault creation flow`, + }; + } else if (isOnL1 || isOnL2) { + return { + disabled: true, + message: `Please upgrade to ${SubscriptionTier.L3} to add an ${getSignerNameFromType(type)}`, + }; + } else if (existingSigners.find((s) => s.type === SignerType.INHERITANCEKEY)) { + return { message: `${getSignerNameFromType(type)} has been already added`, disabled: true }; + } else if (type === SignerType.INHERITANCEKEY && (scheme.n < 5 || scheme.m < 3)) { + return { + disabled: true, + message: 'Please create a vault with a minimum of 5 signers and 3 required signers', + }; + } else { + return { message: '', disabled: false }; } }; @@ -295,6 +336,7 @@ export const getSDMessage = ({ type }: { type: SignerType }) => { case SignerType.LEDGER: case SignerType.PASSPORT: case SignerType.BITBOX02: + case SignerType.SPECTER: case SignerType.KEYSTONE: { return 'Register for full verification'; } @@ -302,7 +344,7 @@ export const getSDMessage = ({ type }: { type: SignerType }) => { return 'Optional registration'; } case SignerType.KEEPER: { - return 'Hot keys on other device'; + return 'Use Collaborative Key as signer'; } case SignerType.MOBILE_KEY: { return 'Hot keys on this device'; @@ -332,3 +374,22 @@ export const getSDMessage = ({ type }: { type: SignerType }) => { return null; } }; + +export const extractKeyFromDescriptor = (data) => { + const xpub = data.slice(data.indexOf(']') + 1); + const masterFingerprint = data.slice(1, 9); + const derivationPath = data + .slice(data.indexOf('[') + 1, data.indexOf(']')) + .replace(masterFingerprint, 'm'); + const purpose = WalletUtilities.getSignerPurposeFromPath(derivationPath); + let forMultiSig: boolean; + let forSingleSig: boolean; + if (purpose && DerivationPurpose.BIP48.toString() === purpose) { + forMultiSig = true; + forSingleSig = false; + } else { + forMultiSig = false; + forSingleSig = true; + } + return { xpub, derivationPath, masterFingerprint, forMultiSig, forSingleSig }; +}; diff --git a/src/hardware/jade/index.ts b/src/hardware/jade/index.ts index 876c69a78..05f86ef11 100644 --- a/src/hardware/jade/index.ts +++ b/src/hardware/jade/index.ts @@ -13,5 +13,5 @@ export const getJadeDetails = (qrData) => { forMultiSig = false; forSingleSig = true; } - return { xpub, derivationPath, xfp: mfp, forMultiSig, forSingleSig }; + return { xpub, derivationPath, masterFingerprint: mfp, forMultiSig, forSingleSig }; }; diff --git a/src/hardware/keystone/index.ts b/src/hardware/keystone/index.ts index 9b5a9fc56..dd870f756 100644 --- a/src/hardware/keystone/index.ts +++ b/src/hardware/keystone/index.ts @@ -14,7 +14,7 @@ const getKeystoneDetails = (qrData) => { forMultiSig = false; forSingleSig = true; } - return { xpub, derivationPath, xfp: mfp, forMultiSig, forSingleSig }; + return { xpub, derivationPath, masterFingerprint: mfp, forMultiSig, forSingleSig }; }; const getKeystoneDetailsFromFile = (data) => { @@ -29,7 +29,7 @@ const getKeystoneDetailsFromFile = (data) => { forMultiSig = false; forSingleSig = true; } - return { xpub, derivationPath, xfp, forMultiSig, forSingleSig }; + return { xpub, derivationPath, masterFingerprint: xfp, forMultiSig, forSingleSig }; }; const getTxHexFromKeystonePSBT = (psbt, signedPsbt): bitcoin.Transaction => { diff --git a/src/hardware/ledger/index.ts b/src/hardware/ledger/index.ts index 707bb971d..c7d9d5357 100644 --- a/src/hardware/ledger/index.ts +++ b/src/hardware/ledger/index.ts @@ -18,7 +18,7 @@ export const getLedgerDetailsFromChannel = (data, isMultisig) => { return { xpub, derivationPath, - xfp, + masterFingerprint: xfp, xpubDetails, }; } catch (error) { diff --git a/src/hardware/passport/index.ts b/src/hardware/passport/index.ts index c3c4226e0..aa3ba7e88 100644 --- a/src/hardware/passport/index.ts +++ b/src/hardware/passport/index.ts @@ -7,15 +7,21 @@ const getPassportDetails = (qrData) => { try { const { p2wsh, p2wsh_deriv: derivationPath, xfp } = qrData; const network = WalletUtilities.getNetworkByType(config.NETWORK_TYPE); - const xpub = WalletUtilities.generateXpubFromYpub(p2wsh, network); - return { xpub, derivationPath, xfp, forMultiSig: true, forSingleSig: false }; + const xpub = WalletUtilities.getXpubFromExtendedKey(p2wsh, network); + return { xpub, derivationPath, masterFingerprint: xfp, forMultiSig: true, forSingleSig: false }; } catch (_) { console.log('Not exported for multisig!'); } try { const { xpub, deriv } = qrData.bip84; - return { xpub, derivationPath: deriv, xfp: qrData.xfp, forMultiSig: false, forSingleSig: true }; + return { + xpub, + derivationPath: deriv, + masterFingerprint: qrData.xfp, + forMultiSig: false, + forSingleSig: true, + }; } catch (_) { console.log('Not exported for singlesig!'); throw new HWError(HWErrorType.INCORRECT_HW); diff --git a/src/hardware/seedsigner/index.ts b/src/hardware/seedsigner/index.ts index 397ff3255..cc1cc6e2f 100644 --- a/src/hardware/seedsigner/index.ts +++ b/src/hardware/seedsigner/index.ts @@ -18,7 +18,7 @@ export const getSeedSignerDetails = (qrData) => { forMultiSig = false; forSingleSig = true; } - return { xpub, derivationPath, xfp, forMultiSig, forSingleSig }; + return { xpub, derivationPath, masterFingerprint: xfp, forMultiSig, forSingleSig }; }; export const updateInputsForSeedSigner = ({ serializedPSBT, signedSerializedPSBT }) => { diff --git a/src/hardware/specter/index.ts b/src/hardware/specter/index.ts new file mode 100644 index 000000000..999a77460 --- /dev/null +++ b/src/hardware/specter/index.ts @@ -0,0 +1,21 @@ +import { DerivationPurpose } from 'src/core/wallets/enums'; +import WalletUtilities from 'src/core/wallets/operations/utils'; + +export const getSpecterDetails = (qrData) => { + const xpub = qrData.slice(qrData.indexOf(']') + 1); + const masterFingerprint = qrData.slice(1, 9); + const derivationPath = qrData + .slice(qrData.indexOf('[') + 1, qrData.indexOf(']')) + .replace(masterFingerprint, 'm'); + const purpose = WalletUtilities.getSignerPurposeFromPath(derivationPath); + let forMultiSig: boolean; + let forSingleSig: boolean; + if (purpose && DerivationPurpose.BIP48.toString() === purpose) { + forMultiSig = true; + forSingleSig = false; + } else { + forMultiSig = false; + forSingleSig = true; + } + return { xpub, derivationPath, masterFingerprint, forMultiSig, forSingleSig }; +}; diff --git a/src/hardware/tapsigner/index.ts b/src/hardware/tapsigner/index.ts index 9b9e2ce41..f60e562a8 100644 --- a/src/hardware/tapsigner/index.ts +++ b/src/hardware/tapsigner/index.ts @@ -22,7 +22,7 @@ const getScriptSpecificDetails = async (card, cvc, isMultisig) => { const xfp = await card.get_xfp(cvc); const xpub = isMultisig ? multiSigXpub : singleSigXpub; const derivationPath = isMultisig ? multiSigPath : singleSigPath; - return { xpub, xfp: xfp.toString('hex'), derivationPath, xpubDetails }; + return { xpub, masterFingerprint: xfp.toString('hex'), derivationPath, xpubDetails }; }; export const getTapsignerDetails = async (card: CKTapCard, cvc: string, isMultisig: boolean) => { @@ -30,25 +30,22 @@ export const getTapsignerDetails = async (card: CKTapCard, cvc: string, isMultis const isLegit = await card.certificate_check(); if (isLegit) { if (status.path) { - const { xpub, xfp, derivationPath, xpubDetails } = await getScriptSpecificDetails( - card, - cvc, - isMultisig - ); + const { xpub, masterFingerprint, derivationPath, xpubDetails } = + await getScriptSpecificDetails(card, cvc, isMultisig); // reset to original path await card.set_derivation(status.path, cvc); - return { xpub, xfp, derivationPath, xpubDetails }; + return { xpub, masterFingerprint, derivationPath, xpubDetails }; } await card.setup(cvc); const newCard = await card.first_look(); - const { xpub, xfp, derivationPath, xpubDetails } = await getScriptSpecificDetails( + const { xpub, masterFingerprint, derivationPath, xpubDetails } = await getScriptSpecificDetails( newCard, cvc, isMultisig ); // reset to original path await card.set_derivation(status.path, cvc); - return { xpub, xfp, derivationPath, xpubDetails }; + return { xpub, masterFingerprint, derivationPath, xpubDetails }; } }; diff --git a/src/hardware/trezor/index.ts b/src/hardware/trezor/index.ts index a85f8c62d..5bea8814f 100644 --- a/src/hardware/trezor/index.ts +++ b/src/hardware/trezor/index.ts @@ -23,7 +23,7 @@ export const getTrezorDetails = (data, isMultisig) => { return { xpub, derivationPath, - xfp: WalletUtilities.getFingerprintFromExtendedKey(xpub, network), + masterFingerprint: WalletUtilities.getFingerprintFromExtendedKey(xpub, network), xpubDetails, }; } catch (_) { diff --git a/src/hooks/useConfigReocvery.tsx b/src/hooks/useConfigReocvery.tsx index dca2decd2..d441c9242 100644 --- a/src/hooks/useConfigReocvery.tsx +++ b/src/hooks/useConfigReocvery.tsx @@ -1,67 +1,71 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { ParsedVauleText, parseTextforVaultConfig } from 'src/core/utils'; import { generateSignerFromMetaData } from 'src/hardware'; import { SignerStorage, SignerType, VaultType } from 'src/core/wallets/enums'; import { useAppSelector } from 'src/store/hooks'; import { NewVaultInfo } from 'src/store/sagas/wallets'; import { useDispatch } from 'react-redux'; -import { addNewVault } from 'src/store/sagaActions/vaults'; +import { addNewVault, addSigningDevice } from 'src/store/sagaActions/vaults'; import { captureError } from 'src/services/sentry'; -import { setupKeeperApp } from 'src/store/sagaActions/storage'; import { VaultScheme } from 'src/core/wallets/interfaces/vault'; -import messaging from '@react-native-firebase/messaging'; -import { useNavigation } from '@react-navigation/native'; +import { CommonActions, useNavigation } from '@react-navigation/native'; import { Alert } from 'react-native'; -import { setRecoveryCreatedApp } from 'src/store/reducers/storage'; +import useToastMessage from './useToastMessage'; +import { resetRealyVaultState } from 'src/store/reducers/bhr'; const useConfigRecovery = () => { - const { appId } = useAppSelector((state) => state.storage); - const { relayVaultError, relayVaultUpdate } = useAppSelector((state) => state.bhr); + const { relayVaultError, relayVaultUpdate, realyVaultErrorMessage } = useAppSelector( + (state) => state.bhr + ); const [recoveryLoading, setRecoveryLoading] = useState(false); const [scheme, setScheme] = useState(); + const [vaultSignersList, setVaultSignersList] = useState([]); + const { showToast } = useToastMessage(); const [signersList, setSignersList] = useState([]); const navigation = useNavigation(); const dispatch = useDispatch(); - async function createNewApp() { - try { - const fcmToken = await messaging().getToken(); - dispatch(setRecoveryCreatedApp(true)); - dispatch(setupKeeperApp(fcmToken)); - } catch (error) { - dispatch(setRecoveryCreatedApp(true)); - dispatch(setupKeeperApp()); - } - } + + let recoveryError = { + failed: false, + message: '', + }; useEffect(() => { - if (appId && scheme) { + if (scheme && signersList.length > 1 && vaultSignersList.length > 1) { try { + dispatch(addSigningDevice(signersList)); const vaultInfo: NewVaultInfo = { vaultType: VaultType.DEFAULT, vaultScheme: scheme, - vaultSigners: signersList, + vaultSigners: vaultSignersList, vaultDetails: { - name: 'Vault', + name: 'Imported Vault', description: 'Secure your sats', }, }; dispatch(addNewVault({ newVaultInfo: vaultInfo })); + setTimeout(() => {}, 3000); } catch (err) { captureError(err); } setRecoveryLoading(false); } - }, [appId, signersList]); + }, [scheme, signersList]); useEffect(() => { if (relayVaultUpdate) { + const navigationState = { + index: 0, + routes: [{ name: 'Home' }], + }; + dispatch(resetRealyVaultState()); setRecoveryLoading(false); - navigation.replace('App'); + showToast('Vault Imported Successfully!'); + navigation.dispatch(CommonActions.reset(navigationState)); } if (relayVaultError) { setRecoveryLoading(false); - Alert.alert('Something went wrong!'); } }, [relayVaultUpdate, relayVaultError]); @@ -70,32 +74,32 @@ const useConfigRecovery = () => { try { const parsedText: ParsedVauleText = parseTextforVaultConfig(text); if (parsedText) { + const vaultSigners = []; const signers = []; parsedText.signersDetails.forEach((config) => { - const signer = generateSignerFromMetaData({ + const { signer, key } = generateSignerFromMetaData({ xpub: config.xpub, derivationPath: config.path, - xfp: config.masterFingerprint, - signerType: SignerType.OTHER_SD, + masterFingerprint: config.masterFingerprint, + signerType: SignerType.UNKOWN_SIGNER, storageType: SignerStorage.WARM, isMultisig: config.isMultisig, }); + vaultSigners.push(key); signers.push(signer); }); - setScheme(parsedText.scheme); setSignersList(signers); - if (!appId) { - createNewApp(); - } + setVaultSignersList(vaultSigners); + setScheme(parsedText.scheme); } } catch (err) { setRecoveryLoading(false); - console.log(err); - Alert.alert(`Something went wrong!`); + recoveryError.failed = true; + recoveryError.message = err; } }; - return { recoveryLoading, initateRecovery }; + return { recoveryLoading, recoveryError, initateRecovery }; }; export default useConfigRecovery; diff --git a/src/hooks/useKeys.ts b/src/hooks/useKeys.ts new file mode 100644 index 000000000..e6329f96e --- /dev/null +++ b/src/hooks/useKeys.ts @@ -0,0 +1,11 @@ +import { RealmSchema } from 'src/storage/realm/enum'; +import { VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { getJSONFromRealmObject } from 'src/storage/realm/utils'; +import { useQuery } from '@realm/react'; + +const useKeys = () => { + const keys: VaultSigner[] = useQuery(RealmSchema.VaultSigner).map(getJSONFromRealmObject); + return { keys }; +}; + +export default useKeys; diff --git a/src/hooks/useSignerFromKey.ts b/src/hooks/useSignerFromKey.ts new file mode 100644 index 000000000..8a7b58ca9 --- /dev/null +++ b/src/hooks/useSignerFromKey.ts @@ -0,0 +1,15 @@ +import { Signer, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { useQuery } from '@realm/react'; +import { RealmSchema } from 'src/storage/realm/enum'; +import { getJSONFromRealmObject } from 'src/storage/realm/utils'; + +const useSignerFromKey = (key: VaultSigner) => { + const signerQuery = useQuery(RealmSchema.Signer); + if (!key) return { signer: null }; + const signer: Signer = signerQuery + .filtered(`masterFingerprint == "${key.masterFingerprint}"`) + .map(getJSONFromRealmObject)[0]; + return { signer }; +}; + +export default useSignerFromKey; diff --git a/src/hooks/useSignerIntel.tsx b/src/hooks/useSignerIntel.tsx index fab5cb580..572e99aa9 100644 --- a/src/hooks/useSignerIntel.tsx +++ b/src/hooks/useSignerIntel.tsx @@ -1,132 +1,81 @@ -import { useEffect, useState } from 'react'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; -import { SignerType, XpubTypes } from 'src/core/wallets/enums'; - -import { useAppSelector } from 'src/store/hooks'; -import useVault from 'src/hooks/useVault'; -import { getSignerNameFromType, getSignerSigTypeInfo, isSignerAMF } from 'src/hardware'; -import idx from 'idx'; -import WalletUtilities from 'src/core/wallets/operations/utils'; -import config from 'src/core/config'; -import useSubscription from './useSubscription'; +import { SignerType } from 'src/core/wallets/enums'; +import { getSignerNameFromType, isSignerAMF } from 'src/hardware'; import { SubscriptionTier } from 'src/models/enums/SubscriptionTier'; +import useSignerMap from './useSignerMap'; +import { VaultScheme, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import usePlan from './usePlan'; -const getPrefillForSignerList = (scheme, vaultSigners) => { - let fills = []; - if (vaultSigners.length < scheme.n) { - fills = new Array(scheme.n - vaultSigners.length).fill(null); - } - return fills; -}; - -const signerLimitMatchesSubscriptionScheme = ({ vaultSigners, currentSignerLimit }) => - vaultSigners && vaultSigners.length !== currentSignerLimit; - -const areSignersSame = ({ activeVault, signersState }) => { - if (!activeVault) { +const areSignersSame = ({ existingKeys, vaultKeys }) => { + if (!existingKeys.length || !vaultKeys.length) { return false; } - const currentSignerIds = signersState.map((signer) => (signer ? signer.signerId : '')); - const activeSignerIds = activeVault.signers.map((signer) => signer.signerId); - return currentSignerIds.sort().join() === activeSignerIds.sort().join(); + const currentXfps = vaultKeys.map((signer) => (signer ? signer.xfp : '')); + const activeXfps = existingKeys.map((signer) => signer.xfp); + return currentXfps.sort().join() === activeXfps.sort().join(); }; -export const updateSignerForScheme = (signer: VaultSigner, schemeN) => { - const xPubTypeToSwitch = schemeN === 1 ? XpubTypes.P2WPKH : XpubTypes.P2WSH; - const completeSigner = - !!idx(signer, (_) => _.xpubDetails[XpubTypes.P2WPKH].xpub) && - !!idx(signer, (_) => _.xpubDetails[XpubTypes.P2WSH].xpub); - const shouldSwitchXpub = - completeSigner && signer.xpub !== signer.xpubDetails[xPubTypeToSwitch].xpub; - if (shouldSwitchXpub) { - const switchedXpub = signer.xpubDetails[xPubTypeToSwitch].xpub; - const switchedDerivation = signer.xpubDetails[xPubTypeToSwitch].derivationPath; - const switchedXpriv = signer.xpubDetails[xPubTypeToSwitch].xpriv; - const network = WalletUtilities.getNetworkByType(config.NETWORK_TYPE); - return { - ...signer, - xpub: switchedXpub, - derivationPath: switchedDerivation, - xpriv: switchedXpriv, - signerId: WalletUtilities.getFingerprintFromExtendedKey(switchedXpub, network), - }; - } - return signer; -}; - -const useSignerIntel = ({ scheme }) => { - const { activeVault } = useVault(); - const vaultSigners = useAppSelector((state) => state.vault.signers); - const [signersState, setSignersState] = useState(vaultSigners); - const { validSigners } = useSubscription(); - - useEffect(() => { - const fills = getPrefillForSignerList(scheme, vaultSigners); - setSignersState( - vaultSigners.map((signer) => updateSignerForScheme(signer, scheme.n)).concat(fills) - ); - }, [vaultSigners]); +const useSignerIntel = ({ + scheme, + vaultKeys, + selectedSigners, + existingKeys, +}: { + scheme: VaultScheme; + vaultKeys: VaultSigner[]; + selectedSigners; + existingKeys: VaultSigner[]; +}) => { + const { signerMap } = useSignerMap(); + const { plan } = usePlan(); + const isOnL1 = plan === SubscriptionTier.L1.toUpperCase(); + const isOnL3 = plan === SubscriptionTier.L3.toUpperCase(); const amfSigners = []; - const misMatchedSigners = []; - signersState.forEach((signer: VaultSigner) => { - if (signer) { - if (isSignerAMF(signer)) amfSigners.push(signer.type); - const { isSingleSig, isMultiSig } = getSignerSigTypeInfo(signer); - if ((scheme.n === 1 && !isSingleSig) || (scheme.n !== 1 && !isMultiSig)) { - misMatchedSigners.push(signer.masterFingerprint); - } - } - }); + for (let mfp of selectedSigners.keys()) { + const signer = signerMap[mfp]; + if (isSignerAMF(signer)) amfSigners.push(signer.type); + } let invalidIKS = false; let invalidSS = false; let invalidMessage = ''; - signersState.forEach((signer) => { - if (signer) { - if (signer.type === SignerType.INHERITANCEKEY) { - if (!validSigners.includes(signer.type)) { - invalidIKS = true; - invalidMessage = `${getSignerNameFromType(signer.type)} is not allowed in ${ - SubscriptionTier.L2 - } Please upgrade your plan or remove them`; - } else if (vaultSigners.length < 5) { - invalidIKS = true; - invalidMessage = `You need at least 5 signers to use ${getSignerNameFromType( - signer.type - )}. Please add more signers`; - } - } - if (signer.type === SignerType.POLICY_SERVER) { - if (!validSigners.includes(signer.type)) { + vaultKeys.forEach((key) => { + if (key) { + const isIKS = signerMap[key.masterFingerprint].type === SignerType.INHERITANCEKEY; + const isSS = signerMap[key.masterFingerprint].type === SignerType.POLICY_SERVER; + const signerName = getSignerNameFromType(signerMap[key.masterFingerprint].type); + if (isSS) { + if (isOnL1) { invalidSS = true; - invalidMessage = `${getSignerNameFromType(signer.type)} is not allowed in ${ - SubscriptionTier.L1 - } Please upgrade your plan or remove them`; - } else if (vaultSigners.length < 3) { + invalidMessage = `${signerName} is allowed from ${SubscriptionTier.L2} Please upgrade your plan or remove them`; + } else if (scheme.m < 2 || scheme.n < 3) { invalidSS = true; - invalidMessage = `You need at least 3 signers to use ${getSignerNameFromType( - signer.type - )}. Please add more signers`; + invalidMessage = `You need at least 3 signers and 2 required signers to use ${signerName}. Please add more signers`; + } + } + if (isIKS) { + if (!isOnL3) { + invalidIKS = true; + invalidMessage = `${signerName} is allowed from ${SubscriptionTier.L3} Please upgrade your plan or remove them`; + } else if (scheme.m < 3 || scheme.n < 5) { + invalidIKS = true; + invalidMessage = `You need at least 5 signers and 3 required signers to use ${signerName}. Please add more signers`; } } } }); const areSignersValid = - signersState.every((signer) => !signer) || - signerLimitMatchesSubscriptionScheme({ vaultSigners, currentSignerLimit: scheme.n }) || - areSignersSame({ activeVault, signersState }) || - !!misMatchedSigners.length || + vaultKeys.every((signer) => !signer) || + scheme.n !== vaultKeys.length || + areSignersSame({ existingKeys, vaultKeys }) || invalidIKS || invalidSS; return { - signersState, areSignersValid, amfSigners, - misMatchedSigners, invalidSS, invalidIKS, invalidMessage, diff --git a/src/hooks/useSignerMap.ts b/src/hooks/useSignerMap.ts new file mode 100644 index 000000000..312b3a3f4 --- /dev/null +++ b/src/hooks/useSignerMap.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@realm/react'; +import { RealmSchema } from 'src/storage/realm/enum'; +import { Signer } from 'src/core/wallets/interfaces/vault'; +import { getJSONFromRealmObject } from 'src/storage/realm/utils'; + +const useSignerMap = () => { + const signerMap = {}; + const signerQuery = useQuery(RealmSchema.Signer); + signerQuery.forEach( + (signer) => (signerMap[(signer as Signer).masterFingerprint] = getJSONFromRealmObject(signer)) + ); + return { signerMap }; +}; + +export default useSignerMap; diff --git a/src/hooks/useSigners.tsx b/src/hooks/useSigners.tsx new file mode 100644 index 000000000..fea743397 --- /dev/null +++ b/src/hooks/useSigners.tsx @@ -0,0 +1,23 @@ +import { Signer, Vault } from 'src/core/wallets/interfaces/vault'; +import { useQuery } from '@realm/react'; +import { RealmSchema } from 'src/storage/realm/enum'; +import { getJSONFromRealmObject } from 'src/storage/realm/utils'; + +const useSigners = (vaultId = ''): { vaultSigners: Signer[]; signers: Signer[] } => { + const vaults = useQuery(RealmSchema.Vault); + const signers = useQuery(RealmSchema.Signer); + let currentVault = null; + const vaultSigners = []; + if (vaultId) { + currentVault = vaults.filtered(`id == "${vaultId}"`)[0]; + const vaultKeys = (currentVault as Vault).signers; + vaultKeys.forEach((key) => { + const signer = signers.filtered(`masterFingerprint == "${key.masterFingerprint}"`)[0]; + vaultSigners.push(signer.toJSON()); + }); + return { vaultSigners, signers: signers.map(getJSONFromRealmObject) }; + } + return { vaultSigners, signers: signers.map(getJSONFromRealmObject) }; +}; + +export default useSigners; diff --git a/src/hooks/useSmallDevices.tsx b/src/hooks/useSmallDevices.tsx new file mode 100644 index 000000000..513292460 --- /dev/null +++ b/src/hooks/useSmallDevices.tsx @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; +import DeviceInfo from 'react-native-device-info'; + +const useIsSmallDevices = () => { + const [isSmallDevice, setIsSmallDevice] = useState(false); + + useEffect(() => { + const checkDevice = async () => { + const model = await DeviceInfo.getModel(); + setIsSmallDevice( + model.includes('mini') || + model.includes('Mini') || + model.includes('SE') || + model.includes('iPhone 7') || + model.includes('iPhone 6') + ); + }; + + checkDevice(); + }, []); + + return isSmallDevice; +}; + +export default useIsSmallDevices; diff --git a/src/hooks/useSubscription.ts b/src/hooks/useSubscription.ts deleted file mode 100644 index 30f47dfed..000000000 --- a/src/hooks/useSubscription.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useQuery } from '@realm/react'; -import { SignerType } from 'src/core/wallets/enums'; -import { SubscriptionTier } from 'src/models/enums/SubscriptionTier'; -import { KeeperApp } from 'src/models/interfaces/KeeperApp'; -import { RealmSchema } from 'src/storage/realm/enum'; -import { getJSONFromRealmObject } from 'src/storage/realm/utils'; - -const useSubscription = () => { - const keeper: KeeperApp = useQuery(RealmSchema.KeeperApp).map(getJSONFromRealmObject)[0]; - const L1Signers = [ - SignerType.TAPSIGNER, - SignerType.KEEPER, - SignerType.TREZOR, - SignerType.LEDGER, - SignerType.COLDCARD, - SignerType.PASSPORT, - SignerType.JADE, - SignerType.KEYSTONE, - SignerType.MOBILE_KEY, - SignerType.SEED_WORDS, - SignerType.SEEDSIGNER, - SignerType.BITBOX02, - SignerType.OTHER_SD, - ]; - const L2Signers = [...L1Signers, SignerType.POLICY_SERVER]; - const L3Signers = [...L2Signers, SignerType.INHERITANCEKEY]; - - const plan = keeper.subscription.name.toUpperCase(); - const validSigners = - plan === SubscriptionTier.L1.toUpperCase() - ? L1Signers - : plan === SubscriptionTier.L2.toUpperCase() - ? L2Signers - : L3Signers; - - return { validSigners }; -}; - -export default useSubscription; diff --git a/src/hooks/useUnkownSigners.tsx b/src/hooks/useUnkownSigners.tsx new file mode 100644 index 000000000..96facdf47 --- /dev/null +++ b/src/hooks/useUnkownSigners.tsx @@ -0,0 +1,53 @@ +import { useQuery } from '@realm/react'; +import { useDispatch } from 'react-redux'; +import { SignerType } from 'src/core/wallets/enums'; +import { Signer } from 'src/core/wallets/interfaces/vault'; +import { getSignerNameFromType } from 'src/hardware'; +import { InheritanceKeyInfo, SignerPolicy } from 'src/services/interfaces'; +import { RealmSchema } from 'src/storage/realm/enum'; +import { getJSONFromRealmObject } from 'src/storage/realm/utils'; +import { updateSignerDetails } from 'src/store/sagaActions/wallets'; + +const useUnkownSigners = () => { + const signers: Signer[] = useQuery(RealmSchema.Signer).map(getJSONFromRealmObject); + const unknowSigners = signers.filter((signer) => signer.type === SignerType.UNKOWN_SIGNER); + const dispatch = useDispatch(); + + const mapUnknownSigner = ({ + masterFingerprint, + type, + signerPolicy, + inheritanceKeyInfo, + }: { + masterFingerprint: string; + type: SignerType; + signerPolicy?: SignerPolicy; + inheritanceKeyInfo?: InheritanceKeyInfo; + }): boolean | void => { + try { + const signer = unknowSigners.find((signer) => signer.masterFingerprint === masterFingerprint); + if (signer) { + dispatch(updateSignerDetails(signer, 'type', type)); + dispatch(updateSignerDetails(signer, 'signerName', getSignerNameFromType(type))); + + if (signerPolicy) { + dispatch(updateSignerDetails(signer, 'signerPolicy', signerPolicy)); + } + + if (inheritanceKeyInfo) { + dispatch(updateSignerDetails(signer, 'inheritanceKeyInfo', inheritanceKeyInfo)); + } + return true; + } else { + return false; + } + } catch (error) { + console.error('Error mapping unknown signer to signer:', error); + throw new Error('Error mapping unknown signer to signer'); + } + }; + + return { unknowSigners, mapUnknownSigner }; +}; + +export default useUnkownSigners; diff --git a/src/hooks/useVault.ts b/src/hooks/useVault.ts index 25fd66c77..4c9ab54e3 100644 --- a/src/hooks/useVault.ts +++ b/src/hooks/useVault.ts @@ -1,23 +1,66 @@ import { RealmSchema } from 'src/storage/realm/enum'; import { Vault } from 'src/core/wallets/interfaces/vault'; import { getJSONFromRealmObject } from 'src/storage/realm/utils'; -import { VaultType } from 'src/core/wallets/enums'; -import useCollaborativeWallet from './useCollaborativeWallet'; import { useQuery } from '@realm/react'; +import { VisibilityType } from 'src/core/wallets/enums'; -const useVault = (collaborativeWalletId?: string) => { - const { collaborativeWallet } = useCollaborativeWallet(collaborativeWalletId); - if (collaborativeWallet) { - return { activeVault: collaborativeWallet }; +type Params = + | { + collaborativeWalletId?: string; + vaultId: string; + includeArchived?: boolean; + getFirst?: boolean; + getHiddenWallets?: boolean; + } + | { + collaborativeWalletId: string; + vaultId?: string; + includeArchived?: boolean; + getFirst?: boolean; + getHiddenWallets?: boolean; + } + | { + collaborativeWalletId?: string; + vaultId?: string; + includeArchived?: boolean; + getFirst?: boolean; + getHiddenWallets?: boolean; + }; + +const useVault = ({ + vaultId = '', + includeArchived = true, + getFirst = false, + getHiddenWallets = true, +}: Params) => { + let allVaults: Vault[] = useQuery(RealmSchema.Vault); + allVaults = includeArchived + ? allVaults.map(getJSONFromRealmObject) + : allVaults.filtered('archived != true').map(getJSONFromRealmObject); + + const allNonHiddenNonArchivedVaults = allVaults.filter( + (vault) => vault.presentationData.visibility === VisibilityType.DEFAULT + ); + if (!vaultId) { + if (getHiddenWallets) { + return { allVaults, activeVault: getFirst ? allVaults[0] : null }; + } else { + return { + allVaults: allNonHiddenNonArchivedVaults, + activeVault: getFirst ? allVaults[0] : null, + }; + } } - const activeVault: Vault = - useQuery(RealmSchema.Vault) - .map(getJSONFromRealmObject) - .filter((vault: Vault) => !vault.archived && vault.type !== VaultType.COLLABORATIVE)[0] || - null; + const activeVault: Vault = vaultId + ? allVaults.filter((v) => v.id === vaultId)[0] + : allVaults.filter((v) => !v.archived)[0]; - return { activeVault }; + if (!getHiddenWallets) { + return { activeVault, allVaults: allNonHiddenNonArchivedVaults }; + } else { + return { activeVault, allVaults }; + } }; export default useVault; diff --git a/src/models/interfaces/KeeperApp.ts b/src/models/interfaces/KeeperApp.ts index 17669083b..62d843a41 100644 --- a/src/models/interfaces/KeeperApp.ts +++ b/src/models/interfaces/KeeperApp.ts @@ -19,4 +19,5 @@ export interface KeeperApp { networkType: NetworkType; backup: AppBackup; subscription: SubScription; + enableAnalytics: boolean; } diff --git a/src/navigation/Navigator.tsx b/src/navigation/Navigator.tsx index d34b9db29..30ddd27a1 100644 --- a/src/navigation/Navigator.tsx +++ b/src/navigation/Navigator.tsx @@ -1,5 +1,8 @@ import { DefaultTheme, NavigationContainer } from '@react-navigation/native'; import React, { useContext, useRef } from 'react'; +import { AppStackParams } from './types'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { routingInstrumentation } from 'src/services/sentry'; import AddAmountScreen from 'src/screens/Recieve/AddAmountScreen'; import AddDescription from 'src/screens/Vault/AddDescription'; import AddSendAmount from 'src/screens/Send/AddSendAmount'; @@ -12,7 +15,7 @@ import ChangeLanguage from 'src/screens/AppSettings/ChangeLanguage'; import ChoosePlan from 'src/screens/ChoosePlanScreen/ChoosePlan'; import ChoosePolicyNew from 'src/screens/Vault/ChoosePolicyNew'; import CreatePin from 'src/screens/LoginScreen/CreatePin'; -import EditWalletSettings from 'src/screens/WalletDetails/EditWalletDetails'; +import EditWalletDetails from 'src/screens/WalletDetails/EditWalletDetails'; import EnterSeedScreen from 'src/screens/Recovery/EnterSeedScreen'; import EnterWalletDetailScreen from 'src/screens/EnterWalletDetailScreen/EnterWalletDetailScreen'; import ExportSeedScreen from 'src/screens/ExportSeedScreen/ExportSeedScreen'; @@ -49,21 +52,15 @@ import TorSettings from 'src/screens/AppSettings/TorSettings'; import ManageWallets from 'src/screens/AppSettings/ManageWallets'; import TransactionDetails from 'src/screens/ViewTransactions/TransactionDetails'; import VaultDetails from 'src/screens/Vault/VaultDetails'; -import VaultRecovery from 'src/screens/VaultRecovery/VaultRecovery'; import VaultSettings from 'src/screens/Vault/VaultSettings'; import AllTransactions from 'src/screens/Vault/AllTransactions'; import WalletBackHistoryScreen from 'src/screens/BackupWallet/WalletBackHistoryScreen'; import WalletDetails from 'src/screens/WalletDetails/WalletDetails'; import WalletSettings from 'src/screens/WalletDetails/WalletSettings'; -import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { routingInstrumentation } from 'src/services/sentry'; import Colors from 'src/theme/Colors'; import NodeSettings from 'src/screens/AppSettings/Node/NodeSettings'; -import HomeScreen from 'src/screens/HomeScreen/HomeScreen'; -import OtherRecoveryMethods from 'src/screens/Recovery/OtherRecoveryMethods'; import ConnectChannel from 'src/screens/Channel/ConnectChannel'; import RegisterWithChannel from 'src/screens/QRScreens/RegisterWithChannel'; -import VaultConfigurationRecovery from 'src/screens/VaultRecovery/VaultConfigurationRecovery'; import SignWithChannel from 'src/screens/QRScreens/SignWithChannel'; import SigningDeviceConfigRecovery from 'src/screens/Recovery/SigningDeviceConfigRecovery'; import ScanQRFileRecovery from 'src/screens/Recovery/ScanQRFileRecovery'; @@ -91,11 +88,19 @@ import InputSeedWordSigner from 'src/screens/SigningDevices/InputSeedWordSigner' import SetupOtherSDScreen from 'src/screens/SigningDevices/SetupOtherSDScreen'; import SetupCollaborativeWallet from 'src/screens/SigningDevices/SetupCollaborativeWallet'; import SetupSigningServer from 'src/screens/SigningDevices/SetupSigningServer'; -import SigningDeviceListRecovery from 'src/screens/Recovery/SigninDeviceListRecovery'; import UnlockTapsigner from 'src/screens/SigningDevices/UnlockTapsigner'; import UTXOSelection from 'src/screens/Send/UTXOSelection'; import VaultSetup from 'src/screens/Vault/VaultSetup'; import NFCScanner from 'src/screens/Vault/NFCScanner'; +import PrivacyAndDisplay from 'src/screens/AppSettings/PrivacyAndDisplay'; +import NetworkSetting from 'src/screens/AppSettings/NetworkSetting'; +import VaultCreationOptions from 'src/screens/Vault/VaultCreationOptions'; +import VaultConfigurationCreation from 'src/screens/Vault/VaultConfigurationRecreation'; +import AddWallet from 'src/screens/AddWalletScreen/AddWallet'; +import AddSigner from 'src/screens/AddSigner/AddSigner'; +import HomeScreen from 'src/screens/Home/HomeScreen'; +import ManageSigners from 'src/screens/SigningDevices/ManageSigners'; +import AppBackupSettings from 'src/screens/AppSettings/AppBackupSettings'; const defaultTheme = { ...DefaultTheme, @@ -125,17 +130,6 @@ function LoginStack() { component={NewKeeperApp} /> - - - - - - - {/* Cold Card */} {/* Tap Signer */} @@ -155,7 +149,7 @@ function LoginStack() { } function AppStack() { - const Stack = createNativeStackNavigator(); + const Stack = createNativeStackNavigator(); return ( @@ -182,7 +176,7 @@ function AppStack() { - + @@ -200,6 +194,7 @@ function AppStack() { + @@ -221,6 +216,8 @@ function AppStack() { + + @@ -234,7 +231,11 @@ function AppStack() { + + + + + + + ); diff --git a/src/navigation/themes.js b/src/navigation/themes.js index 5198ff2d3..9a6819f0f 100644 --- a/src/navigation/themes.js +++ b/src/navigation/themes.js @@ -53,6 +53,7 @@ export const customTheme = extendTheme({ primaryGreen: Colors.GenericViridian, primaryBackground: Colors.LightYellow, primaryGreenBackground: Colors.pantoneGreen, + pantoneGreenLight: Colors.pantoneGreenLight, mainBackground: Colors.LightWhite, modalGreenBackground: Colors.pantoneGreen, modalGreenContent: Colors.White, @@ -91,6 +92,7 @@ export const customTheme = extendTheme({ TorLable: Colors.Menthol, divider: Colors.GrayX11, errorRed: Colors.CarmineRed, + SlateGreen: Colors.SlateGreen, textWallet: Colors.MediumJungleGreen, indicator: Colors.OutrageousOrange, addTransactionText: Colors.PineTree, @@ -108,11 +110,38 @@ export const customTheme = extendTheme({ forestGreen: Colors.ForestGreen, pantoneGreen: Colors.pantoneGreen, seashellWhite: Colors.Seashell, + Champagne: Colors.Champagne, + RussetBrown: Colors.RussetBrown, + GreenishGrey: Colors.GreenishGrey, + Ivory: Colors.Ivory, + RussetBrownLight: Colors.RussetBrownLight, + brownColor: Colors.brownColor, + learMoreTextcolor: Colors.learMoreTextcolor, + Linen: Colors.Linen, + OffWhite: Colors.OffWhite, + SageGreen: Colors.SageGreen, + SlateGrey: Colors.SlateGrey, + LightKhaki: Colors.LightKhaki, + Eggshell: Colors.Eggshell, + Teal: Colors.Teal, + SmokeGreen: Colors.SmokeGreen, + DeepOlive: Colors.DeepOlive, + PaleKhaki: Colors.PaleKhaki, + Warmbeige: Colors.Warmbeige, + LightBrown: Colors.LightBrown, + PearlWhite: Colors.PearlWhite, + PaleTurquoise: Colors.PaleTurquoise, + PaleIvory: Colors.PaleIvory, + DarkSage: Colors.DarkSage, + Smoke: Colors.Smoke, + ForestGreenDark: Colors.ForestGreenDark, + deepTeal: Colors.deepTeal, }, dark: { primaryGreen: Colors.GenericViridian, primaryBackground: Colors.LightYellowDark, primaryGreenBackground: Colors.LightYellowDark, + pantoneGreenLight: Colors.pantoneGreenLight, mainBackground: Colors.LightWhite, modalGreenBackground: Colors.LightYellowDark, modalGreenContent: Colors.White, @@ -148,6 +177,7 @@ export const customTheme = extendTheme({ copyBackground: Colors.LightGray, sendCardHeading: Colors.BlueGreen, Glass: Colors.Glass, + SlateGreen: Colors.SlateGreen, TorLable: Colors.Menthol, divider: Colors.GrayX11, errorRed: Colors.CarmineRed, @@ -168,6 +198,32 @@ export const customTheme = extendTheme({ forestGreen: Colors.ForestGreen, pantoneGreen: Colors.pantoneGreenDark, seashellWhite: Colors.SeashellDark, + RussetBrown: Colors.RussetBrown, + GreenishGrey: Colors.GreenishGrey, + RussetBrownLight: Colors.RussetBrownLight, + brownColor: Colors.brownColor, + learMoreTextcolor: Colors.learMoreTextcolor, + Linen: Colors.Linen, + OffWhite: Colors.OffWhite, + SageGreen: Colors.SageGreen, + SlateGrey: Colors.SlateGrey, + LightKhaki: Colors.LightKhaki, + Eggshell: Colors.Eggshell, + Teal: Colors.Teal, + SmokeGreen: Colors.SmokeGreen, + DeepOlive: Colors.DeepOlive, + PaleKhaki: Colors.PaleKhaki, + Warmbeige: Colors.Warmbeige, + LightBrown: Colors.LightBrown, + PearlWhite: Colors.PearlWhite, + PaleTurquoise: Colors.PaleTurquoise, + PaleIvory: Colors.PaleIvory, + DarkSage: Colors.DarkSage, + Smoke: Colors.Smoke, + ForestGreenDark: Colors.ForestGreenDark, + deepTeal: Colors.deepTeal, + Champagne: Colors.Champagne, + Ivory: Colors.Ivory, }, }, config: { diff --git a/src/navigation/types.ts b/src/navigation/types.ts new file mode 100644 index 000000000..780f6160f --- /dev/null +++ b/src/navigation/types.ts @@ -0,0 +1,120 @@ +import { Vault, VaultScheme, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Wallet } from 'src/core/wallets/interfaces/wallet'; + +export type AppStackParams = { + Home: undefined; + Login: undefined; + SigningDeviceList: undefined; + AddTapsigner: undefined; + SignWithTapsigner: undefined; + AddColdCard: undefined; + AppSettings: undefined; + AppVersionHistory: undefined; + TorSettings: undefined; + ManageWallets: undefined; + SetupInheritance: undefined; + PreviewPDF: undefined; + InheritanceStatus: undefined; + InheritanceSetupInfo: undefined; + IKSAddEmailPhone: undefined; + EnterOTPEmailConfirmation: undefined; + Send: undefined; + UTXOLabeling: undefined; + Receive: undefined; + ChangeLanguage: undefined; + ChoosePlan: undefined; + EnterWalletDetail: undefined; + UpdateWalletDetails: undefined; + EditWalletDetails: { wallet: Wallet | Vault }; + WalletDetailsSettings: undefined; + ImportDescriptorScreen: undefined; + CollaborativeWalletSettings: undefined; + AddAmount: undefined; + ExportSeed: undefined; + ImportWallet: undefined; + ImportWalletDetails: undefined; + AddDetailsFinal: undefined; + AddSendAmount: undefined; + SendConfirmation: undefined; + WalletDetails: { autoRefresh?: boolean; walletId: string }; + VaultDetails: { + vaultId: string; + vaultTransferSuccessful: boolean; + autoRefresh: boolean; + collaborativeWalletId: string; + }; + UTXOManagement: + | { + data: Wallet | Vault; + routeName: string; + accountType?: string; + vaultId: string; + collaborativeWalletId?: string; + } + | { + data: Wallet | Vault; + routeName: string; + accountType: string; + vaultId?: string; + collaborativeWalletId?: string; + }; + WalletSettings: undefined; + BackupWallet: undefined; + SigningDeviceDetails: undefined; + WalletBackHistory: undefined; + SignTransactionScreen: undefined; + AddSigningDevice: undefined; + SetupSigningServer: undefined; + SetupSeedWordSigner: undefined; + InputSeedWordSigner: undefined; + ArchivedVault: undefined; + VaultSettings: undefined; + SignWithColdCard: undefined; + ChoosePolicyNew: undefined; + SigningServerSettings: undefined; + SigningServer: undefined; + AddDescription: undefined; + AllTransactions: undefined; + TransactionDetails: undefined; + TimelockScreen: undefined; + SignerAdvanceSettings: undefined; + ScanQR: undefined; + ShowQR: undefined; + RegisterWithQR: undefined; + SignWithQR: undefined; + NodeSettings: undefined; + PrivacyAndDisplay: undefined; + NetworkSetting: undefined; + ConnectChannel: undefined; + RegisterWithChannel: undefined; + SetupOtherSDScreen: undefined; + SignWithChannel: undefined; + PoolSelection: undefined; + BroadcastPremix: undefined; + WhirlpoolConfiguration: undefined; + CosignerDetails: undefined; + GenerateVaultDescriptor: undefined; + SetupCollaborativeWallet: undefined; + EnterSeedScreen: undefined; + UnlockTapsigner: undefined; + UTXOSelection: { sender: Wallet | Vault; amount: string; address: string }; + VaultCreationOptions: undefined; + VaultConfigurationCreation: undefined; + ScanQRFileRecovery: undefined; + VaultSetup: { isRecreation: Boolean; scheme: VaultScheme; vaultId?: string }; + SigningDeviceConfigRecovery: undefined; + MixProgress: undefined; + AssignSignerType: undefined; + NFCScanner: undefined; + AddWallet: undefined; + AddSigner: undefined; + ManageSigners: { + vaultId: string; + vaultKeys: VaultSigner[]; + }; + AppBackupSettings: undefined; +}; + +// Usage: +// type ScreenProps = NativeStackScreenProps; +// const ScreenName = ({ navigation, route }: ScreenProps) => { diff --git a/src/screens/AddSigner/AddSigner.tsx b/src/screens/AddSigner/AddSigner.tsx new file mode 100644 index 000000000..2d8bea8ac --- /dev/null +++ b/src/screens/AddSigner/AddSigner.tsx @@ -0,0 +1,53 @@ +import { Box, ScrollView, useColorMode } from 'native-base'; +import { useContext, useState } from 'react'; +import KeeperHeader from 'src/components/KeeperHeader'; +import ScreenWrapper from 'src/components/ScreenWrapper'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import WalletActiveIcon from 'src/assets/images/walleTabFilled.svg'; +import WalletDark from 'src/assets/images/walletDark.svg'; +import SignerCard from 'src/screens/AddSigner/SignerCard'; +import { StyleSheet } from 'react-native'; +import AddCard from 'src/components/AddCard'; + +function AddSigner({ navigation }) { + const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { signer, common } = translations; + + const [selectedCard, selectCard] = useState(''); + + const onCardSelect = (name: string) => { + if (name === selectedCard) selectCard(''); + else selectCard(name); + }; + + return ( + + + + + : } + selectedCard={selectedCard} + onCardSelect={onCardSelect} + /> + + + + + + ); +} + +const styles = StyleSheet.create({ + signerContainer: { + flexDirection: 'row', + gap: 5, + flexWrap: 'wrap', + marginTop: 5, + }, +}); + +export default AddSigner; diff --git a/src/screens/AddSigner/SignerCard.tsx b/src/screens/AddSigner/SignerCard.tsx new file mode 100644 index 000000000..1db989c3f --- /dev/null +++ b/src/screens/AddSigner/SignerCard.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { Box, Pressable, useColorMode } from 'native-base'; +import { StyleSheet } from 'react-native'; +import { windowWidth } from 'src/constants/responsive'; +import Text from 'src/components/KeeperText'; +import Checked from 'src/assets/images/check.svg'; + +type SignerCardProps = { + name: string; + description?: string; + icon: Element; + isSelected: boolean; + onCardSelect?: (selected: any) => void; + showSelection?: boolean; + colorVarient?: string; + disabled?: boolean; + isFullText?: boolean; + showDot?: boolean; +}; + +function SignerCard({ + name, + description = '', + icon, + isSelected, + onCardSelect, + showSelection = true, + colorVarient = 'brown', + disabled = false, + isFullText = false, + showDot = false, +}: SignerCardProps) { + const { colorMode } = useColorMode(); + const backgroundColor = + colorVarient === 'brown' ? `${colorMode}.RussetBrown` : `${colorMode}.pantoneGreen`; + + return ( + { + onCardSelect(isSelected); + }} + > + {showSelection && + (isSelected ? ( + + ) : ( + + ))} + + + {icon} + {showDot ? : null} + + + {name} + + + {description} + + + + ); +} + +const styles = StyleSheet.create({ + walletContainer: { + width: windowWidth / 3 - windowWidth * 0.05, + padding: 10, + height: 125, + alignItems: 'flex-start', + borderRadius: 10, + borderWidth: 0.5, + backgroundColor: '#FDF7F0', + margin: 3, + }, + walletName: { + fontSize: 12, + }, + walletDescription: { + fontSize: 11, + }, + circle: { + width: 20, + height: 20, + borderRadius: 20 / 2, + alignSelf: 'flex-end', + borderWidth: 1, + }, + detailContainer: { + gap: 2, + marginTop: 15, + }, + iconWrapper: { + width: 34, + height: 34, + borderRadius: 30, + justifyContent: 'center', + alignItems: 'center', + }, + redDot: { + width: 10, + height: 10, + borderRadius: 10 / 2, + backgroundColor: 'red', + position: 'absolute', + top: 0, + right: 0, + borderWidth: 1, + borderColor: 'white', + }, +}); + +export default SignerCard; diff --git a/src/screens/AddWalletScreen/AddWallet.tsx b/src/screens/AddWalletScreen/AddWallet.tsx new file mode 100644 index 000000000..072b653d1 --- /dev/null +++ b/src/screens/AddWalletScreen/AddWallet.tsx @@ -0,0 +1,99 @@ +import { Box, ScrollView, useColorMode } from 'native-base'; +import React, { useContext, useState } from 'react'; +import KeeperHeader from 'src/components/KeeperHeader'; +import ScreenWrapper from 'src/components/ScreenWrapper'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import WalletActiveIcon from 'src/assets/images/walleTabFilled.svg'; +import WalletGreenIcon from 'src/assets/images/wallet_green.svg'; +import AdvancedGreenIcon from 'src/assets/images/advanced_green.svg'; +import AdvancedIcon from 'src/assets/images/advanced.svg'; +import ImportGreenIcon from 'src/assets/images/import_green.svg'; +import ImportIcon from 'src/assets/images/import.svg'; +import WalletCard from 'src/components/WalletCard'; +import { StyleSheet } from 'react-native'; +import Wallets from './Wallets'; +import AdvancedWallets from './AdvancedWallets'; +import ImportWallets from './ImportWallets'; + +function AddWallet({ navigation }) { + const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { wallet } = translations; + + const [selectedCard, selectCard] = useState(1); + + const onCardSelect = (id: number) => { + selectCard(id); + }; + + //TODO: add learn more modal + return ( + + + + + } + selectedIcon={} + selectedCard={selectedCard} + onCardSelect={onCardSelect} + arrowStyles={{ alignSelf: 'flex-end', marginRight: 10 }} + /> + } + selectedIcon={} + selectedCard={selectedCard} + onCardSelect={onCardSelect} + arrowStyles={{ alignSelf: 'center' }} + /> + } + selectedIcon={} + selectedCard={selectedCard} + onCardSelect={onCardSelect} + arrowStyles={{ marginLeft: 10 }} + /> + + {selectedCard === 1 && } + {selectedCard === 2 && } + {selectedCard === 3 && } + + + ); +} + +const styles = StyleSheet.create({ + container: { + gap: 25, + marginTop: 20, + }, + walletType: { + justifyContent: 'space-between', + gap: 10, + }, + note: { + position: 'absolute', + bottom: 40, + width: '90%', + alignSelf: 'center', + }, +}); + +export default AddWallet; diff --git a/src/screens/AddWalletScreen/AdvancedWallets.tsx b/src/screens/AddWalletScreen/AdvancedWallets.tsx new file mode 100644 index 000000000..68d0e3a7a --- /dev/null +++ b/src/screens/AddWalletScreen/AdvancedWallets.tsx @@ -0,0 +1,60 @@ +import { Box, useColorMode } from 'native-base'; +import React from 'react'; +import OptionCard from 'src/components/OptionCard'; +import TimeLockIcon from 'src/assets/images/calendar_disabled.svg'; +import MultiSigIcon from 'src/assets/images/degrading_multisig_disabled.svg'; +import VaultGreenIcon from 'src/assets/images/vault_green.svg'; +import { CommonActions } from '@react-navigation/native'; +import CardPill from 'src/components/CardPill'; + +function AdvancedWallets({ navigation }) { + const { colorMode } = useColorMode(); + const navigateToVaultSetup = (scheme?) => { + navigation.dispatch(CommonActions.navigate({ name: 'VaultSetup', params: { scheme } })); + }; + + return ( + + } + titleColor={`${colorMode}.DarkSage`} + descriptionColor={`${colorMode}.Smoke`} + CardPill={ + + } + callback={() => {}} + disabled + /> + } + titleColor={`${colorMode}.DarkSage`} + descriptionColor={`${colorMode}.Smoke`} + CardPill={ + + } + callback={() => {}} + disabled + /> + } + callback={() => navigateToVaultSetup()} + /> + + ); +} + +export default AdvancedWallets; diff --git a/src/screens/AddWalletScreen/ImportWallets.tsx b/src/screens/AddWalletScreen/ImportWallets.tsx new file mode 100644 index 000000000..fb17171d1 --- /dev/null +++ b/src/screens/AddWalletScreen/ImportWallets.tsx @@ -0,0 +1,33 @@ +import { Box } from 'native-base'; +import React from 'react'; +import OptionCard from 'src/components/OptionCard'; +import WatchOnlyIcon from 'src/assets/images/watch_only.svg'; +import ConfigurationIcon from 'src/assets/images/file.svg'; +import SignerIcon from 'src/assets/images/signer.svg'; + +function ImportWallets({ navigation }) { + return ( + + } + callback={() => navigation.navigate('ImportWallet')} + /> + } + callback={() => navigation.navigate('VaultConfigurationCreation')} + /> + } + callback={() => navigation.navigate('SigningDeviceConfigRecovery')} + /> + + ); +} + +export default ImportWallets; diff --git a/src/screens/AddWalletScreen/Wallets.tsx b/src/screens/AddWalletScreen/Wallets.tsx new file mode 100644 index 000000000..1c9f1624d --- /dev/null +++ b/src/screens/AddWalletScreen/Wallets.tsx @@ -0,0 +1,95 @@ +import { Box } from 'native-base'; +import React from 'react'; +import OptionCard from 'src/components/OptionCard'; +import WalletGreenIcon from 'src/assets/images/wallet_green.svg'; +import VaultGreenIcon from 'src/assets/images/vault_green.svg'; +import CollaborativeIcon from 'src/assets/images/collaborative_vault.svg'; +import useCollaborativeWallet from 'src/hooks/useCollaborativeWallet'; +import useWallets from 'src/hooks/useWallets'; +import { NewWalletInfo } from 'src/store/sagas/wallets'; +import { WalletType } from 'src/core/wallets/enums'; +import { defaultTransferPolicyThreshold } from 'src/store/sagas/storage'; +import { addNewWallets } from 'src/store/sagaActions/wallets'; +import { useDispatch } from 'react-redux'; +import { CommonActions } from '@react-navigation/native'; +import { VaultScheme } from 'src/core/wallets/interfaces/vault'; + +const addNewDefaultWallet = (walletsCount, dispatch) => { + const newWallet: NewWalletInfo = { + walletType: WalletType.DEFAULT, + walletDetails: { + name: `Wallet ${walletsCount + 1} `, + description: '', + transferPolicy: { + id: uuidv4(), + threshold: defaultTransferPolicyThreshold, + }, + }, + }; + dispatch(addNewWallets([newWallet])); +}; + +function Wallets({ navigation }) { + const dispatch = useDispatch(); + const { wallets } = useWallets({ getAll: true }); + const { collaborativeWallets } = useCollaborativeWallet(); + const collaborativeWalletsCount = collaborativeWallets?.length || 0; + const walletsCount = wallets.length; + + const navigateToVaultSetup = (scheme: VaultScheme) => { + navigation.dispatch(CommonActions.navigate({ name: 'VaultSetup', params: { scheme } })); + }; + + const navigateToWalletCreation = () => { + navigation.navigate('EnterWalletDetail', { + name: `Wallet ${wallets.length + 1}`, + description: '', + type: WalletType.DEFAULT, + }); + }; + const handleCollaaborativeWalletCreation = () => { + if (collaborativeWalletsCount < walletsCount) { + navigation.navigate('SetupCollaborativeWallet', { + coSigner: wallets[collaborativeWalletsCount], + walletId: wallets[collaborativeWalletsCount].id, + collaborativeWalletsCount, + }); + } else { + addNewDefaultWallet(wallets.length, dispatch); + } + }; + + return ( + + } + callback={navigateToWalletCreation} + /> + } + callback={() => navigateToVaultSetup({ m: 2, n: 3 })} + /> + } + callback={() => navigateToVaultSetup({ m: 3, n: 5 })} + /> + } + callback={handleCollaaborativeWalletCreation} + /> + + ); +} + +export default Wallets; +function uuidv4(): string { + throw new Error('Function not implemented.'); +} diff --git a/src/screens/AppSettings/AppBackupSettings.tsx b/src/screens/AppSettings/AppBackupSettings.tsx new file mode 100644 index 000000000..94a05cb7d --- /dev/null +++ b/src/screens/AppSettings/AppBackupSettings.tsx @@ -0,0 +1,79 @@ +import React, { useContext, useState } from 'react'; +import { ScrollView, useColorMode } from 'native-base'; +import { StyleSheet } from 'react-native'; +import { useQuery } from '@realm/react'; +import { CommonActions, useNavigation } from '@react-navigation/native'; +import KeeperHeader from 'src/components/KeeperHeader'; +import OptionCard from 'src/components/OptionCard'; +import KeeperModal from 'src/components/KeeperModal'; +import ScreenWrapper from 'src/components/ScreenWrapper'; +import PasscodeVerifyModal from 'src/components/Modal/PasscodeVerify'; +import { wp } from 'src/constants/responsive'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import { RealmSchema } from 'src/storage/realm/enum'; +import { getJSONFromRealmObject } from 'src/storage/realm/utils'; + +function AppBackupSettings() { + const { colorMode } = useColorMode(); + const navigation = useNavigation(); + const { primaryMnemonic } = useQuery(RealmSchema.KeeperApp).map(getJSONFromRealmObject)[0]; + + const [confirmPassVisible, setConfirmPassVisible] = useState(false); + + const { translations } = useContext(LocalizationContext); + const { settings } = translations; + + return ( + + + + { + setConfirmPassVisible(true); + }} + /> + + setConfirmPassVisible(false)} + title={'Confirm Passcode'} + subTitleWidth={wp(240)} + subTitle={'To backup app recovery key'} + modalBackground={`${colorMode}.modalWhiteBackground`} + subTitleColor={`${colorMode}.secondaryText`} + textColor={`${colorMode}.primaryText`} + Content={() => ( + { + setConfirmPassVisible(false); + }} + onSuccess={() => { + navigation.dispatch( + CommonActions.navigate('ExportSeed', { + seed: primaryMnemonic, + next: false, + viewRecoveryKeys: true, + }) + ); + }} + /> + )} + /> + + ); +} + +const styles = StyleSheet.create({ + optionsListContainer: { + alignItems: 'center', + marginTop: 20, + }, +}); +export default AppBackupSettings; diff --git a/src/screens/AppSettings/AppSettings.tsx b/src/screens/AppSettings/AppSettings.tsx index 3a551f7e2..4a1a65f47 100644 --- a/src/screens/AppSettings/AppSettings.tsx +++ b/src/screens/AppSettings/AppSettings.tsx @@ -1,301 +1,159 @@ -import React, { useContext, useEffect, useState, useMemo } from 'react'; +import React, { useContext, useState } from 'react'; import { StyleSheet } from 'react-native'; import Text from 'src/components/KeeperText'; import { Box, Pressable, ScrollView, useColorMode } from 'native-base'; import { hp, wp } from 'src/constants/responsive'; -import BackupIcon from 'src/assets/images/backup.svg'; +import AppBackupIcon from 'src/assets/images/app_backup.svg'; +import SettingsIcon from 'src/assets/images/settings_white.svg'; +import FaqIcon from 'src/assets/images/faq.svg'; +import WalletIcon from 'src/assets/images/daily_wallet.svg'; import Twitter from 'src/assets/images/Twitter.svg'; import Telegram from 'src/assets/images/Telegram.svg'; -import CurrencyTypeSwitch from 'src/components/Switch/CurrencyTypeSwitch'; import KeeperHeader from 'src/components/KeeperHeader'; -import LinkIcon from 'src/assets/images/link.svg'; import { LocalizationContext } from 'src/context/Localization/LocContext'; -import LoginMethod from 'src/models/enums/LoginMethod'; -import ThemeMode from 'src/models/enums/ThemeMode'; -import ReactNativeBiometrics from 'react-native-biometrics'; import ScreenWrapper from 'src/components/ScreenWrapper'; import openLink from 'src/utils/OpenLink'; -import { RealmSchema } from 'src/storage/realm/enum'; -import { BackupAction, BackupHistory } from 'src/models/enums/BHR'; -import moment from 'moment'; -import ToastErrorIcon from 'src/assets/images/toast_error.svg'; -import { getBackupDuration } from 'src/utils/utilities'; -import useToastMessage from 'src/hooks/useToastMessage'; -import { setThemeMode } from 'src/store/reducers/settings'; -import { changeLoginMethod } from 'src/store/sagaActions/login'; -import { useAppDispatch, useAppSelector } from 'src/store/hooks'; -import { useQuery } from '@realm/react'; import OptionCard from 'src/components/OptionCard'; import Switch from 'src/components/Switch/Switch'; import { KEEPER_KNOWLEDGEBASE, KEEPER_WEBSITE_BASE_URL } from 'src/core/config'; - -const RNBiometrics = new ReactNativeBiometrics(); +import ActionCard from 'src/components/ActionCard'; +import NavButton from 'src/components/NavButton'; +import CurrencyTypeSwitch from 'src/components/Switch/CurrencyTypeSwitch'; +import PasscodeVerifyModal from 'src/components/Modal/PasscodeVerify'; +import { CommonActions } from '@react-navigation/native'; +import { RealmSchema } from 'src/storage/realm/enum'; +import { useQuery } from '@realm/react'; +import { getJSONFromRealmObject } from 'src/storage/realm/utils'; +import KeeperModal from 'src/components/KeeperModal'; +import CircleIconWrapper from 'src/components/CircleIconWrapper'; +import LoginMethod from 'src/models/enums/LoginMethod'; +import { useAppSelector } from 'src/store/hooks'; function AppSettings({ navigation }) { - const { colorMode, toggleColorMode } = useColorMode(); - const { backupMethod } = useAppSelector((state) => state.bhr); - const data: BackupHistory = useQuery(RealmSchema.BackupHistory); - const { loginMethod }: { loginMethod: LoginMethod } = useAppSelector((state) => state.settings); - - const dispatch = useAppDispatch(); - const { showToast } = useToastMessage(); - - const [sensorType, setSensorType] = useState('Biometrics'); - const { translations, formatString } = useContext(LocalizationContext); - const { common } = translations; - const { settings } = translations; - const backupWalletStrings = translations.BackupWallet; - - const backupHistory = useMemo(() => data.sorted('date', true), [data]); - const backupSubTitle = useMemo(() => { - if (backupMethod === null) { - return 'Backup your Recovery Phrase'; - } - if (backupHistory[0].title === BackupAction.SEED_BACKUP_CONFIRMED) { - const lastBackupDate = moment(backupHistory[0].date); - const today = moment(moment().unix()); - const remainingDays = getBackupDuration() - lastBackupDate.diff(today, 'seconds'); - if (remainingDays > 0) { - return `Recovery Health check due in ${Math.floor(remainingDays / 86400)} days`; - } - return 'Recovery Health check is due'; - } - return backupWalletStrings[backupHistory[0].title]; - }, [backupHistory, backupMethod]); - - useEffect(() => { - if (colorMode === 'dark') { - dispatch(setThemeMode(ThemeMode.DARK)); - } else { - dispatch(setThemeMode(ThemeMode.LIGHT)); - } - }, [colorMode]); - - useEffect(() => { - init(); - }, []); - - const init = async () => { - try { - const { available, biometryType } = await RNBiometrics.isSensorAvailable(); - if (available) { - const type = - biometryType === 'TouchID' - ? 'Touch ID' - : biometryType === 'FaceID' - ? 'Face ID' - : biometryType; - setSensorType(type); - } - } catch (error) { - console.log(error); - } - }; + // const { colorMode } = useColorMode(); + const { satsEnabled }: { loginMethod: LoginMethod; satsEnabled: boolean } = useAppSelector( + (state) => state.settings + ); - const onChangeLoginMethod = async () => { - try { - const { available } = await RNBiometrics.isSensorAvailable(); - if (available) { - if (loginMethod === LoginMethod.PIN) { - const { keysExist } = await RNBiometrics.biometricKeysExist(); - if (keysExist) { - await RNBiometrics.createKeys(); - } - const { publicKey } = await RNBiometrics.createKeys(); - const { success } = await RNBiometrics.simplePrompt({ - promptMessage: 'Confirm your identity', - }); - if (success) { - dispatch(changeLoginMethod(LoginMethod.BIOMETRIC, publicKey)); - } - } else { - dispatch(changeLoginMethod(LoginMethod.PIN)); - } - } else { - showToast( - 'Biometrics not enabled.\nPlease go to setting and enable it', - - ); - } - } catch (error) { - console.log(error); - } - }; + const { colorMode, toggleColorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { common, settings } = translations; + const data = useQuery(RealmSchema.BackupHistory); + const { primaryMnemonic } = useQuery(RealmSchema.KeeperApp).map(getJSONFromRealmObject)[0]; + const [confirmPassVisible, setConfirmPassVisible] = useState(false); const changeThemeMode = () => { toggleColorMode(); }; - function Option({ title, subTitle, onPress, Icon }) { - return ( - - {Icon && ( - - {/* { Notification indicator } */} - {backupMethod === null && ( - - )} - - - )} - - - {title} - - - {subTitle} - - - - ); - } + const actionCardData = [ + { + cardName: settings.appBackup, + icon: , + callback: () => { + if (data.length === 0) { + setConfirmPassVisible(true); + } else { + navigation.navigate('WalletBackHistory'); + } + }, + }, + { + cardName: settings.ManageWallets, + icon: , + callback: () => navigation.navigate('ManageWallets'), + }, + { + cardName: common.FAQs, + icon: , + callback: () => openLink(`${KEEPER_KNOWLEDGEBASE}knowledge-base/`), + }, + ]; + //TODO: add learn more modal return ( - - + //To-Do-Learn-More + icon={ + } + /> } + rightComponent={} /> - - - openLink('https://telegram.me/bitcoinkeeper')}> - - - - - - {settings.keeperTelegram} - - - - - - - - - openLink('https://twitter.com/bitcoinKeeper_')} - testID="btn_keeperTwitter" - > - - - - - - {settings.keeperTwitter} - - - - - - - - + + } + heading="Keeper Telegram" + link="https://telegram.me/bitcoinkeeper" + /> + } + heading="Keeper X" + link="https://twitter.com/bitcoinKeeper_" + /> - openLink(`${KEEPER_KNOWLEDGEBASE}knowledge-base/`)} - testID="btn_FAQ" - > - - {common.FAQs} - - - | openLink(`${KEEPER_KNOWLEDGEBASE}terms-of-service/`)} testID="btn_termsCondition" @@ -323,6 +181,33 @@ function AppSettings({ navigation }) { + setConfirmPassVisible(false)} + title={'Confirm Passcode'} + subTitleWidth={wp(240)} + subTitle={'To backup app recovery key'} + modalBackground={`${colorMode}.modalWhiteBackground`} + subTitleColor={`${colorMode}.secondaryText`} + textColor={`${colorMode}.primaryText`} + Content={() => ( + { + setConfirmPassVisible(false); + }} + onSuccess={() => { + navigation.dispatch( + CommonActions.navigate('ExportSeed', { + seed: primaryMnemonic, + next: true, + }) + ); + }} + /> + )} + /> ); } @@ -348,7 +233,7 @@ const styles = StyleSheet.create({ borderRadius: 10, borderWidth: 0.3, position: 'absolute', - right: wp(-2), + right: 18, zIndex: 999, }, appBackupTitle: { @@ -398,14 +283,25 @@ const styles = StyleSheet.create({ }, bottomLinkWrapper: { flexDirection: 'row', - justifyContent: 'space-evenly', + justifyContent: 'center', alignItems: 'center', - borderRadius: 8, + gap: 15, }, bottomLinkText: { fontSize: 13, fontWeight: '400', letterSpacing: 0.79, }, + actionContainer: { + flexDirection: 'row', + gap: 5, + marginBottom: 20, + }, + bottomNav: { + flexDirection: 'row', + gap: 10, + justifyContent: 'space-around', + marginBottom: 10, + }, }); export default AppSettings; diff --git a/src/screens/AppSettings/ChangeLanguage.tsx b/src/screens/AppSettings/ChangeLanguage.tsx index 91ac5541c..30e89ae11 100644 --- a/src/screens/AppSettings/ChangeLanguage.tsx +++ b/src/screens/AppSettings/ChangeLanguage.tsx @@ -2,7 +2,6 @@ import React, { useState, useContext } from 'react'; import { View, TouchableOpacity, StyleSheet } from 'react-native'; import { Box, ScrollView, useColorMode } from 'native-base'; import Text from 'src/components/KeeperText'; -import CountryCard from 'src/components/SettingComponent/CountryCard'; import CountrySwitchCard from 'src/components/SettingComponent/CountrySwitchCard'; import { setCurrencyCode, setLanguage, setSatsEnabled } from 'src/store/reducers/settings'; import { widthPercentageToDP as wp } from 'react-native-responsive-screen'; @@ -16,18 +15,21 @@ import { useAppSelector, useAppDispatch } from 'src/store/hooks'; import { LocalizationContext } from 'src/context/Localization/LocContext'; import Fonts from 'src/constants/Fonts'; import FiatCurrencies from 'src/constants/FiatCurrencies'; +import LoginMethod from 'src/models/enums/LoginMethod'; +import Switch from 'src/components/Switch/Switch'; +import OptionCard from 'src/components/OptionCard'; const styles = StyleSheet.create({ btn: { flexDirection: 'row', + width: '90%', height: wp('13%'), position: 'relative', marginHorizontal: 12, + borderRadius: 10, }, textCurrency: { fontSize: 18, - color: '#00836A', - fontWeight: '700', }, icArrow: { marginLeft: wp('3%'), @@ -65,8 +67,6 @@ const styles = StyleSheet.create({ menuWrapper: { height: wp('13%'), width: wp('15%'), - borderTopLeftRadius: 10, - borderBottomLeftRadius: 10, justifyContent: 'center', alignItems: 'center', }, @@ -78,10 +78,9 @@ const styles = StyleSheet.create({ borderBottomRightRadius: 10, }, emptyView: { - height: '65%', - marginTop: 10, + height: '55%', + alignSelf: 'center', width: 2, - backgroundColor: '#D8A572', }, textValueWrapper: { flex: 1, @@ -154,11 +153,19 @@ const styles = StyleSheet.create({ countryCodeText: { textTransform: 'uppercase', }, + contentContainer: { + flex: 1, + marginTop: 20, + paddingHorizontal: 20, + }, }); function ChangeLanguage() { const { appLanguage, setAppLanguage } = useContext(LocalizationContext); - const { currencyCode, language, satsEnabled } = useAppSelector((state) => state.settings); + const { currencyCode, language } = useAppSelector((state) => state.settings); + const { satsEnabled }: { loginMethod: LoginMethod; satsEnabled: boolean } = useAppSelector( + (state) => state.settings + ); const dispatch = useAppDispatch(); const [currencyList] = useState(FiatCurrencies); @@ -173,35 +180,39 @@ function ChangeLanguage() { ); const [isDisabled, setIsDisabled] = useState(true); - const changeThemeMode = () => { - dispatch(setSatsEnabled(!satsEnabled)); - }; - const { translations } = useContext(LocalizationContext); const { settings } = translations; + const changeSatsMode = () => { + dispatch(setSatsEnabled(!satsEnabled)); + }; + function Menu({ label, value, onPress, arrow }) { return ( - - - {label} - - - - - {value} - - - - - + + + + + {label} + + + + + + {value} + + + + + + @@ -210,19 +221,26 @@ function ChangeLanguage() { return ( - - - + + changeThemeMode()} - value={satsEnabled} + description={settings.satsModeSubTitle} + callback={() => changeSatsMode()} + Icon={ + changeSatsMode()} + testID="switch_darkmode" + /> + } /> - - - - {title} - - - {subtitle} - - + // TODO: Drag and rearrange wallet functionality + // + // + // + // + + + } + /> + + + {title} + + + {subtitle} + + + + + {colorMode === 'light' ? : } {SatsToBtc(balance)} + + + {isHidden ? : } + + {isHidden ? 'Unhide' : 'Hide'} + + + - - - - - {btnTitle} - - - + // ); } @@ -97,6 +162,12 @@ function ManageWallets() { const walletsWithoutWhirlpool: Wallet[] = useQuery(RealmSchema.Wallet).filtered( `type != "${WalletType.PRE_MIX}" && type != "${WalletType.POST_MIX}" && type != "${WalletType.BAD_BANK}"` ); + + const { allVaults } = useVault({ includeArchived: false }); + const allWallets: (Wallet | Vault)[] = [...walletsWithoutWhirlpool, ...allVaults].filter( + (item) => item !== null + ); + const visibleWallets = walletsWithoutWhirlpool.filter( (wallet) => wallet.presentationData.visibility === VisibilityType.DEFAULT ); @@ -107,7 +178,6 @@ function ManageWallets() { const [confirmPassVisible, setConfirmPassVisible] = useState(false); const navigation = useNavigation(); - const route = useRoute(); const dispatch = useDispatch(); const [selectedWallet, setSelectedWallet] = useState(null); @@ -129,52 +199,46 @@ function ManageWallets() { }; const onProceed = () => { - unhideWallet(selectedWallet); + updateWalletVisibility(selectedWallet, false); }; - const hideWallet = (wallet: Wallet, checkBalance = true) => { - if (wallet.specs.balances.confirmed > 0 && checkBalance) { + const updateWalletVisibility = (wallet: Wallet | Vault, hide: boolean, checkBalance = true) => { + const { id, entityKind, specs } = wallet; + const isWallet = entityKind === EntityKind.WALLET; + + if (hide && checkBalance && specs.balances.confirmed > 0) { setShowBalanceAlert(true); setSelectedWallet(wallet); return; } - try { - dbManager.updateObjectById(RealmSchema.Wallet, wallet.id, { - presentationData: { - name: wallet.presentationData.name, - description: wallet.presentationData.description, - visibility: VisibilityType.HIDDEN, - shell: wallet.presentationData.shell, - }, - }); - } catch (error) { - captureError(error); - } - }; - const unhideWallet = (wallet: Wallet) => { try { - dbManager.updateObjectById(RealmSchema.Wallet, wallet.id, { + const visibilityType = hide ? VisibilityType.HIDDEN : VisibilityType.DEFAULT; + console.log({ visibilityType }); + const schema = isWallet ? RealmSchema.Wallet : RealmSchema.Vault; + + dbManager.updateObjectById(schema, id, { presentationData: { name: wallet.presentationData.name, description: wallet.presentationData.description, - visibility: VisibilityType.DEFAULT, + visibility: visibilityType, shell: wallet.presentationData.shell, }, }); } catch (error) { console.log(error); + captureError(error); } }; function BalanceAlertModalContent() { return ( - + { - hideWallet(selectedWallet, false); + updateWalletVisibility(selectedWallet, true, false); setShowBalanceAlert(false); }} activeOpacity={0.5} @@ -194,10 +258,7 @@ function ManageWallets() { }} > - + Move Funds @@ -211,45 +272,43 @@ function ManageWallets() { return ( - - ( - hideWallet(item)} - /> - )} - showsVerticalScrollIndicator={false} - keyExtractor={(item) => item.id} + } /> - ( { - setSelectedWallet(item); - setConfirmPassVisible(true); - }} + isHidden={item.presentationData.visibility === VisibilityType.HIDDEN} + onBtnPress={ + item.presentationData.visibility === VisibilityType.HIDDEN + ? () => updateWalletVisibility(item, false) + : () => updateWalletVisibility(item, true) + } /> )} showsVerticalScrollIndicator={false} keyExtractor={(item) => item.id} /> + + {/* TODO: showAll/hideAll wallet functionality + + setShowAll(true)} style={styles.footer}> + + + + + Show all + + */} + { @@ -269,9 +328,9 @@ function ManageWallets() { setConfirmPassVisible(false)} - title={'Confirm Passcode'} + title="Confirm Passcode" subTitleWidth={wp(240)} - subTitle={''} + subTitle="" modalBackground={`${colorMode}.modalWhiteBackground`} subTitleColor={`${colorMode}.secondaryText`} textColor={`${colorMode}.primaryText`} diff --git a/src/screens/AppSettings/NetworkSetting.tsx b/src/screens/AppSettings/NetworkSetting.tsx new file mode 100644 index 000000000..15baf876d --- /dev/null +++ b/src/screens/AppSettings/NetworkSetting.tsx @@ -0,0 +1,40 @@ +import React, { useContext } from 'react'; +import { Box, useColorMode } from 'native-base'; +import ScreenWrapper from 'src/components/ScreenWrapper'; +import KeeperHeader from 'src/components/KeeperHeader'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import OptionCard from 'src/components/OptionCard'; +import { StyleSheet } from 'react-native'; +import { hp } from 'src/constants/responsive'; + +function NetworkSetting({ navigation }) { + const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { settings } = translations; + return ( + + + + navigation.navigate('TorSettings')} + /> + navigation.navigate('NodeSettings')} + /> + + + ) +} +const styles = StyleSheet.create({ + wrapper: { + marginTop: hp(35) + } +}) +export default NetworkSetting \ No newline at end of file diff --git a/src/screens/AppSettings/PrivacyAndDisplay.tsx b/src/screens/AppSettings/PrivacyAndDisplay.tsx new file mode 100644 index 000000000..39904e40f --- /dev/null +++ b/src/screens/AppSettings/PrivacyAndDisplay.tsx @@ -0,0 +1,178 @@ +import * as Sentry from '@sentry/react-native'; +import React, { useContext, useEffect, useState } from 'react'; +import { Box, ScrollView, useColorMode } from 'native-base'; +import ReactNativeBiometrics from 'react-native-biometrics'; +import ScreenWrapper from 'src/components/ScreenWrapper'; +import KeeperHeader from 'src/components/KeeperHeader'; +import OptionCard from 'src/components/OptionCard'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import Switch from 'src/components/Switch/Switch'; +import { useAppDispatch, useAppSelector } from 'src/store/hooks'; +import LoginMethod from 'src/models/enums/LoginMethod'; +import { changeLoginMethod } from 'src/store/sagaActions/login'; +import ToastErrorIcon from 'src/assets/images/toast_error.svg'; +import useToastMessage from 'src/hooks/useToastMessage'; +import { setThemeMode } from 'src/store/reducers/settings'; +import ThemeMode from 'src/models/enums/ThemeMode'; +import { StyleSheet } from 'react-native'; +import { hp } from 'src/constants/responsive'; +import Note from 'src/components/Note/Note'; +import { sentryConfig } from 'src/services/sentry'; +import useAsync from 'src/hooks/useAsync'; +import { KeeperApp } from 'src/models/interfaces/KeeperApp'; +import { useQuery } from '@realm/react'; +import { RealmSchema } from 'src/storage/realm/enum'; +import dbManager from 'src/storage/realm/dbManager'; +import { getJSONFromRealmObject } from 'src/storage/realm/utils'; + +const RNBiometrics = new ReactNativeBiometrics(); + +function PrivacyAndDisplay() { + const { colorMode } = useColorMode(); + const dispatch = useAppDispatch(); + const { showToast } = useToastMessage(); + + const [sensorType, setSensorType] = useState('Biometrics'); + const { translations, formatString } = useContext(LocalizationContext); + const { settings, common } = translations; + const { loginMethod }: { loginMethod: LoginMethod } = useAppSelector((state) => state.settings); + const { inProgress, start } = useAsync(); + const app: KeeperApp = useQuery(RealmSchema.KeeperApp).map(getJSONFromRealmObject)[0]; + const analyticsEnabled = app.enableAnalytics; + + const toggleSentryReports = async () => { + if (inProgress) { + return; + } + if (!analyticsEnabled) { + await start(() => Sentry.init(sentryConfig)); + } else { + await start(() => Sentry.init({ ...sentryConfig, enabled: false })); + } + dbManager.updateObjectById(RealmSchema.KeeperApp, app.id, { + enableAnalytics: !analyticsEnabled, + }); + }; + + useEffect(() => { + init(); + }, []); + + useEffect(() => { + if (colorMode === 'dark') { + dispatch(setThemeMode(ThemeMode.DARK)); + } else { + dispatch(setThemeMode(ThemeMode.LIGHT)); + } + }, [colorMode]); + + const init = async () => { + try { + const { available, biometryType } = await RNBiometrics.isSensorAvailable(); + if (available) { + const type = + biometryType === 'TouchID' + ? 'Touch ID' + : biometryType === 'FaceID' + ? 'Face ID' + : biometryType; + setSensorType(type); + } + } catch (error) { + console.log(error); + } + }; + + const onChangeLoginMethod = async () => { + try { + const { available } = await RNBiometrics.isSensorAvailable(); + if (available) { + if (loginMethod === LoginMethod.PIN) { + const { keysExist } = await RNBiometrics.biometricKeysExist(); + if (keysExist) { + await RNBiometrics.createKeys(); + } + const { publicKey } = await RNBiometrics.createKeys(); + const { success } = await RNBiometrics.simplePrompt({ + promptMessage: 'Confirm your identity', + }); + if (success) { + dispatch(changeLoginMethod(LoginMethod.BIOMETRIC, publicKey)); + } + } else { + dispatch(changeLoginMethod(LoginMethod.PIN)); + } + } else { + showToast( + 'Biometrics not enabled.\nPlease go to setting and enable it', + + ); + } + } catch (error) { + console.log(error); + } + }; + + return ( + + + + + + onChangeLoginMethod()} + Icon={ + onChangeLoginMethod()} + value={loginMethod === LoginMethod.BIOMETRIC} + testID="switch_biometrics" + /> + } + /> + await toggleSentryReports()} + value={app.enableAnalytics} + testID="switch_darkmode" + /> + } + /> + + {/* + TODO: missing functionality + */} + {/* navigation.navigate('NodeSettings')} + /> */} + + + + + + + ); +} +const styles = StyleSheet.create({ + wrapper: { + marginTop: hp(35), + gap: 50, + }, + note: { + position: 'absolute', + bottom: 50, + width: '95%', + alignSelf: 'center', + }, +}); +export default PrivacyAndDisplay; diff --git a/src/screens/AppSettings/TorModalMap.tsx b/src/screens/AppSettings/TorModalMap.tsx index d50b0dffc..141bed32a 100644 --- a/src/screens/AppSettings/TorModalMap.tsx +++ b/src/screens/AppSettings/TorModalMap.tsx @@ -31,8 +31,7 @@ function TorConnectionFailed() { - Could not established connection with Whirlpool client over in-app Tor. Try again later or - use other options + This can be due to network or other conditions. @@ -54,7 +53,7 @@ function TorModalMap({ visible, close }) { visible={visible && torStatus === TorStatus.CONNECTING} close={close} title="Connecting to Tor" - subTitle="Network calls and some functions may work slower when enabled" + subTitle="Network calls and some function may work slower when enabled" textColor="light.primaryText" subTitleColor="light.secondaryText" Content={TorConnectionContent} @@ -63,7 +62,7 @@ function TorModalMap({ visible, close }) { visible={visible && torStatus === TorStatus.ERROR} close={close} title="Connection Error" - subTitle="This can be due to the network or other conditions " + subTitle="There was an error when connecting via Tor. You could continue without connecting to Tor or try after sometime." subTitleColor="light.secondaryText" buttonText="Close" buttonTextColor="light.white" diff --git a/src/screens/BackupWallet/BackupWallet.tsx b/src/screens/BackupWallet/BackupWallet.tsx index 68d990811..b454c85fc 100644 --- a/src/screens/BackupWallet/BackupWallet.tsx +++ b/src/screens/BackupWallet/BackupWallet.tsx @@ -32,7 +32,6 @@ function BackupWallet() { const navigation = useNavigation(); const { primaryMnemonic } = useQuery(RealmSchema.KeeperApp).map(getJSONFromRealmObject)[0]; - return backupMethod !== null ? ( ) : ( @@ -102,7 +101,7 @@ function BackupWallet() { close={() => setConfirmPassVisible(false)} title={'Confirm Passcode'} subTitleWidth={wp(240)} - subTitle={'To backup app recovery phrase'} + subTitle={'To backup app recovery key'} modalBackground={`${colorMode}.modalWhiteBackground`} subTitleColor={`${colorMode}.secondaryText`} textColor={`${colorMode}.primaryText`} diff --git a/src/screens/BackupWallet/WalletBackHistoryScreen.tsx b/src/screens/BackupWallet/WalletBackHistoryScreen.tsx index c087bb6dd..db0a53e94 100644 --- a/src/screens/BackupWallet/WalletBackHistoryScreen.tsx +++ b/src/screens/BackupWallet/WalletBackHistoryScreen.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { Box, useColorMode } from 'native-base'; import BackupHealthCheckList from 'src/components/Backup/BackupHealthCheckList'; @@ -9,18 +9,32 @@ import { } from 'react-native-responsive-screen'; import KeeperHeader from 'src/components/KeeperHeader'; import ScreenWrapper from 'src/components/ScreenWrapper'; +import KeeperModal from 'src/components/KeeperModal'; function WalletBackHistoryScreen() { const { colorMode } = useColorMode(); const { translations } = useContext(LocalizationContext); + const [isLearnMore, setIsLearnMore] = useState(false); const { BackupWallet } = translations; return ( - + { + setIsLearnMore(true); + }} + /> + { + setIsLearnMore(false); + }} + /> ); } diff --git a/src/screens/Channel/ConnectChannel.tsx b/src/screens/Channel/ConnectChannel.tsx index b9a2a3aae..82a1035ac 100644 --- a/src/screens/Channel/ConnectChannel.tsx +++ b/src/screens/Channel/ConnectChannel.tsx @@ -32,13 +32,13 @@ import config from 'src/core/config'; import { getTrezorDetails } from 'src/hardware/trezor'; import { getLedgerDetailsFromChannel } from 'src/hardware/ledger'; import { healthCheckSigner } from 'src/store/sagaActions/bhr'; -import { checkSigningDevice } from '../Vault/AddSigningDevice'; import MockWrapper from 'src/screens/Vault/MockWrapper'; import { InteracationMode } from '../Vault/HardwareModalMap'; import { setSigningDevices } from 'src/store/reducers/bhr'; import Text from 'src/components/KeeperText'; import crypto from 'crypto'; import { createDecipheriv } from 'src/core/utils'; +import useUnkownSigners from 'src/hooks/useUnkownSigners'; const ScanAndInstruct = ({ onBarCodeRead, mode }) => { const { colorMode } = useColorMode(); @@ -83,6 +83,7 @@ function ConnectChannel() { signer, mode, isMultisig, + addSignerFlow = false, } = route.params as any; const [channel] = useState(io(config.CHANNEL_URL)); @@ -90,6 +91,7 @@ function ConnectChannel() { const { translations } = useContext(LocalizationContext); const { common } = translations; + const { mapUnknownSigner } = useUnkownSigners(); const dispatch = useDispatch(); const navigation = useNavigation(); @@ -107,14 +109,14 @@ function ConnectChannel() { channel.on(BITBOX_SETUP, async (data) => { try { const decrypted = createDecipheriv(data, decryptionKey.current); - const { xpub, derivationPath, xfp, xpubDetails } = getBitbox02Details( + const { xpub, derivationPath, masterFingerprint, xpubDetails } = getBitbox02Details( decrypted, isMultisig ); - const bitbox02 = generateSignerFromMetaData({ + const { signer: bitbox02 } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, isMultisig, signerType: SignerType.BITBOX02, storageType: SignerStorage.COLD, @@ -127,16 +129,14 @@ function ConnectChannel() { CommonActions.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }) ); } else { - dispatch(addSigningDevice(bitbox02)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); + dispatch(addSigningDevice([bitbox02])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); } showToast(`${bitbox02.signerName} added successfully`, ); - const exsists = await checkSigningDevice(bitbox02.signerId); - if (exsists) - showToast('Warning: Vault with this signer already exisits', ); } catch (error) { if (error instanceof HWError) { showToast(error.message, , 3000); @@ -148,11 +148,14 @@ function ConnectChannel() { channel.on(TREZOR_SETUP, async (data) => { try { const decrypted = createDecipheriv(data, decryptionKey.current); - const { xpub, derivationPath, xfp, xpubDetails } = getTrezorDetails(decrypted, isMultisig); - const trezor = generateSignerFromMetaData({ + const { xpub, derivationPath, masterFingerprint, xpubDetails } = getTrezorDetails( + decrypted, + isMultisig + ); + const { signer: trezor } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, isMultisig, signerType: SignerType.TREZOR, storageType: SignerStorage.COLD, @@ -164,15 +167,13 @@ function ConnectChannel() { CommonActions.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }) ); } else { - dispatch(addSigningDevice(trezor)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); + dispatch(addSigningDevice([trezor])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); } showToast(`${trezor.signerName} added successfully`, ); - const exsists = await checkSigningDevice(trezor.signerId); - if (exsists) - showToast('Warning: Vault with this signer already exisits', ); } catch (error) { if (error instanceof HWError) { showToast(error.message, , 3000); @@ -184,14 +185,12 @@ function ConnectChannel() { channel.on(LEDGER_SETUP, async (data) => { try { const decrypted = createDecipheriv(data, decryptionKey.current); - const { xpub, derivationPath, xfp, xpubDetails } = getLedgerDetailsFromChannel( - decrypted, - isMultisig - ); - const ledger = generateSignerFromMetaData({ + const { xpub, derivationPath, masterFingerprint, xpubDetails } = + getLedgerDetailsFromChannel(decrypted, isMultisig); + const { signer: ledger } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, isMultisig, signerType: SignerType.LEDGER, storageType: SignerStorage.COLD, @@ -203,16 +202,14 @@ function ConnectChannel() { CommonActions.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }) ); } else { - dispatch(addSigningDevice(ledger)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); + dispatch(addSigningDevice([ledger])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); } showToast(`${ledger.signerName} added successfully`, ); - const exsists = await checkSigningDevice(ledger.signerId); - if (exsists) - showToast('Warning: Vault with this signer already exisits', ); } catch (error) { if (error instanceof HWError) { showToast(error.message, , 3000); @@ -222,65 +219,74 @@ function ConnectChannel() { } }); - channel.on(LEDGER_HEALTHCHECK, async (data) => { + const handleVerification = async (data, deviceType) => { + const handleSuccess = () => { + dispatch(healthCheckSigner([signer])); + navigation.dispatch(CommonActions.goBack()); + showToast(`${signer.signerName} verified successfully`, ); + }; + + const handleFailure = () => { + navigation.dispatch(CommonActions.goBack()); + showToast(`${signer.signerName} verification failed`, ); + }; + try { const decrypted = createDecipheriv(data, decryptionKey.current); - const { xpub } = getLedgerDetailsFromChannel(decrypted, isMultisig); - if (signer.xpub === xpub) { - dispatch(healthCheckSigner([signer])); - navigation.dispatch(CommonActions.goBack()); - showToast(`${signer.signerName} verified successfully`, ); + let masterFingerprint, signerType; + + switch (deviceType) { + case LEDGER_HEALTHCHECK: + ({ masterFingerprint } = getLedgerDetailsFromChannel(decrypted, isMultisig)); + signerType = SignerType.LEDGER; + break; + case TREZOR_HEALTHCHECK: + ({ masterFingerprint } = getTrezorDetails(decrypted, isMultisig)); + signerType = SignerType.TREZOR; + break; + case BITBOX_HEALTHCHECK: + ({ masterFingerprint } = getTrezorDetails(decrypted, isMultisig)); + signerType = SignerType.BITBOX02; + break; + default: + break; + } + + if (mode === InteracationMode.IDENTIFICATION) { + const mapped = mapUnknownSigner({ masterFingerprint, type: signerType }); + if (mapped) { + handleSuccess(); + } else { + handleFailure(); + } } else { - navigation.dispatch(CommonActions.goBack()); - showToast(`${signer.signerName} verification failed`, ); + if (masterFingerprint === signer.masterFingerprint) { + handleSuccess(); + } else { + handleFailure(); + } } } catch (error) { if (error instanceof HWError) { showToast(error.message, , 3000); } else if (error.toString() === 'Error') { // ignore if user cancels NFC interaction - } else captureError(error); - } - }); - channel.on(TREZOR_HEALTHCHECK, async (data) => { - try { - const decrypted = createDecipheriv(data, decryptionKey.current); - const { xpub } = getTrezorDetails(decrypted, isMultisig); - if (signer.xpub === xpub) { - dispatch(healthCheckSigner([signer])); - navigation.dispatch(CommonActions.goBack()); - showToast(`${signer.signerName} verified successfully`, ); } else { - navigation.dispatch(CommonActions.goBack()); - showToast(`${signer.signerName} verification failed`, ); + captureError(error); } - } catch (error) { - if (error instanceof HWError) { - showToast(error.message, , 3000); - } else if (error.toString() === 'Error') { - // ignore if user cancels NFC interaction - } else captureError(error); } + }; + + channel.on(LEDGER_HEALTHCHECK, async (data) => { + await handleVerification(data, LEDGER_HEALTHCHECK); + }); + + channel.on(TREZOR_HEALTHCHECK, async (data) => { + await handleVerification(data, TREZOR_HEALTHCHECK); }); + channel.on(BITBOX_HEALTHCHECK, async (data) => { - try { - const decrypted = createDecipheriv(data, decryptionKey.current); - const { xpub } = getTrezorDetails(decrypted, isMultisig); - if (signer.xpub === xpub) { - dispatch(healthCheckSigner([signer])); - navigation.dispatch(CommonActions.goBack()); - showToast(`${signer.signerName} verified successfully`, ); - } else { - navigation.dispatch(CommonActions.goBack()); - showToast(`${signer.signerName} verification failed`, ); - } - } catch (error) { - if (error instanceof HWError) { - showToast(error.message, , 3000); - } else if (error.toString() === 'Error') { - // ignore if user cancels NFC interaction - } else captureError(error); - } + await handleVerification(data, BITBOX_HEALTHCHECK); }); return () => { @@ -290,7 +296,12 @@ function ConnectChannel() { return ( - + diff --git a/src/screens/ChoosePlanScreen/ChoosePlan.tsx b/src/screens/ChoosePlanScreen/ChoosePlan.tsx index 71b59bb6f..8ed86339f 100644 --- a/src/screens/ChoosePlanScreen/ChoosePlan.tsx +++ b/src/screens/ChoosePlanScreen/ChoosePlan.tsx @@ -5,9 +5,9 @@ import RNIap, { getSubscriptions, purchaseErrorListener, purchaseUpdatedListener, - requestSubscription, getAvailablePurchases, SubscriptionPurchase, + requestSubscription, } from 'react-native-iap'; import React, { useContext, useEffect, useState } from 'react'; import ChoosePlanCarousel from 'src/components/Carousel/ChoosePlanCarousel'; @@ -21,7 +21,6 @@ import SubScription, { SubScriptionPlan } from 'src/models/interfaces/Subscripti import dbManager from 'src/storage/realm/dbManager'; import { wp } from 'src/constants/responsive'; import Relay from 'src/services/operations/Relay'; -import MonthlyYearlySwitch from 'src/components/Switch/MonthlyYearlySwitch'; import moment from 'moment'; import { getBundleId } from 'react-native-device-info'; import { useDispatch } from 'react-redux'; @@ -30,12 +29,15 @@ import { uaiType } from 'src/models/interfaces/Uai'; import useToastMessage from 'src/hooks/useToastMessage'; import KeeperModal from 'src/components/KeeperModal'; import LoadingAnimation from 'src/components/Loader'; -import TierUpgradeModal from './TierUpgradeModal'; import { useQuery } from '@realm/react'; -import { useRoute } from '@react-navigation/native'; +import SettingsIcon from 'src/assets/images/settings_white.svg'; +import TierUpgradeModal from './TierUpgradeModal'; +import { useNavigation, useRoute } from '@react-navigation/native'; +import CircleIconWrapper from 'src/components/CircleIconWrapper'; function ChoosePlan() { const route = useRoute(); + const navigation = useNavigation(); const initialPosition = route.params?.planPosition || 0; const { colorMode } = useColorMode(); const { translations, formatString } = useContext(LocalizationContext); @@ -55,6 +57,7 @@ function ChoosePlan() { const [isMonthly, setIsMonthly] = useState(true); const { subscription }: KeeperApp = useQuery(RealmSchema.KeeperApp)[0]; const disptach = useDispatch(); + const [isServiceUnavailible, setIsServiceUnavailible] = useState(false); useEffect(() => { const purchaseUpdateSubscription = purchaseUpdatedListener(async (purchase) => { @@ -80,13 +83,14 @@ function ChoosePlan() { }, []); async function init() { + let data = []; try { const getPlansResponse = await Relay.getSubscriptionDetails(id, publicId); if (getPlansResponse.plans) { + data = getPlansResponse.plans; const skus = []; getPlansResponse.plans.forEach((plan) => skus.push(...plan.productIds)); const subscriptions = await getSubscriptions({ skus }); - const data = getPlansResponse.plans; subscriptions.forEach((subscription, i) => { const index = data.findIndex((plan) => plan.productIds.includes(subscription.productId)); const monthlyPlans = []; @@ -134,6 +138,15 @@ function ChoosePlan() { } } catch (error) { console.log('error', error); + if (error.message.includes('Billing is unavailable.')) { + setItems(data); + setLoading(false); + showToast(error.message); + setIsServiceUnavailible(true); + } else { + navigation.goBack(); + showToast(error.message); + } } } @@ -160,7 +173,7 @@ function ChoosePlan() { } else if (response.error) { showToast(response.error); } - await RNIap.finishTransaction({ purchase, isConsumable: false }); + if (receipt) await RNIap.finishTransaction({ purchase, isConsumable: false }); } catch (error) { setRequesting(false); console.log(error); @@ -248,11 +261,17 @@ function ChoosePlan() { ]); } } else { + if (isServiceUnavailible) { + showToast( + 'It seems that you don’t have Google services for app subscriptions. Ability to pay using bitcoin coming soon' + ); + return; + } setRequesting(true); const plan = isMonthly ? subscription.monthlyPlanDetails : subscription.yearlyPlanDetails; const sku = plan.productId; const { offerToken } = plan; - var purchaseTokenAndroid = null; + let purchaseTokenAndroid = null; if (Platform.OS === 'android' && appSubscription.receipt) { purchaseTokenAndroid = JSON.parse(appSubscription.receipt).purchaseToken; } @@ -309,7 +328,6 @@ function ChoosePlan() { if (purchases.length === 0) { showToast('No purchases found'); } else { - // eslint-disable-next-line no-plusplus for (let i = 0; i < purchases.length; i++) { const purchase = purchases[i]; if (purchase.productId === subscription.productId) { @@ -347,14 +365,8 @@ function ChoosePlan() { setIsMonthly(!isMonthly)} /> - } + subtitle={'Upgrade or downgrade'} + //To-Do-Learn-More /> - + {getBenifitsTitle(items[currentPosition].name)}: @@ -447,10 +459,10 @@ function ChoosePlan() { > - + {choosePlan.restorePurchases} @@ -463,14 +475,14 @@ const styles = StyleSheet.create({ noteWrapper: { bottom: 1, margin: 1, - alignItems: 'center', + alignItems: 'flex-end', flexDirection: 'row', justifyContent: 'center', width: '100%', }, restorePurchaseWrapper: { padding: 1, - margin: 1, + marginBottom: 10, borderRadius: 5, borderWidth: 0.7, alignItems: 'center', diff --git a/src/screens/EnterWalletDetailScreen/EnterWalletDetailScreen.tsx b/src/screens/EnterWalletDetailScreen/EnterWalletDetailScreen.tsx index a36a84260..9898d748c 100644 --- a/src/screens/EnterWalletDetailScreen/EnterWalletDetailScreen.tsx +++ b/src/screens/EnterWalletDetailScreen/EnterWalletDetailScreen.tsx @@ -165,7 +165,7 @@ function EnterWalletDetailScreen({ route }) { placeholderTextColor={`${colorMode}.GreyText`} value={path} onChangeText={(value) => { - setPath(value) + setPath(value); }} style={styles.inputField} width={wp(260)} @@ -210,12 +210,11 @@ function EnterWalletDetailScreen({ route }) { value={walletName} onChangeText={(value) => { if (route.params?.name === walletName) { - setWalletName('') - return + setWalletName(''); + return; } - setWalletName(value) - } - } + setWalletName(value); + }} style={styles.inputField} width={wp(260)} autoCorrect={false} @@ -236,12 +235,11 @@ function EnterWalletDetailScreen({ route }) { value={walletDescription} onChangeText={(value) => { if (route.params?.description === walletDescription) { - setWalletDescription('') - return + setWalletDescription(''); + return; } - setWalletDescription(value) - } - } + setWalletDescription(value); + }} style={styles.inputField} width={wp(260)} autoCorrect={false} @@ -343,7 +341,7 @@ function EnterWalletDetailScreen({ route }) { }} primaryText={`${common.create}`} primaryCallback={createNewWallet} - primaryDisable={!walletName || !walletDescription} + primaryDisable={!walletName} primaryLoading={walletLoading || relayWalletUpdateLoading} /> @@ -351,7 +349,7 @@ function EnterWalletDetailScreen({ route }) { { }} + close={() => {}} visible={hasNewWalletsGenerationFailed} subTitle={err} title="Failed" diff --git a/src/screens/ExportSeedScreen/ExportSeedScreen.tsx b/src/screens/ExportSeedScreen/ExportSeedScreen.tsx index 46ea11cc4..c4f49826c 100644 --- a/src/screens/ExportSeedScreen/ExportSeedScreen.tsx +++ b/src/screens/ExportSeedScreen/ExportSeedScreen.tsx @@ -25,6 +25,7 @@ import { SignerType } from 'src/core/wallets/enums'; import { Wallet } from 'src/core/wallets/interfaces/wallet'; import { VaultSigner } from 'src/core/wallets/interfaces/vault'; import Illustration from 'src/assets/images/illustration.svg'; +import Note from 'src/components/Note/Note'; function ExportSeedScreen({ route, navigation }) { const { colorMode } = useColorMode(); @@ -41,14 +42,13 @@ function ExportSeedScreen({ route, navigation }) { }: { seed: string; wallet: Wallet; isHealthCheck: boolean; signer: VaultSigner } = route.params; const { showToast } = useToastMessage(); const [words, setWords] = useState(seed.split(' ')); - const { next } = route.params; + const { next, viewRecoveryKeys } = route.params; const [confirmSeedModal, setConfirmSeedModal] = useState(false); const [backupSuccessModal, setBackupSuccessModal] = useState(false); const [showQRVisible, setShowQRVisible] = useState(false); const [showWordIndex, setShowWordIndex] = useState(''); const { backupMethod } = useAppSelector((state) => state.bhr); const seedText = translations.seed; - useEffect(() => { if (backupMethod !== null && next) { setBackupSuccessModal(true); @@ -57,36 +57,60 @@ function ExportSeedScreen({ route, navigation }) { function SeedCard({ item, index }: { item; index }) { return ( - { - setShowWordIndex((prev) => { - if (prev === index) { - return ''; - } - return index; - }); - }} - > - - - {index < 9 ? '0' : null} - {index + 1} - - + {viewRecoveryKeys ? ( + + + + {index < 9 ? '0' : null} + {index + 1} + + + {item} + + + + ) : ( + { + setShowWordIndex((prev) => { + if (prev === index) { + return ''; + } + return index; + }); + }} > - {showWordIndex === index ? item : '******'} - - - + + + {index < 9 ? '0' : null} + {index + 1} + + + {showWordIndex === index ? item : '******'} + + + + )} + ); } @@ -97,7 +121,7 @@ function ExportSeedScreen({ route, navigation }) { return ( - + item} /> - {!next && ( + + + + {!viewRecoveryKeys && !next && ( { // setShowQRVisible(true); @@ -151,11 +182,6 @@ function ExportSeedScreen({ route, navigation }) { )} - {!next && ( - - {seedText.desc} - - )} {/* Modals */} { }} + close={() => {}} title={BackupWallet.backupSuccessTitle} subTitleColor="light.secondaryText" textColor="light.primaryText" diff --git a/src/screens/Home/HomeScreen.tsx b/src/screens/Home/HomeScreen.tsx new file mode 100644 index 000000000..d82294f87 --- /dev/null +++ b/src/screens/Home/HomeScreen.tsx @@ -0,0 +1,258 @@ +import { Linking, StyleSheet } from 'react-native'; +import { Box, useColorMode } from 'native-base'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import React, { useEffect, useState } from 'react'; +import useWallets from 'src/hooks/useWallets'; +import { useAppSelector } from 'src/store/hooks'; +import { Wallet } from 'src/core/wallets/interfaces/wallet'; +import { VisibilityType, WalletType } from 'src/core/wallets/enums'; +import TickIcon from 'src/assets/images/icon_tick.svg'; +import useToastMessage from 'src/hooks/useToastMessage'; +import { useDispatch } from 'react-redux'; +import { resetElectrumNotConnectedErr } from 'src/store/reducers/login'; +import ToastErrorIcon from 'src/assets/images/toast_error.svg'; +import { Vault } from 'src/core/wallets/interfaces/vault'; +import useCollaborativeWallet from 'src/hooks/useCollaborativeWallet'; +import { resetRealyWalletState } from 'src/store/reducers/bhr'; +import useVault from 'src/hooks/useVault'; +import idx from 'idx'; +import { CommonActions } from '@react-navigation/native'; +import BTC from 'src/assets/images/icon_bitcoin_white.svg'; +import InheritanceIcon from 'src/assets/images/inheri.svg'; +import SignerIcon from 'src/assets/images/signer_white.svg'; +import usePlan from 'src/hooks/usePlan'; +import { SubscriptionTier } from 'src/models/enums/SubscriptionTier'; +import { urlParamsToObj } from 'src/core/utils'; +import { HomeModals } from './components/HomeModals'; +import { TopSection } from './components/TopSection'; +import { WalletsList } from './components/WalletList'; + +const calculateBalancesForVaults = (vaults) => { + let totalUnconfirmedBalance = 0; + let totalConfirmedBalance = 0; + + vaults.forEach((vault) => { + const unconfirmedBalance = idx(vault, (_) => _.specs.balances.unconfirmed) || 0; + const confirmedBalance = idx(vault, (_) => _.specs.balances.confirmed) || 0; + + totalUnconfirmedBalance += unconfirmedBalance; + totalConfirmedBalance += confirmedBalance; + }); + return totalUnconfirmedBalance + totalConfirmedBalance; +}; + +function NewHomeScreen({ navigation }) { + const { colorMode } = useColorMode(); + const dispatch = useDispatch(); + const { wallets } = useWallets({ getAll: true }); + const { collaborativeWallets } = useCollaborativeWallet(); + const { allVaults, activeVault } = useVault({ + includeArchived: false, + getFirst: true, + getHiddenWallets: false, + }); + const nonHiddenWallets = wallets.filter( + (wallet) => wallet.presentationData.visibility !== VisibilityType.HIDDEN + ); + const allWallets: (Wallet | Vault)[] = [...nonHiddenWallets, ...allVaults].filter( + (item) => item !== null + ); + const [isShowAmount, setIsShowAmount] = useState(false); + const [addImportVisible, setAddImportVisible] = useState(false); + const [electrumErrorVisible, setElectrumErrorVisible] = useState(false); + const { relayWalletUpdate, relayWalletError, realyWalletErrorMessage } = useAppSelector( + (state) => state.bhr + ); + const netBalanceWallets = useAppSelector((state) => state.wallet.netBalance); + const netBalanceAllVaults = calculateBalancesForVaults(allVaults); + + const [defaultWalletCreation, setDefaultWalletCreation] = useState(false); + const { showToast } = useToastMessage(); + const { top } = useSafeAreaInsets(); + const { plan } = usePlan(); + const electrumClientConnectionStatus = useAppSelector( + (state) => state.login.electrumClientConnectionStatus + ); + const [showBuyRampModal, setShowBuyRampModal] = useState(false); + const receivingAddress = idx(wallets[0], (_) => _.specs.receivingAddress) || ''; + const balance = idx(wallets[0], (_) => _.specs.balances.confirmed) || 0; + const presentationName = idx(wallets[0], (_) => _.presentationData.name) || ''; + + function handleDeepLinkEvent({ url }) { + if (url) { + if (url.includes('backup')) { + const splits = url.split('backup/'); + const decoded = Buffer.from(splits[1], 'base64').toString(); + const params = urlParamsToObj(decoded); + if (params.seed) { + navigation.navigate('EnterWalletDetail', { + seed: params.seed, + name: `${ + params.name.slice(0, 1).toUpperCase() + params.name.slice(1, params.name.length) + } `, + path: params.path, + appId: params.appId, + description: `Imported from ${ + params.name.slice(0, 1).toUpperCase() + params.name.slice(1, params.name.length) + } `, + type: WalletType.IMPORTED, + }); + } else { + showToast('Invalid deeplink'); + } + } + } + } + + async function handleDeepLinking() { + try { + const initialUrl = await Linking.getInitialURL(); + if (initialUrl) { + if (initialUrl.includes('backup')) { + const splits = initialUrl.split('backup/'); + const decoded = Buffer.from(splits[1], 'base64').toString(); + const params = urlParamsToObj(decoded); + if (params.seed) { + navigation.navigate('EnterWalletDetail', { + seed: params.seed, + name: `${ + params.name.slice(0, 1).toUpperCase() + params.name.slice(1, params.name.length) + } `, + path: params.path, + appId: params.appId, + purpose: params.purpose, + description: `Imported from ${ + params.name.slice(0, 1).toUpperCase() + params.name.slice(1, params.name.length) + } `, + type: WalletType.IMPORTED, + }); + } else { + showToast('Invalid deeplink'); + } + } else if (initialUrl.includes('create/')) { + } + } + } catch (error) { + // + } + } + + useEffect(() => { + Linking.addEventListener('url', handleDeepLinkEvent); + handleDeepLinking(); + return () => { + Linking.removeAllListeners('url'); + }; + }, []); + + useEffect(() => { + if (electrumClientConnectionStatus.success) { + showToast(`Connected to: ${electrumClientConnectionStatus.connectedTo}`, ); + if (electrumErrorVisible) setElectrumErrorVisible(false); + } else if (electrumClientConnectionStatus.failed) { + showToast(`${electrumClientConnectionStatus.error}`, ); + setElectrumErrorVisible(true); + } + }, [electrumClientConnectionStatus.success, electrumClientConnectionStatus.error]); + + useEffect(() => { + if (electrumClientConnectionStatus.setElectrumNotConnectedErr) { + showToast(`${electrumClientConnectionStatus.setElectrumNotConnectedErr}`, ); + dispatch(resetElectrumNotConnectedErr()); + } + }, [electrumClientConnectionStatus.setElectrumNotConnectedErr]); + + useEffect(() => { + if (relayWalletUpdate) { + if (defaultWalletCreation && wallets[collaborativeWallets.length]) { + navigation.navigate('SetupCollaborativeWallet', { + coSigner: wallets[collaborativeWallets.length], + walletId: wallets[collaborativeWallets.length].id, + collaborativeWalletsCount: collaborativeWallets.length, + }); + dispatch(resetRealyWalletState()); + setDefaultWalletCreation(false); + } + } + if (relayWalletError) { + showToast( + realyWalletErrorMessage || 'Something went wrong - Wallet creation failed', + + ); + setDefaultWalletCreation(false); + dispatch(resetRealyWalletState()); + } + }, [relayWalletUpdate, relayWalletError, wallets]); + + const onPressBuyBitcoin = () => setShowBuyRampModal(true); + const cardsData = [ + { + name: 'Buy\nBitcoin', + icon: , + callback: onPressBuyBitcoin, + }, + { + name: 'Manage\nAll Signers', + icon: , + callback: () => navigation.dispatch(CommonActions.navigate({ name: 'ManageSigners' })), + }, + { + name: 'Inheritance & Security Tools', + icon: , + callback: () => { + const eligible = plan === SubscriptionTier.L3.toUpperCase(); + if (!eligible) { + showToast(`Please upgrade to ${SubscriptionTier.L3} to use Inheritance Tools`); + navigation.navigate('ChoosePlan', { planPosition: 2 }); + } else if (!activeVault) { + showToast('Please create a vault to setup inheritance'); + navigation.dispatch( + CommonActions.navigate({ + name: 'AddSigningDevice', + merge: true, + params: { scheme: { m: 3, n: 5 } }, + }) + ); + } else { + navigation.dispatch(CommonActions.navigate({ name: 'SetupInheritance' })); + } + }, + }, + ]; + + return ( + + + setIsShowAmount(!isShowAmount)} + /> + + + ); +} + +export default NewHomeScreen; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, +}); diff --git a/src/screens/Home/components/AddWalletModal.tsx b/src/screens/Home/components/AddWalletModal.tsx new file mode 100644 index 000000000..2190b5a4d --- /dev/null +++ b/src/screens/Home/components/AddWalletModal.tsx @@ -0,0 +1,147 @@ +import { StyleSheet } from 'react-native'; +import React, { useContext } from 'react'; +import KeeperModal from 'src/components/KeeperModal'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import { Box, useColorMode } from 'native-base'; +import MenuItemButton from 'src/components/CustomButton/MenuItemButton'; +import AddWallet from 'src/assets/images/addWallet.svg'; +import ImportWallet from 'src/assets/images/importWallet.svg'; +import AddCollaborativeWalletIcon from 'src/assets/images/icon_collab.svg'; +import { NewWalletInfo } from 'src/store/sagas/wallets'; +import { WalletType } from 'src/core/wallets/enums'; +import { v4 as uuidv4 } from 'uuid'; +import { defaultTransferPolicyThreshold } from 'src/store/sagas/storage'; +import { addNewWallets } from 'src/store/sagaActions/wallets'; +import { useDispatch } from 'react-redux'; +import Text from 'src/components/KeeperText'; +import { hp } from 'src/constants/responsive'; + +const addNewDefaultWallet = (walletsCount, dispatch) => { + const newWallet: NewWalletInfo = { + walletType: WalletType.DEFAULT, + walletDetails: { + name: `Wallet ${walletsCount + 1} `, + description: ``, + transferPolicy: { + id: uuidv4(), + threshold: defaultTransferPolicyThreshold, + }, + }, + }; + dispatch(addNewWallets([newWallet])); +}; + +function AddImportWallet({ + wallets, + collaborativeWallets, + setAddImportVisible, + setDefaultWalletCreation, + navigation, +}) { + const { colorMode } = useColorMode(); + const dispatch = useDispatch(); + const { translations } = useContext(LocalizationContext); + const { wallet } = translations; + + const addCollaborativeWallet = () => { + setAddImportVisible(false); + const collaborativeWalletsCount = collaborativeWallets.length; + const walletsCount = wallets.length; + if (collaborativeWalletsCount < walletsCount) { + navigation.navigate('SetupCollaborativeWallet', { + coSigner: wallets[collaborativeWalletsCount], + walletId: wallets[collaborativeWalletsCount].id, + collaborativeWalletsCount, + }); + } else { + setDefaultWalletCreation(true); + addNewDefaultWallet(wallets.length, dispatch); + } + }; + + return ( + + { + setAddImportVisible(false); + navigation.navigate('EnterWalletDetail', { + name: `Wallet ${wallets.length + 1}`, + description: '', + type: WalletType.DEFAULT, + }); + }} + icon={} + title={wallet.addWallet} + subTitle={wallet.addWalletSubTitle} + height={80} + /> + { + setAddImportVisible(false); + navigation.navigate('ImportWallet'); + }} + icon={} + title={wallet.importWalletTitle} + subTitle={wallet.manageWalletSubTitle} + height={80} + /> + } + title={wallet.addCollabWalletTitle} + subTitle={wallet.addCollabWalletSubTitle} + height={80} + /> + + + {wallet.addCollabWalletParagraph} + + + + ); +} + +const AddWalletModal = ({ + navigation, + visible, + setAddImportVisible, + wallets, + collaborativeWallets, + setDefaultWalletCreation, +}) => { + const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { importWallet } = translations; + + return ( + setAddImportVisible(false)} + title={importWallet.AddImportModalTitle} + subTitle={importWallet.AddImportModalSubTitle} + modalBackground={`${colorMode}.modalWhiteBackground`} + subTitleColor={`${colorMode}.secondaryText`} + textColor={`${colorMode}.primaryText`} + DarkCloseIcon={colorMode === 'dark'} + Content={() => ( + + )} + /> + ); +}; + +export default AddWalletModal; + +const styles = StyleSheet.create({ + addImportParaContent: { + fontSize: 13, + padding: 2, + marginTop: hp(20), + }, +}); diff --git a/src/screens/Home/components/BalanceComponent.tsx b/src/screens/Home/components/BalanceComponent.tsx new file mode 100644 index 000000000..5023d878e --- /dev/null +++ b/src/screens/Home/components/BalanceComponent.tsx @@ -0,0 +1,49 @@ +import { Box, HStack, useColorMode } from 'native-base'; +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import Text from 'src/components/KeeperText'; +import CurrencyInfo from 'src/screens/Home/components/CurrencyInfo'; +import Colors from 'src/theme/Colors'; + +function BalanceComponent({ balance, count, isShowAmount, setIsShowAmount }) { + const { colorMode } = useColorMode(); + return ( + + + {count} + Wallet{count > 1 && 's'} + + + + + + ); +} + +export default BalanceComponent; + +const styles = StyleSheet.create({ + walletWrapper: { + justifyContent: 'center', + marginHorizontal: 20, + }, + noOfWallet: { + fontSize: 27, + lineHeight: 27, + }, + amount: { + textAlign: 'center', + gap: 5, + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + }, +}); diff --git a/src/screens/HomeScreen/components/CurrencyInfo.tsx b/src/screens/Home/components/CurrencyInfo.tsx similarity index 83% rename from src/screens/HomeScreen/components/CurrencyInfo.tsx rename to src/screens/Home/components/CurrencyInfo.tsx index 38a615003..c0d375c95 100644 --- a/src/screens/HomeScreen/components/CurrencyInfo.tsx +++ b/src/screens/Home/components/CurrencyInfo.tsx @@ -11,8 +11,8 @@ interface ICurrencyInfo { hideAmounts: boolean; amount: number; fontSize: number; - color: string; - variation: 'light' | 'green' | 'dark' | 'grey'; + color?: string; + variation?: 'light' | 'green' | 'dark' | 'grey'; } function CurrencyInfo({ hideAmounts, @@ -22,13 +22,17 @@ function CurrencyInfo({ variation = 'grey', }: ICurrencyInfo) { const { getSatUnit, getBalance, getCurrencyIcon } = useBalance(); - return ( {getCurrencyIcon(BTC, variation)} {!hideAmounts ? ( - + {` ${getBalance(amount)} ${getSatUnit()}`} @@ -37,7 +41,7 @@ function CurrencyInfo({ style={[styles.rowCenter, styles.hiddenContainer, { height: fontSize + 1 }]} testID="view_hideCurrencyView" > - + )} diff --git a/src/screens/Home/components/DowngradeModal.tsx b/src/screens/Home/components/DowngradeModal.tsx new file mode 100644 index 000000000..bbd62f13e --- /dev/null +++ b/src/screens/Home/components/DowngradeModal.tsx @@ -0,0 +1,123 @@ +import { useQuery } from '@realm/react'; +import { Box, useColorMode } from 'native-base'; +import React, { useContext } from 'react'; +import { StyleSheet, TouchableOpacity } from 'react-native'; +import { Shadow } from 'react-native-shadow-2'; +import { useDispatch } from 'react-redux'; +import DowngradeToPleb from 'src/assets/images/downgradetopleb.svg'; +import DowngradeToPlebDark from 'src/assets/images/downgradetoplebDark.svg'; +import KeeperModal from 'src/components/KeeperModal'; +import Text from 'src/components/KeeperText'; +import { hp, wp } from 'src/constants/responsive'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import { AppSubscriptionLevel, SubscriptionTier } from 'src/models/enums/SubscriptionTier'; +import { KeeperApp } from 'src/models/interfaces/KeeperApp'; +import SubScription from 'src/models/interfaces/Subscription'; +import Relay from 'src/services/operations/Relay'; +import dbManager from 'src/storage/realm/dbManager'; +import { RealmSchema } from 'src/storage/realm/enum'; +import { getJSONFromRealmObject } from 'src/storage/realm/utils'; +import { useAppSelector } from 'src/store/hooks'; +import { setRecepitVerificationFailed } from 'src/store/reducers/login'; + +async function downgradeToPleb(dispatch, app) { + try { + const updatedSubscription: SubScription = { + receipt: '', + productId: SubscriptionTier.L1, + name: SubscriptionTier.L1, + level: AppSubscriptionLevel.L1, + icon: 'assets/ic_pleb.svg', + }; + dbManager.updateObjectById(RealmSchema.KeeperApp, app.id, { + subscription: updatedSubscription, + }); + dispatch(setRecepitVerificationFailed(false)); + const response = await Relay.updateSubscription(app.id, app.publicId, { + productId: SubscriptionTier.L1.toLowerCase(), + }); + } catch (error) { + // + } +} + +function DowngradeModalContent({ navigation, app }) { + const { colorMode } = useColorMode(); + const dispatch = useDispatch(); + const { translations } = useContext(LocalizationContext); + const { common } = translations; + + return ( + + {colorMode === 'light' ? : } + + { + navigation.navigate('ChoosePlan'); + dispatch(setRecepitVerificationFailed(false)); + }} + activeOpacity={0.5} + > + + {common.viewSubscription} + + + { + downgradeToPleb(dispatch, app); + }} + > + + + + {common.continuePleb} + + + + + + + ); +} + +export const DowngradeModal = ({ navigation }) => { + const { colorMode } = useColorMode(); + const app: KeeperApp = useQuery(RealmSchema.KeeperApp).map(getJSONFromRealmObject)[0]; + const { recepitVerificationFailed } = useAppSelector((state) => state.login); + const { translations } = useContext(LocalizationContext); + const { choosePlan } = translations; + + return ( + {}} + visible={recepitVerificationFailed} + title={choosePlan.validateSubscriptionTitle} + subTitle={choosePlan.validateSubscriptionSubTitle} + Content={() => } + modalBackground={`${colorMode}.modalWhiteBackground`} + subTitleColor={`${colorMode}.secondaryText`} + textColor={`${colorMode}.primaryText`} + subTitleWidth={wp(210)} + showButtons + showCloseIcon={false} + /> + ); +}; + +const styles = StyleSheet.create({ + cancelBtn: { + marginRight: wp(20), + borderRadius: 10, + }, + btnText: { + fontSize: 12, + letterSpacing: 0.84, + }, + createBtn: { + paddingVertical: hp(15), + borderRadius: 10, + paddingHorizontal: 20, + }, +}); diff --git a/src/screens/Home/components/ElectrumDisconnectModal.tsx b/src/screens/Home/components/ElectrumDisconnectModal.tsx new file mode 100644 index 000000000..3713fb6ed --- /dev/null +++ b/src/screens/Home/components/ElectrumDisconnectModal.tsx @@ -0,0 +1,59 @@ +import { StyleSheet } from 'react-native'; +import React, { useContext } from 'react'; +import KeeperModal from 'src/components/KeeperModal'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import { Box, useColorMode } from 'native-base'; +import { hp, wp } from 'src/constants/responsive'; +import DowngradeToPleb from 'src/assets/images/downgradetopleb.svg'; +import DowngradeToPlebDark from 'src/assets/images/downgradetoplebDark.svg'; +import Text from 'src/components/KeeperText'; + +function ElectrumErrorContent() { + const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { common } = translations; + return ( + + + {colorMode === 'light' ? : } + + + + Please try again later + + + + ); +} + +const ElectrumDisconnectModal = ({ electrumErrorVisible, setElectrumErrorVisible }) => { + const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { common } = translations; + return ( + setElectrumErrorVisible(false)} + title={common.connectionError} + subTitle={common.electrumErrorSubTitle} + buttonText={common.continue} + modalBackground={`${colorMode}.modalWhiteBackground`} + subTitleColor={`${colorMode}.secondaryText`} + textColor={`${colorMode}.primaryText`} + buttonTextColor="light.white" + DarkCloseIcon={colorMode === 'dark'} + buttonCallback={() => setElectrumErrorVisible(false)} + Content={ElectrumErrorContent} + /> + ); +}; + +export default ElectrumDisconnectModal; + +const styles = StyleSheet.create({ + networkText: { + fontSize: 13, + padding: 1, + letterSpacing: 0.65, + }, +}); diff --git a/src/screens/Home/components/HeaderDetails/components/CurrentPlanView.tsx b/src/screens/Home/components/HeaderDetails/components/CurrentPlanView.tsx new file mode 100644 index 000000000..d8c6aaf1d --- /dev/null +++ b/src/screens/Home/components/HeaderDetails/components/CurrentPlanView.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Box, useColorMode } from 'native-base'; +import Text from 'src/components/KeeperText'; +import { StyleSheet } from 'react-native'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import { useNavigation } from '@react-navigation/native'; +import Fonts from 'src/constants/Fonts'; +import PlebIcon from 'src/assets/images/pleb_white.svg'; +import HodlerIcon from 'src/assets/images/hodler.svg'; +import DiamondIcon from 'src/assets/images/diamond_hands.svg'; +import SettingIcon from 'src/assets/images/settings.svg'; + +function CurrentPlanView({ plan }) { + const navigation = useNavigation(); + const { colorMode } = useColorMode(); + return ( + + + navigation.navigate('ChoosePlan')}> + {plan === 'Pleb'.toUpperCase() && } + {plan === 'Hodler'.toUpperCase() && } + {plan === 'Diamond Hands'.toUpperCase() && } + + {plan} + + + navigation.navigate('AppSettings')} + testID="btn_choosePlan" + > + + + + + ); +} +const styles = StyleSheet.create({ + wrapper: { + paddingVertical: 10, + }, + planContianer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + plan: { + flexDirection: 'row', + gap: 5, + }, + titleTxet: { + fontSize: 12, + }, + currentPlanText: { + fontSize: 18, + letterSpacing: 1.8, + fontFamily: Fonts.FiraSansCondensedMedium, + }, +}); +export default CurrentPlanView; diff --git a/src/screens/HomeScreen/components/HeaderDetails/components/HeaderBar.tsx b/src/screens/Home/components/HeaderDetails/components/HeaderBar.tsx similarity index 100% rename from src/screens/HomeScreen/components/HeaderDetails/components/HeaderBar.tsx rename to src/screens/Home/components/HeaderDetails/components/HeaderBar.tsx diff --git a/src/screens/HomeScreen/components/HeaderDetails/components/UAIView.tsx b/src/screens/Home/components/HeaderDetails/components/UAIView.tsx similarity index 84% rename from src/screens/HomeScreen/components/HeaderDetails/components/UAIView.tsx rename to src/screens/Home/components/HeaderDetails/components/UAIView.tsx index 4e08f936b..7a528fb75 100644 --- a/src/screens/HomeScreen/components/HeaderDetails/components/UAIView.tsx +++ b/src/screens/Home/components/HeaderDetails/components/UAIView.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { Box, useColorMode } from 'native-base'; import Text from 'src/components/KeeperText'; import { StyleSheet, TouchableOpacity } from 'react-native'; -import { hp } from 'src/constants/responsive'; import Fonts from 'src/constants/Fonts'; function UAIView({ title, + subTitle, primaryCallbackText, primaryCallback, secondaryCallbackText, @@ -16,9 +16,12 @@ function UAIView({ return ( - + {title} + + {subTitle} + + + + + ); +} + +export default HeaderDetails; diff --git a/src/screens/Home/components/HomeModals.tsx b/src/screens/Home/components/HomeModals.tsx new file mode 100644 index 000000000..d6ceb3472 --- /dev/null +++ b/src/screens/Home/components/HomeModals.tsx @@ -0,0 +1,43 @@ +import AddWalletModal from '../components/AddWalletModal'; +import RampModal from '../../WalletDetails/components/RampModal'; +import { DowngradeModal } from '../components/DowngradeModal'; +import ElectrumDisconnectModal from '../components/ElectrumDisconnectModal'; + +export const HomeModals = ({ + addImportVisible, + electrumErrorVisible, + showBuyRampModal, + setAddImportVisible, + setElectrumErrorVisible, + setShowBuyRampModal, + receivingAddress, + balance, + presentationName, + navigation, + wallets, + collaborativeWallets, + setDefaultWalletCreation, +}) => ( + <> + + + + + +); diff --git a/src/screens/HomeScreen/components/HomeScreenWrapper.tsx b/src/screens/Home/components/HomeScreenWrapper.tsx similarity index 70% rename from src/screens/HomeScreen/components/HomeScreenWrapper.tsx rename to src/screens/Home/components/HomeScreenWrapper.tsx index 33d71d44e..de06346ad 100644 --- a/src/screens/HomeScreen/components/HomeScreenWrapper.tsx +++ b/src/screens/Home/components/HomeScreenWrapper.tsx @@ -7,9 +7,9 @@ function HomeScreenWrapper({ children }) { const { colorMode } = useColorMode(); return ( - + - {children} + {children} ); } @@ -18,7 +18,9 @@ export default HomeScreenWrapper; const styles = StyleSheet.create({ container: { + width: '100%', + }, + childrenWrapper: { flex: 1, - paddingHorizontal: 10, }, }); diff --git a/src/screens/Home/components/TopSection.tsx b/src/screens/Home/components/TopSection.tsx new file mode 100644 index 000000000..19865bdef --- /dev/null +++ b/src/screens/Home/components/TopSection.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { Box } from 'native-base'; +import HeaderDetails from '../components/HeaderDetails'; +import ActionCard from 'src/components/ActionCard'; +import { hp } from 'src/constants/responsive'; + +export const TopSection = ({ colorMode, top, cardsData }) => ( + + + + + + {cardsData.map((data, index) => ( + + ))} + + +); + +const styles = StyleSheet.create({ + padding: { + paddingHorizontal: 10, + }, + wrapper: { + flex: 0.35, + width: '100%', + alignItems: 'center', + position: 'relative', + }, + actionContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 7, + position: 'absolute', + top: hp(220), + }, +}); diff --git a/src/screens/HomeScreen/UaiDisplay.tsx b/src/screens/Home/components/UaiDisplay.tsx similarity index 91% rename from src/screens/HomeScreen/UaiDisplay.tsx rename to src/screens/Home/components/UaiDisplay.tsx index 7fba6d195..9c1a04c49 100644 --- a/src/screens/HomeScreen/UaiDisplay.tsx +++ b/src/screens/Home/components/UaiDisplay.tsx @@ -13,16 +13,16 @@ import useVault from 'src/hooks/useVault'; import useToastMessage from 'src/hooks/useToastMessage'; import InheritanceKeyServer from 'src/services/operations/InheritanceKey'; import ActivityIndicatorView from 'src/components/AppActivityIndicator/ActivityIndicatorView'; -import UAIView from './components/HeaderDetails/components/UAIView'; +import UAIView from '../../HomeScreen/components/HeaderDetails/components/UAIView'; const nonSkippableUAIs = [uaiType.DEFAULT, uaiType.SECURE_VAULT]; -function UaiDisplay({ uaiStack }) { +function UaiDisplay({ uaiStack, vaultId }) { const [uai, setUai] = useState({}); const [uaiConfig, setUaiConfig] = useState({}); const [showModal, setShowModal] = useState(false); const [modalActionLoader, setmodalActionLoader] = useState(false); - const { activeVault } = useVault(); + const { activeVault } = useVault({ vaultId }); const { showToast } = useToastMessage(); const dispatch = useDispatch(); @@ -44,7 +44,7 @@ function UaiDisplay({ uaiStack }) { case uaiType.VAULT_TRANSFER: return { modalDetails: { - heading: 'Trasfer to Vault', + heading: 'Trasfer to vault', subTitle: 'Your Auto-transfer policy has triggered a transaction that needs your approval', btnText: ' Transfer Now', @@ -72,7 +72,7 @@ function UaiDisplay({ uaiStack }) { case uaiType.SIGNING_DEVICES_HEALTH_CHECK: return { cta: () => { - navigtaion.navigate('VaultDetails'); + navigtaion.navigate('VaultDetails', { vaultId: activeVault.id }); }, }; case uaiType.VAULT_MIGRATION: @@ -117,7 +117,7 @@ function UaiDisplay({ uaiStack }) { return { cta: () => { activeVault - ? navigtaion.navigate('VaultDetails') + ? navigtaion.navigate('VaultDetails', { vaultId: activeVault.id }) : showToast('No vaults found', ); }, }; @@ -125,7 +125,7 @@ function UaiDisplay({ uaiStack }) { return { cta: () => { activeVault - ? navigtaion.navigate('VaultDetails') + ? navigtaion.navigate('VaultDetails', { vaultId: activeVault.id }) : showToast('No vaults found', ); }, }; @@ -156,8 +156,9 @@ function UaiDisplay({ uaiStack }) { return ( <> void; +}; + +function WalletInfoCard({ + walletName, + walletDescription, + icon, + amount, + tags, + isShowAmount = false, + setIsShowAmount, +}: WalletInfoCardProps) { + const { colorMode } = useColorMode(); + const { satsEnabled } = useAppSelector((state) => state.settings); + + return ( + + + {tags?.map((tag, index) => { + return ( + + ); + })} + + + + + + {walletDescription} + + + {walletName} + + + + + + + + ); +} + +const styles = StyleSheet.create({ + walletContainer: { + width: wp(160), + height: hp(260), + padding: 10, + borderRadius: 10, + justifyContent: 'space-between', + }, + pillsContainer: { + flexDirection: 'row', + gap: 5, + justifyContent: 'flex-end', + }, + detailContainer: { + alignItems: 'flex-start', + gap: 15, + marginBottom: 20, + marginLeft: 10, + }, +}); + +export default WalletInfoCard; diff --git a/src/screens/Home/components/WalletList.tsx b/src/screens/Home/components/WalletList.tsx new file mode 100644 index 000000000..e22439cc2 --- /dev/null +++ b/src/screens/Home/components/WalletList.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { FlatList, StyleSheet, TouchableOpacity } from 'react-native'; +import { Box } from 'native-base'; +import AddCard from 'src/components/AddCard'; +import BalanceComponent from './BalanceComponent'; +import WalletInfoCard from './WalletInfoCard'; +import { EntityKind, VaultType, WalletType } from 'src/core/wallets/enums'; +import { Vault } from 'src/core/wallets/interfaces/vault'; +import CollaborativeIcon from 'src/assets/images/collaborative_vault_white.svg'; +import WalletIcon from 'src/assets/images/daily_wallet.svg'; +import VaultIcon from 'src/assets/images/vault_icon.svg'; +import { hp, wp } from 'src/constants/responsive'; +export const WalletsList = ({ + allWallets, + navigation, + totalBalance, + isShowAmount, + setIsShowAmount, +}) => ( + + + item.id} + renderItem={({ item: wallet }) => ( + handleWalletPress(wallet, navigation)} + > + + + )} + ListFooterComponent={() => ( + navigation.navigate('AddWallet')} + /> + )} + /> + +); + +const handleWalletPress = (wallet, navigation) => { + if (wallet.entityKind === EntityKind.VAULT) { + navigation.navigate('VaultDetails', { vaultId: wallet.id }); + } else { + navigation.navigate('WalletDetails', { walletId: wallet.id }); + } +}; + +const getWalletTags = (wallet) => { + return wallet.entityKind === EntityKind.VAULT + ? [ + `${wallet.type === VaultType.COLLABORATIVE ? 'COLLABORATIVE' : 'VAULT'}`, + `${(wallet as Vault).scheme.m} of ${(wallet as Vault).scheme.n}`, + ] + : ['SINGLE SIG', `${wallet.type === WalletType.DEFAULT ? 'HOT WALLET' : 'WATCH ONLY'}`]; +}; + +const getWalletIcon = (wallet) => { + if (wallet.entityKind === EntityKind.VAULT) { + return wallet.type === VaultType.COLLABORATIVE ? : ; + } else { + return ; + } +}; + +const calculateWalletBalance = (wallet) => { + const { confirmed, unconfirmed } = wallet.specs.balances; + return confirmed + unconfirmed; +}; + +const styles = StyleSheet.create({ + valueWrapper: { + flex: 0.65, + justifyContent: 'center', + alignItems: 'center', + marginTop: '35%', + gap: 10, + height: '100%', + }, + walletDetailWrapper: { + marginTop: 20, + paddingHorizontal: 20, + }, + walletCardWrapper: { + marginRight: 10, + }, +}); diff --git a/src/screens/HomeScreen/HomeScreen.tsx b/src/screens/HomeScreen/HomeScreen.tsx deleted file mode 100644 index dcf99a41e..000000000 --- a/src/screens/HomeScreen/HomeScreen.tsx +++ /dev/null @@ -1,206 +0,0 @@ -/* eslint-disable react/no-unstable-nested-components */ -import { Linking, StyleSheet, Text, TouchableOpacity } from 'react-native'; -import React, { useEffect } from 'react'; -import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import WalletIcon from 'src/assets/images/walletTab.svg'; -import WalletActiveIcon from 'src/assets/images/walleTabFilled.svg'; -import walletDark from 'src/assets/images/walletDark.svg'; -import VaultIcon from 'src/assets/images/vaultTab.svg'; -import VaultActiveIcon from 'src/assets/images/white_icon_vault.svg'; -import VaultDark from 'src/assets/images/vaultDark.svg'; -import { urlParamsToObj } from 'src/core/utils'; -import { WalletType } from 'src/core/wallets/enums'; -import useToastMessage from 'src/hooks/useToastMessage'; -import { Box, useColorMode } from 'native-base'; -import VaultScreen from './VaultScreen'; -import WalletsScreen from './WalletsScreen'; - -function TabButton({ - label, - Icon, - IconActive, - active, - onPress, - backgroundColorActive, - backgroundColor, - textColorActive, - textColor, -}) { - const { colorMode } = useColorMode(); - const styles = getStyles(colorMode); - return ( - - {active ? : } - - {label} - - - ); -} - -const Tab = createBottomTabNavigator(); - -function NewHomeScreen({ navigation }) { - const { showToast } = useToastMessage(); - useEffect(() => { - Linking.addEventListener('url', handleDeepLinkEvent); - handleDeepLinking(); - return () => { - Linking.removeAllListeners('url'); - }; - }, []); - - function handleDeepLinkEvent({ url }) { - if (url) { - if (url.includes('backup')) { - const splits = url.split('backup/'); - const decoded = Buffer.from(splits[1], 'base64').toString(); - const params = urlParamsToObj(decoded); - if (params.seed) { - navigation.navigate('EnterWalletDetail', { - seed: params.seed, - name: `${ - params.name.slice(0, 1).toUpperCase() + params.name.slice(1, params.name.length) - } `, - path: params.path, - appId: params.appId, - description: `Imported from ${ - params.name.slice(0, 1).toUpperCase() + params.name.slice(1, params.name.length) - } `, - type: WalletType.IMPORTED, - }); - } else { - showToast('Invalid deeplink'); - } - } - } - } - async function handleDeepLinking() { - try { - const initialUrl = await Linking.getInitialURL(); - if (initialUrl) { - if (initialUrl.includes('backup')) { - const splits = initialUrl.split('backup/'); - const decoded = Buffer.from(splits[1], 'base64').toString(); - const params = urlParamsToObj(decoded); - if (params.seed) { - navigation.navigate('EnterWalletDetail', { - seed: params.seed, - name: `${ - params.name.slice(0, 1).toUpperCase() + params.name.slice(1, params.name.length) - } `, - path: params.path, - appId: params.appId, - purpose: params.purpose, - description: `Imported from ${ - params.name.slice(0, 1).toUpperCase() + params.name.slice(1, params.name.length) - } `, - type: WalletType.IMPORTED, - }); - } else { - showToast('Invalid deeplink'); - } - } else if (initialUrl.includes('create/')) { - } - } - } catch (error) { - // - } - } - - function TabBarButton({ state, descriptors }) { - const { colorMode } = useColorMode(); - const styles = getStyles(colorMode); - return ( - - {state.routes.map((route, index) => { - const { options } = descriptors[route.key]; - const label = - options.tabBarLabel !== undefined - ? options.tabBarLabel - : options.title !== undefined - ? options.title - : route.name; - - const isFocused = state.index === index; - - const onPress = () => { - if (!isFocused) { - navigation.navigate({ name: route.name, merge: true }); - } - }; - const themeWalletActive = colorMode === 'light' ? '#2D6759' : '#89AEA7'; - const themeVaultActive = colorMode === 'light' ? '#704E2E' : '#96826F'; - const textWalletColor = colorMode === 'light' ? '#2D6759' : '#89AEA7'; - const textVaultColor = colorMode === 'light' ? '#704E2E' : '#e3be96'; - const vaultActiveIcon = colorMode === 'light' ? VaultActiveIcon : VaultDark; - const walletActiveIcon = colorMode === 'light' ? WalletActiveIcon : walletDark; - return ( - - ); - })} - - ); - } - const { colorMode } = useColorMode(); - const styles = getStyles(colorMode); - return ( - } - > - - - - ); -} - -export default NewHomeScreen; - -const getStyles = (colorMode) => - StyleSheet.create({ - container: { - backgroundColor: colorMode === 'light' ? '#FDF7F0' : '#48514F', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 27, - paddingVertical: 15, - }, - tabContainer: { - backgroundColor: colorMode === 'light' ? '#F2EDE6' : '#323C3A', - }, - label: { - marginLeft: 10, - fontSize: 14, - }, - }); diff --git a/src/screens/HomeScreen/VaultScreen.tsx b/src/screens/HomeScreen/VaultScreen.tsx deleted file mode 100644 index 8f91ecce9..000000000 --- a/src/screens/HomeScreen/VaultScreen.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React, { useContext, useState } from 'react'; -import { Box, ScrollView, useColorMode } from 'native-base'; -import { StyleSheet, TouchableOpacity } from 'react-native'; -import idx from 'idx'; - -import InheritanceIcon from 'src/assets/images/inheritanceWhite.svg'; -import InheritanceDarkIcon from 'src/assets/images/icon_inheritance_dark.svg'; -import EmptyVaultIllustration from 'src/assets/images/EmptyVaultIllustration.svg'; -import { hp } from 'src/constants/responsive'; -import Text from 'src/components/KeeperText'; -import useVault from 'src/hooks/useVault'; -import { CommonActions, useNavigation } from '@react-navigation/native'; -import ListItemView from './components/ListItemView'; -import CurrencyInfo from './components/CurrencyInfo'; -import { SDIcons } from '../Vault/SigningDeviceIcons'; -import HomeScreenWrapper from './components/HomeScreenWrapper'; -import Fonts from 'src/constants/Fonts'; -import { LocalizationContext } from 'src/context/Localization/LocContext'; - -function VaultScreen() { - const { activeVault } = useVault(); - const { colorMode } = useColorMode(); - const signers = idx(activeVault, (_) => _.signers) || []; - const unconfirmedBalance = idx(activeVault, (_) => _.specs.balances.unconfirmed) || 0; - const confirmedBalance = idx(activeVault, (_) => _.specs.balances.confirmed) || 0; - const scheme = idx(activeVault, (_) => _.scheme) || { m: 0, n: 0 }; - const [hideAmounts, setHideAmounts] = useState(true); - - const navigation = useNavigation(); - const { translations } = useContext(LocalizationContext); - const { vault } = translations; - - const navigateToHardwareSetup = () => { - navigation.dispatch(CommonActions.navigate({ name: 'VaultSetup' })); - }; - - const onVaultPress = () => { - if (signers.length) { - navigation.dispatch(CommonActions.navigate({ name: 'VaultDetails' })); - } else { - navigateToHardwareSetup(); - } - }; - - return ( - - {/* */} - - - - {vault.yourVault} - - - - - {!activeVault ? ( - - - - - {vault.toActiveVault} - - ) : ( - <> - - - - {`${scheme.m} of ${scheme.n} Vault`} - - - {signers.map((signer: any) => ( - - {SDIcons(signer.type, colorMode !== 'dark').Icon} - - ))} - - - - - setHideAmounts(!hideAmounts)} - testID="btn_vaultBalance" - > - - - - - )} - - - : } - title={vault.inheritanceTools} - subTitle={vault.manageInheritance} - iconBackColor={`${colorMode}.learnMoreBorder`} - onPress={() => { - navigation.dispatch(CommonActions.navigate({ name: 'SetupInheritance' })); - }} - /> - - - ); -} - -export default VaultScreen; - -const styles = StyleSheet.create({ - titleWrapper: { - marginVertical: hp(20), - }, - titleText: { - fontSize: 16, - fontFamily: Fonts.FiraSansCondensedMedium, - letterSpacing: 1.28, - }, - subTitleText: { - fontSize: 12, - }, - vaultDetailsWrapper: { - paddingHorizontal: 15, - borderRadius: 10, - height: hp(210), - justifyContent: 'center', - marginBottom: hp(20), - }, - emptyVaultIllustration: { - alignSelf: 'center', - marginBottom: 10, - }, - signingDeviceWrapper: { - flexDirection: 'row', - width: '100%', - }, - signingDeviceDetails: { - width: '70%', - }, - signingDeviceList: { - flexDirection: 'row', - marginVertical: hp(5), - }, - unConfirmBalanceView: { - width: '30%', - alignItems: 'flex-end', - }, - signingDeviceText: { - fontSize: 11, - }, - unconfirmText: { - fontSize: 11, - }, - balanceText: { - fontSize: 14, - }, - availableBalanceWrapper: { - marginTop: hp(30), - }, - availableText: { - fontSize: 11, - }, - availablebalanceText: { - fontSize: 20, - }, - vaultSigner: { - justifyContent: 'center', - alignItems: 'center', - marginHorizontal: 2, - width: 30, - height: 30, - borderRadius: 30, - }, -}); diff --git a/src/screens/HomeScreen/WalletsScreen.tsx b/src/screens/HomeScreen/WalletsScreen.tsx deleted file mode 100644 index 6bdf37480..000000000 --- a/src/screens/HomeScreen/WalletsScreen.tsx +++ /dev/null @@ -1,748 +0,0 @@ -import { - StyleSheet, - TouchableOpacity, - Animated, - View, - TouchableWithoutFeedback, -} from 'react-native'; -import React, { useContext, useEffect, useState } from 'react'; -import useWallets from 'src/hooks/useWallets'; -import { useAppSelector } from 'src/store/hooks'; -import { Box, useColorMode } from 'native-base'; -import { hp, windowHeight, windowWidth, wp } from 'src/constants/responsive'; -import { LocalizationContext } from 'src/context/Localization/LocContext'; -import { Wallet } from 'src/core/wallets/interfaces/wallet'; -import { EntityKind, VaultType, VisibilityType, WalletType } from 'src/core/wallets/enums'; -import GradientIcon from 'src/screens/WalletDetails/components/GradientIcon'; -import WalletActiveIcon from 'src/assets/images/walleTabFilled.svg'; -import WalletDark from 'src/assets/images/walletDark.svg'; -import WhirlpoolAccountIcon from 'src/assets/images/whirlpool_account.svg'; -import AddWallet from 'src/assets/images/addWallet.svg'; -import ImportWallet from 'src/assets/images/importWallet.svg'; -import AddCollaborativeWalletIcon from 'src/assets/images/icon_collab.svg'; -import WhirlpoolWhiteIcon from 'src/assets/images/white_icon_whirlpool.svg'; -import WhirlpoolDarkIcon from 'src/assets/images/icon_whirlpool_dark.svg'; -import AddNewWalletIllustration from 'src/assets/images/addNewWalletIllustration.svg'; -import TickIcon from 'src/assets/images/icon_tick.svg'; -import AddSCardIcon from 'src/assets/images/icon_add_white.svg'; -import Text from 'src/components/KeeperText'; -import KeeperModal from 'src/components/KeeperModal'; -import useToastMessage from 'src/hooks/useToastMessage'; -import idx from 'idx'; -import { Shadow } from 'react-native-shadow-2'; -import DowngradeToPleb from 'src/assets/images/downgradetopleb.svg'; -import DowngradeToPlebDark from 'src/assets/images/downgradetoplebDark.svg'; -import dbManager from 'src/storage/realm/dbManager'; -import { SubscriptionTier, AppSubscriptionLevel } from 'src/models/enums/SubscriptionTier'; -import { KeeperApp } from 'src/models/interfaces/KeeperApp'; -import SubScription from 'src/models/interfaces/Subscription'; -import Relay from 'src/services/operations/Relay'; -import { RealmSchema } from 'src/storage/realm/enum'; -import { useDispatch } from 'react-redux'; -import MenuItemButton from 'src/components/CustomButton/MenuItemButton'; -import CollaborativeWalletIcon from 'src/assets/images/icon_collaborative_home.svg'; -import { - resetElectrumNotConnectedErr, - setRecepitVerificationFailed, -} from 'src/store/reducers/login'; -import ToastErrorIcon from 'src/assets/images/toast_error.svg'; -import Fonts from 'src/constants/Fonts'; -import { Vault } from 'src/core/wallets/interfaces/vault'; -import useCollaborativeWallet from 'src/hooks/useCollaborativeWallet'; -import { NewWalletInfo } from 'src/store/sagas/wallets'; -import { v4 as uuidv4 } from 'uuid'; -import { defaultTransferPolicyThreshold } from 'src/store/sagas/storage'; -import { resetRealyWalletState } from 'src/store/reducers/bhr'; -import ActivityIndicatorView from 'src/components/AppActivityIndicator/ActivityIndicatorView'; -import { addNewWallets } from 'src/store/sagaActions/wallets'; -import ListItemView from './components/ListItemView'; -import HomeScreenWrapper from './components/HomeScreenWrapper'; -import CurrencyInfo from './components/CurrencyInfo'; -import { getJSONFromRealmObject } from 'src/storage/realm/utils'; -import { useQuery } from '@realm/react'; - -const ITEM_SIZE = hp(220); - -const calculateBalancesForVaults = (vaults) => { - let totalUnconfirmedBalance = 0; - let totalConfirmedBalance = 0; - - vaults.forEach((vault) => { - const unconfirmedBalance = idx(vault, (_) => _.specs.balances.unconfirmed) || 0; - const confirmedBalance = idx(vault, (_) => _.specs.balances.confirmed) || 0; - - totalUnconfirmedBalance += unconfirmedBalance; - totalConfirmedBalance += confirmedBalance; - }); - return totalUnconfirmedBalance + totalConfirmedBalance; -}; - -function AddNewWalletTile({ wallet, setAddImportVisible }) { - return ( - setAddImportVisible()} - testID="btn_add_wallet" - > - - - {wallet.AddImportNewWallet} - - - ); -} - -function WalletItem({ - item, - walletIndex, - navigation, - translations, - hideAmounts, - setAddImportVisible, -}: { - item: Wallet | Vault; - walletIndex: number; - navigation; - translations; - hideAmounts: boolean; - setAddImportVisible: any; -}) { - const { colorMode } = useColorMode(); - const { wallet } = translations; - if (!item) { - return null; - } - - if (item.key && item.key === 'add-wallet') { - return ( - - - - ); - } - const isWhirlpoolWallet = Boolean(item?.whirlpoolConfig?.whirlpoolWalletDetails); - const isCollaborativeWallet = - item.entityKind === EntityKind.VAULT && item.type === VaultType.COLLABORATIVE; - - return ( - { - isCollaborativeWallet - ? navigation.navigate('VaultDetails', { - collaborativeWalletId: item.collaborativeWalletId, - }) - : navigation.navigate('WalletDetails', { walletId: item.id, walletIndex }); - }} - > - - - - - ); -} - -function WalletList({ - walletIndex, - setWalletIndex, - wallets, - hideAmounts, - setAddImportVisible, - navigation, -}: any) { - const { translations } = useContext(LocalizationContext); - const items = [{ key: 'spacer-start' }, ...wallets, { key: 'add-wallet' }, { key: 'spacer-end' }]; - const scrollX = React.useRef(new Animated.Value(0)).current; - - return ( - - item.id || item.key} - horizontal - showsHorizontalScrollIndicator={false} - data={items} - disableIntervalMomentum - decelerationRate={'fast'} - bounces={false} - snapToInterval={ITEM_SIZE} - scrollEventThrottle={16} - onScroll={Animated.event([{ nativeEvent: { contentOffset: { x: scrollX } } }], { - useNativeDriver: true, - listener: (event) => { - const offsetX = event.nativeEvent.contentOffset.x; - const index = Math.round(offsetX / ITEM_SIZE); - if (walletIndex !== index) { - setWalletIndex(index); - } - }, - })} - renderItem={({ item, index }) => { - const inputRange = [(index - 2) * ITEM_SIZE, (index - 1) * ITEM_SIZE, index * ITEM_SIZE]; - const scale = scrollX.interpolate({ inputRange, outputRange: [0.8, 1, 0.8] }); - const opacity = scrollX.interpolate({ inputRange, outputRange: [0.6, 1, 0.6] }); - if (item.key && item.key.includes('spacer')) { - return ; - } - return ( - - - - ); - }} - /> - - ); -} - -function WalletTile({ wallet, balances, isWhirlpoolWallet, hideAmounts, isCollaborativeWallet }) { - const { colorMode } = useColorMode(); - const { satsEnabled } = useAppSelector((state) => state.settings); - const { translations } = useContext(LocalizationContext); - const { importWallet } = translations; - return ( - - - - {isWhirlpoolWallet ? ( - - ) : isCollaborativeWallet ? ( - - ) : ( - - {colorMode === 'light' ? : } - - )} - - - {wallet?.type === 'IMPORTED' ? ( - - {importWallet.importedWalletTitle} - - ) : null} - - {wallet?.presentationData?.name} - - - - - - - - - ); -} - -const addNewDefaultWallet = (walletsCount, dispatch) => { - const newWallet: NewWalletInfo = { - walletType: WalletType.DEFAULT, - walletDetails: { - name: `Wallet ${walletsCount + 1} `, - description: `Single-sig Wallet`, - transferPolicy: { - id: uuidv4(), - threshold: defaultTransferPolicyThreshold, - }, - }, - }; - dispatch(addNewWallets([newWallet])); -}; - -function AddImportWallet({ - wallets, - collaborativeWallets, - setAddImportVisible, - setDefaultWalletCreation, - navigation, -}) { - const { colorMode } = useColorMode(); - const dispatch = useDispatch(); - const { translations } = useContext(LocalizationContext); - const { wallet } = translations; - - const addCollaborativeWallet = () => { - setAddImportVisible(false); - const collaborativeWalletsCount = collaborativeWallets.length; - const walletsCount = wallets.length; - if (collaborativeWalletsCount < walletsCount) { - navigation.navigate('SetupCollaborativeWallet', { - coSigner: wallets[collaborativeWalletsCount], - walletId: wallets[collaborativeWalletsCount].id, - collaborativeWalletsCount, - }); - } else { - setDefaultWalletCreation(true); - addNewDefaultWallet(wallets.length, dispatch); - } - }; - - return ( - - { - setAddImportVisible(false); - navigation.navigate('EnterWalletDetail', { - name: `Wallet ${wallets.length + 1}`, - description: 'Single-sig Wallet', - type: WalletType.DEFAULT, - }); - }} - icon={} - title={wallet.addWallet} - subTitle={wallet.addWalletSubTitle} - height={80} - /> - { - setAddImportVisible(false); - navigation.navigate('ImportWallet'); - }} - icon={} - title={wallet.importWalletTitle} - subTitle={wallet.manageWalletSubTitle} - height={80} - /> - } - title={wallet.addCollabWalletTitle} - subTitle={wallet.addCollabWalletSubTitle} - height={80} - /> - - - {wallet.addCollabWalletParagraph} - - - - ); -} - -function ElectrumErrorContent() { - const { colorMode } = useColorMode(); - const { translations } = useContext(LocalizationContext); - const { common } = translations; - return ( - - - {colorMode === 'light' ? : } - - - - {common.changeNetwork} - - - - ); -} - -async function downgradeToPleb(dispatch, app) { - try { - const updatedSubscription: SubScription = { - receipt: '', - productId: SubscriptionTier.L1, - name: SubscriptionTier.L1, - level: AppSubscriptionLevel.L1, - icon: 'assets/ic_pleb.svg', - }; - dbManager.updateObjectById(RealmSchema.KeeperApp, app.id, { - subscription: updatedSubscription, - }); - dispatch(setRecepitVerificationFailed(false)); - const response = await Relay.updateSubscription(app.id, app.publicId, { - productId: SubscriptionTier.L1.toLowerCase(), - }); - } catch (error) { - // - } -} - -function DowngradeModalContent({ navigation, app }) { - const { colorMode } = useColorMode(); - const dispatch = useDispatch(); - const { translations } = useContext(LocalizationContext); - const { common } = translations; - // const app: KeeperApp = useQuery(RealmSchema.KeeperApp).map(getJSONFromRealmObject)[0]; - - return ( - - {colorMode === 'light' ? : } - - { - navigation.navigate('ChoosePlan'); - dispatch(setRecepitVerificationFailed(false)); - }} - activeOpacity={0.5} - > - - {common.viewSubscription} - - - { - downgradeToPleb(dispatch, app); - }} - > - - - - {common.continuePleb} - - - - - - - ); -} - -const WalletsScreen = ({ navigation }) => { - const dispatch = useDispatch(); - const { colorMode } = useColorMode(); - const { translations } = useContext(LocalizationContext); - const { wallet, choosePlan, importWallet, common } = translations; - const { wallets } = useWallets({ getAll: true }); - const { collaborativeWallets } = useCollaborativeWallet(); - const nonHiddenWallets = wallets.filter( - (wallet) => wallet.presentationData.visibility !== VisibilityType.HIDDEN - ); - const allWallets = nonHiddenWallets.concat(collaborativeWallets); - const netBalanceWallets = useAppSelector((state) => state.wallet.netBalance); - const netBalanceCollaborativeWallets = calculateBalancesForVaults(collaborativeWallets); - const [walletIndex, setWalletIndex] = useState(0); - const currentWallet = allWallets[walletIndex]; - const [addImportVisible, setAddImportVisible] = useState(false); - const [electrumErrorVisible, setElectrumErrorVisible] = useState(false); - const { relayWalletUpdate, relayWalletError, realyWalletErrorMessage } = useAppSelector( - (state) => state.bhr - ); - const app: KeeperApp = useQuery(RealmSchema.KeeperApp).map(getJSONFromRealmObject)[0]; - - const [defaultWalletCreation, setDefaultWalletCreation] = useState(false); - - const { showToast } = useToastMessage(); - const { recepitVerificationFailed } = useAppSelector((state) => state.login); - - const electrumClientConnectionStatus = useAppSelector( - (state) => state.login.electrumClientConnectionStatus - ); - const hideAmounts = false; - - useEffect(() => { - if (electrumClientConnectionStatus.success) { - showToast(`Connected to: ${electrumClientConnectionStatus.connectedTo}`, ); - if (electrumErrorVisible) setElectrumErrorVisible(false); - } else if (electrumClientConnectionStatus.failed) { - showToast(`${electrumClientConnectionStatus.error}`, ); - setElectrumErrorVisible(true); - } - }, [electrumClientConnectionStatus.success, electrumClientConnectionStatus.error]); - - useEffect(() => { - if (electrumClientConnectionStatus.setElectrumNotConnectedErr) { - showToast(`${electrumClientConnectionStatus.setElectrumNotConnectedErr}`, ); - dispatch(resetElectrumNotConnectedErr()); - } - }, [electrumClientConnectionStatus.setElectrumNotConnectedErr]); - - useEffect(() => { - if (relayWalletUpdate) { - if (defaultWalletCreation && wallets[collaborativeWallets.length]) { - navigation.navigate('SetupCollaborativeWallet', { - coSigner: wallets[collaborativeWallets.length], - walletId: wallets[collaborativeWallets.length].id, - collaborativeWalletsCount: collaborativeWallets.length, - }); - dispatch(resetRealyWalletState()); - setDefaultWalletCreation(false); - } - } - if (relayWalletError) { - showToast( - realyWalletErrorMessage || 'Something went wrong - Wallet creation failed', - - ); - setDefaultWalletCreation(false); - dispatch(resetRealyWalletState()); - } - }, [relayWalletUpdate, relayWalletError, wallets]); - - return ( - - - - - - {nonHiddenWallets?.length + collaborativeWallets?.length} Wallet - {nonHiddenWallets?.length + collaborativeWallets?.length > 1 && 's'} - - - - - - - setAddImportVisible(true)} - navigation={navigation} - /> - - - {!currentWallet ? ( - - - - - - - Add a new wallet, import it, or create a collaborative wallet. - - - - ) : currentWallet.entityKind === EntityKind.VAULT ? null : ( - : } - title={wallet.whirlpoolUtxoTitle} - subTitle={wallet.whirlpoolUtxoSubTitle} - iconBackColor={`${colorMode}.pantoneGreen`} - onPress={() => { - if (currentWallet) - navigation.navigate('UTXOManagement', { - data: currentWallet, - routeName: 'Wallet', - accountType: WalletType.DEFAULT, - }); - }} - /> - )} - - - {}} - visible={recepitVerificationFailed} - title={choosePlan.validateSubscriptionTitle} - subTitle={choosePlan.validateSubscriptionSubTitle} - Content={() => } - modalBackground={`${colorMode}.modalWhiteBackground`} - subTitleColor={`${colorMode}.secondaryText`} - textColor={`${colorMode}.primaryText`} - subTitleWidth={wp(210)} - showButtons - showCloseIcon={false} - /> - setAddImportVisible(false)} - title={importWallet.AddImportModalTitle} - subTitle={importWallet.AddImportModalSubTitle} - modalBackground={`${colorMode}.modalWhiteBackground`} - subTitleColor={`${colorMode}.secondaryText`} - textColor={`${colorMode}.primaryText`} - DarkCloseIcon={colorMode === 'dark'} - Content={() => ( - - )} - /> - setElectrumErrorVisible(false)} - title={common.connectionError} - subTitle={common.electrumErrorSubTitle} - buttonText={common.continue} - modalBackground={`${colorMode}.modalWhiteBackground`} - subTitleColor={`${colorMode}.secondaryText`} - textColor={`${colorMode}.primaryText`} - buttonTextColor="light.white" - DarkCloseIcon={colorMode === 'dark'} - buttonCallback={() => setElectrumErrorVisible(false)} - Content={ElectrumErrorContent} - /> - - ); -}; - -export default WalletsScreen; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#F7F2EC', - paddingVertical: 15, - paddingHorizontal: 20, - position: 'relative', - justifyContent: 'flex-end', - }, - titleWrapper: { - flexDirection: 'row', - width: '100%', - alignItems: 'center', - marginVertical: hp(20), - justifyContent: 'space-between', - }, - titleText: { - fontSize: 16, - fontFamily: Fonts.FiraSansCondensedMedium, - }, - subTitleText: { - fontSize: 12, - }, - addWalletContainer: { - justifyContent: 'center', - alignItems: 'center', - height: '100%', - }, - center: { - justifyContent: 'center', - alignItems: 'center', - }, - walletContainer: { - borderRadius: hp(10), - height: hp(210), - padding: '10%', - justifyContent: 'flex-end', - }, - addWalletText: { - fontSize: 14, - textAlign: 'center', - }, - walletCard: { - paddingTop: windowHeight > 680 ? hp(20) : 0, - }, - walletInnerView: { - width: wp(170), - }, - walletDescription: { - letterSpacing: 0.2, - fontSize: 13, - }, - walletName: { - letterSpacing: 0.2, - fontSize: 14, - fontWeight: '400', - }, - walletType: { - letterSpacing: 0.2, - fontSize: 11, - fontWeight: '400', - }, - walletBalance: { - marginTop: hp(12), - }, - border: { - borderWidth: 0.5, - borderRadius: 20, - opacity: 0.2, - }, - - unconfirmedText: { - fontSize: 11, - letterSpacing: 0.72, - textAlign: 'right', - }, - unconfirmedBalance: { - fontSize: 17, - letterSpacing: 0.6, - alignSelf: 'flex-end', - }, - availableBalance: { - fontSize: hp(24), - letterSpacing: 1.2, - lineHeight: hp(30), - }, - walletDetailsWrapper: { - marginTop: 5, - width: '68%', - }, - listItemsWrapper: { - marginTop: hp(20), - width: '99%', - }, - whirlpoolListItemWrapper: { - width: '99%', - }, - titleInfoView: { - width: '60%', - }, - netBalanceView: { - width: '40%', - alignItems: 'flex-end', - }, - AddNewWalletIllustrationWrapper: { - flexDirection: 'row', - alignItems: 'center', - marginLeft: wp(30), - marginTop: hp(20), - width: '100%', - }, - addNewWallIconWrapper: { - marginRight: wp(10), - alignItems: 'flex-start', - }, - addNewWallTextWrapper: { - width: '50%', - justifyContent: 'center', - }, - addNewWallText: { - fontSize: 14, - }, - cancelBtn: { - marginRight: wp(20), - borderRadius: 10, - }, - btnText: { - fontSize: 12, - letterSpacing: 0.84, - }, - createBtn: { - paddingVertical: hp(15), - borderRadius: 10, - paddingHorizontal: 20, - }, - addImportParaContent: { - fontSize: 13, - padding: 2, - marginTop: hp(20), - }, - walletIconWrapper: { - marginVertical: hp(5), - }, -}); diff --git a/src/screens/HomeScreen/components/BalanceToggle.tsx b/src/screens/HomeScreen/components/BalanceToggle.tsx deleted file mode 100644 index 4496bf8f8..000000000 --- a/src/screens/HomeScreen/components/BalanceToggle.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { StyleSheet, Text, TouchableOpacity } from 'react-native'; -import React from 'react'; -import { hp } from 'src/constants/responsive'; -import HideIcon from 'src/assets/images/icon_hide.svg'; -import ShowIcon from 'src/assets/images/icon_show.svg'; - -function BalanceToggle({ hideAmounts, setHideAmounts }) { - const toggleCurrencyVisibility = () => setHideAmounts(!hideAmounts); - return ( - - {hideAmounts ? : } - -   {`${hideAmounts ? 'SHOW' : 'HIDE'} BALANCES`} - - - ); -} - -export default BalanceToggle; - -const styles = StyleSheet.create({ - hideBalanceText: { - fontSize: 10, - }, - hideBalanceWrapper: { - alignSelf: 'flex-end', - flexDirection: 'row', - alignItems: 'center', - marginVertical: hp(10), - height: hp(20), - }, -}); diff --git a/src/screens/HomeScreen/components/HeaderDetails/components/CurrentPlanView.tsx b/src/screens/HomeScreen/components/HeaderDetails/components/CurrentPlanView.tsx deleted file mode 100644 index 838ac638a..000000000 --- a/src/screens/HomeScreen/components/HeaderDetails/components/CurrentPlanView.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { Box, useColorMode } from 'native-base'; -import Text from 'src/components/KeeperText'; -import { StyleSheet } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; -import { useNavigation } from '@react-navigation/native'; -import Fonts from 'src/constants/Fonts'; - -function CurrentPlanView({ plan }) { - const navigation = useNavigation(); - const { colorMode } = useColorMode(); - - return ( - - - You are at - - navigation.navigate('ChoosePlan')} testID="btn_choosePlan"> - - {plan} - - - View subscription details - - - - ); -} -const styles = StyleSheet.create({ - wrapper: { - borderBottomWidth: 0.8, - paddingVertical: 10, - }, - titleTxet: { - fontSize: 12, - }, - currentPlanText: { - fontSize: 18, - letterSpacing: 1.8, - fontFamily: Fonts.FiraSansCondensedMedium, - }, -}); -export default CurrentPlanView; diff --git a/src/screens/HomeScreen/components/HeaderDetails/index.tsx b/src/screens/HomeScreen/components/HeaderDetails/index.tsx deleted file mode 100644 index 9419be092..000000000 --- a/src/screens/HomeScreen/components/HeaderDetails/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { StyleSheet } from 'react-native'; -import React from 'react'; -import { Box, useColorMode } from 'native-base'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import UaiDisplay from 'src/screens/HomeScreen/UaiDisplay'; -import useUaiStack from 'src/hooks/useUaiStack'; -import usePlan from 'src/hooks/usePlan'; - -import HeaderBar from './components/HeaderBar'; -import CurrentPlanView from './components/CurrentPlanView'; - -function HeaderDetails() { - const { colorMode } = useColorMode(); - const { top } = useSafeAreaInsets(); - const { uaiStack } = useUaiStack(); - const { plan } = usePlan(); - - return ( - - - - - - ); -} - -export default HeaderDetails; - -const styles = StyleSheet.create({ - wrapper: { - paddingHorizontal: 30, - paddingVertical: 30, - borderBottomEndRadius: 10, - borderBottomStartRadius: 10, - }, -}); diff --git a/src/screens/HomeScreen/components/ListItemView.tsx b/src/screens/HomeScreen/components/ListItemView.tsx deleted file mode 100644 index 962f0224e..000000000 --- a/src/screens/HomeScreen/components/ListItemView.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { Box, Pressable, useColorMode } from 'native-base'; -import Text from 'src/components/KeeperText'; -import { StyleSheet } from 'react-native'; -import { hp, windowHeight } from 'src/constants/responsive'; -import Fonts from 'src/constants/Fonts'; - -function ListItemView(props) { - const { colorMode } = useColorMode(); - return ( - - - - {props.icon} - - - - - {props.title} - - - {props.subTitle} - - - - ); -} -const styles = StyleSheet.create({ - wrapper: { - paddingVertical: windowHeight > 680 ? 25 : 10, - paddingHorizontal: 18, - width: '100%', - borderRadius: 10, - marginVertical: hp(5), - }, - iconView: { - borderRadius: 100, - height: windowHeight > 680 ? 35 : 33, - width: windowHeight > 680 ? 35 : 33, - alignItems: 'center', - justifyContent: 'center', - }, - titleWrapper: { - marginTop: 10, - }, - titleText: { - letterSpacing: 1.04, - fontSize: 13, - fontFamily: Fonts.FiraSansCondensedMedium, - }, - subTitleText: { - fontSize: 12, - flexWrap: 'wrap', - width: '100%', - }, -}); -export default ListItemView; diff --git a/src/screens/ImportWalletDetailsScreen/AddDetailsFinalScreen.tsx b/src/screens/ImportWalletDetailsScreen/AddDetailsFinalScreen.tsx index c1ea05657..85d13da31 100644 --- a/src/screens/ImportWalletDetailsScreen/AddDetailsFinalScreen.tsx +++ b/src/screens/ImportWalletDetailsScreen/AddDetailsFinalScreen.tsx @@ -1,9 +1,4 @@ -import { - KeyboardAvoidingView, - Platform, - StyleSheet, - TouchableOpacity, -} from 'react-native'; +import { KeyboardAvoidingView, Platform, StyleSheet, TouchableOpacity } from 'react-native'; import { Box, Text, View, useColorMode, ScrollView, Input } from 'native-base'; import React, { useCallback, useContext, useEffect, useState } from 'react'; import { hp, windowHeight, windowWidth, wp } from 'src/constants/responsive'; @@ -29,6 +24,12 @@ import TickIcon from 'src/assets/images/icon_tick.svg'; import ToastErrorIcon from 'src/assets/images/toast_error.svg'; import { v4 as uuidv4 } from 'uuid'; +const derivationPurposeToLabel = { + [DerivationPurpose.BIP84]: 'P2WPKH: native segwit, single-sig', + [DerivationPurpose.BIP49]: 'P2SH-P2WPKH: wrapped segwit, single-sig', + [DerivationPurpose.BIP44]: 'P2PKH: legacy, single-sig', +}; + function AddDetailsFinalScreen({ route }) { const navigation = useNavigation(); const { showToast } = useToastMessage(); @@ -37,24 +38,25 @@ function AddDetailsFinalScreen({ route }) { const { translations } = useContext(LocalizationContext); const { home, importWallet } = translations; const [arrow, setArrow] = useState(false); + + const { importedKey, importedKeyDetails } = route.params; + const [walletType, setWalletType] = useState(route.params?.type); + const [walletName, setWalletName] = useState(route.params?.name); + const [walletDescription, setWalletDescription] = useState(route.params?.description); + const [transferPolicy, setTransferPolicy] = useState(route.params?.policy); + const [showPurpose, setShowPurpose] = useState(false); const [purposeList, setPurposeList] = useState([ - { label: 'P2PKH: legacy, single-sig', value: DerivationPurpose.BIP44 }, - { label: 'P2SH-P2WPKH: wrapped segwit, single-sg', value: DerivationPurpose.BIP49 }, { label: 'P2WPKH: native segwit, single-sig', value: DerivationPurpose.BIP84 }, + { label: 'P2SH-P2WPKH: wrapped segwit, single-sig', value: DerivationPurpose.BIP49 }, + { label: 'P2PKH: legacy, single-sig', value: DerivationPurpose.BIP44 }, ]); - const [purpose, setPurpose] = useState(`${DerivationPurpose.BIP84}`); - const [purposeLbl, setPurposeLbl] = useState('P2PKH: legacy, single-sig'); + const [purpose, setPurpose] = useState(importedKeyDetails?.purpose || DerivationPurpose.BIP84); + const [purposeLbl, setPurposeLbl] = useState(derivationPurposeToLabel[purpose]); const [path, setPath] = useState( - route.params?.path - ? route.params?.path - : WalletUtilities.getDerivationPath(EntityKind.WALLET, config.NETWORK_TYPE, 0, purpose) + route.params?.path || + WalletUtilities.getDerivationPath(EntityKind.WALLET, config.NETWORK_TYPE, 0, purpose) ); - const [walletType, setWalletType] = useState(route.params?.type); - const [importedSeed, setImportedSeed] = useState(route.params?.seed); - const [walletName, setWalletName] = useState(route.params?.name); - const [walletDescription, setWalletDescription] = useState(route.params?.description); - const [transferPolicy, setTransferPolicy] = useState(route.params?.policy); const { relayWalletUpdateLoading, relayWalletUpdate, relayWalletError } = useAppSelector( (state) => state.bhr ); @@ -66,7 +68,7 @@ function AddDetailsFinalScreen({ route }) { EntityKind.WALLET, config.NETWORK_TYPE, 0, - Number(purpose) + purpose ); setPath(path); }, [purpose]); @@ -77,7 +79,7 @@ function AddDetailsFinalScreen({ route }) { setTimeout(() => { const derivationConfig: DerivationConfig = { path, - purpose: Number(purpose), + purpose, }; const newWallet: NewWalletInfo = { @@ -92,8 +94,9 @@ function AddDetailsFinalScreen({ route }) { }, }, importDetails: { + importedKey, + importedKeyDetails, derivationConfig, - mnemonic: importedSeed, }, }; dispatch(addNewWallets([newWallet])); @@ -154,7 +157,9 @@ function AddDetailsFinalScreen({ route }) { - {purposeLbl} + + {purposeLbl} + {showPurpose && ( - + {purposeList.map((item) => ( state.settings.currencyKind); + const [walletName, setWalletName] = useState(name || ''); + const [walletDescription, setWalletDescription] = useState(description || ''); const onNextClick = () => { navigation.navigate('AddDetailsFinal', { - type: walletType, - description, + type, // walletType + description: walletDescription, name: walletName, - seed: importedSeed, + importedKey, + importedKeyDetails, policy: transferPolicy, }); }; - const formatNumber = (value: string) => - value.replace(/\D/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ','); - const { colorMode } = useColorMode(); return ( @@ -71,7 +52,7 @@ function ImportWalletDetailsScreen({ route }) { style={styles.textInput} borderWidth="0" backgroundColor={`${colorMode}.seashellWhite`} - value={walletName} + value={name} onChangeText={(text) => setWalletName(text)} /> @@ -82,41 +63,9 @@ function ImportWalletDetailsScreen({ route }) { borderWidth="0" backgroundColor={`${colorMode}.seashellWhite`} value={description} - onChangeText={(text) => setDescription(text)} + onChangeText={(text) => setWalletDescription(text)} /> - {importWallet.autoTransfer} - - - {getCurrencyImageByRegion(currencyCode, 'dark', currentCurrency, colorMode === 'light' ? BitcoinInput : BitcoinWhite)} - - - - - {importWallet.walletBalance} - @@ -250,10 +199,10 @@ const styles = StyleSheet.create({ }, amountWrapper: { marginTop: hp(10), - flexDirection: "row", + flexDirection: 'row', marginHorizontal: 2, - alignItems: "center", - borderRadius: 5 + alignItems: 'center', + borderRadius: 5, }, balanceCrossesText: { width: '100%', diff --git a/src/screens/ImportWalletScreen/ImportWalletScreen.tsx b/src/screens/ImportWalletScreen/ImportWalletScreen.tsx index 75bfbd593..401de70c1 100644 --- a/src/screens/ImportWalletScreen/ImportWalletScreen.tsx +++ b/src/screens/ImportWalletScreen/ImportWalletScreen.tsx @@ -21,16 +21,15 @@ import { WalletType } from 'src/core/wallets/enums'; import { RealmSchema } from 'src/storage/realm/enum'; import { getJSONFromRealmObject } from 'src/storage/realm/utils'; import { useQuery } from '@realm/react'; +import WalletUtilities from 'src/core/wallets/operations/utils'; -function ImportWalletScreen({ route }) { +function ImportWalletScreen() { const { colorMode } = useColorMode(); const navigation = useNavigation(); const { showToast } = useToastMessage(); const { translations } = useContext(LocalizationContext); - const { common, importWallet, home } = translations; - // const { sender } = route.params as { sender: Wallet | Vault }; - // const network = WalletUtilities.getNetworkByType(sender.networkType); + const { common, importWallet, wallet } = translations; const wallets: Wallet[] = useQuery(RealmSchema.Wallet).map(getJSONFromRealmObject) || []; const handleChooseImage = () => { @@ -56,7 +55,7 @@ function ImportWalletScreen({ route }) { } else { QRreader(response.assets[0].uri) .then((data) => { - handleTextChange(data); + initiateWalletImport(data); }) .catch((err) => { showToast('Invalid or No related QR code'); @@ -65,16 +64,23 @@ function ImportWalletScreen({ route }) { }); }; - const handleTextChange = (info: string) => { - info = info.trim(); - navigation.navigate('ImportWalletDetails', { - seed: info, - type: WalletType.IMPORTED, - name: `Wallet ${wallets.length + 1}`, - description: 'Single-sig Wallet', - }); + const initiateWalletImport = (data: string) => { + try { + const importedKey = data.trim(); + const importedKeyDetails = WalletUtilities.getImportedKeyDetails(importedKey); + navigation.navigate('ImportWalletDetails', { + importedKey, + importedKeyDetails, + type: WalletType.IMPORTED, + name: `Wallet ${wallets.length + 1}`, + description: importedKeyDetails.watchOnly ? 'Watch Only' : 'Imported Wallet', + }); + } catch (err) { + showToast('Invalid Import Key'); + } }; + //TODO: add learn more modal return ( - + @@ -91,7 +103,7 @@ function ImportWalletScreen({ route }) { style={styles.cameraView} captureAudio={false} onBarCodeRead={(data) => { - handleTextChange(data.data); + initiateWalletImport(data.data); }} notAuthorizedView={} /> diff --git a/src/screens/Inheritance/IKSAddEmailPhone.tsx b/src/screens/Inheritance/IKSAddEmailPhone.tsx index d4c199b18..bfbf4ab82 100644 --- a/src/screens/Inheritance/IKSAddEmailPhone.tsx +++ b/src/screens/Inheritance/IKSAddEmailPhone.tsx @@ -2,37 +2,47 @@ import React, { useState } from 'react'; import ScreenWrapper from 'src/components/ScreenWrapper'; import KeeperHeader from 'src/components/KeeperHeader'; import { useNavigation } from '@react-navigation/native'; -import { Box, Input } from 'native-base'; +import { Box, Input, useColorMode } from 'native-base'; import Buttons from 'src/components/Buttons'; -import { StyleSheet } from 'react-native'; +import { StyleSheet, Text } from 'react-native'; + import { hp } from 'src/constants/responsive'; -import { Vault, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Signer, Vault } from 'src/core/wallets/interfaces/vault'; import useVault from 'src/hooks/useVault'; import { SignerType } from 'src/core/wallets/enums'; import { InheritancePolicy } from 'src/services/interfaces'; -import idx from 'idx'; import InheritanceKeyServer from 'src/services/operations/InheritanceKey'; import useToastMessage from 'src/hooks/useToastMessage'; import { captureError } from 'src/services/sentry'; -import { RealmSchema } from 'src/storage/realm/enum'; -import dbManager from 'src/storage/realm/dbManager'; import TickIcon from 'src/assets/images/icon_tick.svg'; +import useSignerMap from 'src/hooks/useSignerMap'; +import { useDispatch } from 'react-redux'; +import { updateSignerDetails } from 'src/store/sagaActions/wallets'; +import { emailCheck } from 'src/utils/utilities'; -function IKSAddEmailPhone() { +function IKSAddEmailPhone({ route }) { const navigtaion = useNavigation(); const [email, setEmail] = useState(''); - const vault: Vault = useVault().activeVault; + const [emailStatusFail, setEmailStatusFail] = useState(false); + const { vaultId } = route.params; + const vault: Vault = useVault({ vaultId }).activeVault; const { showToast } = useToastMessage(); + const { signerMap } = useSignerMap() as { signerMap: { [key: string]: Signer } }; + const dispatch = useDispatch(); + const [ikVaultKey] = vault.signers.filter( + (vaultKey) => signerMap[vaultKey.masterFingerprint].type === SignerType.INHERITANCEKEY + ); + const { colorMode } = useColorMode(); const updateIKSPolicy = async (email: string) => { try { - const [ikSigner] = vault.signers.filter( - (signer) => signer.type === SignerType.INHERITANCEKEY - ); - const thresholdDescriptors = vault.signers.map((signer) => signer.signerId).slice(0, 2); + const thresholdDescriptors = vault.signers.map((signer) => signer.xfp).slice(0, 2); + const IKSigner = signerMap[ikVaultKey.masterFingerprint]; + + if (IKSigner.inheritanceKeyInfo === undefined) + showToast('Something went wrong, IKS configuration missing', ); - const existingPolicy: InheritancePolicy | any = - idx(ikSigner, (_) => _.inheritanceKeyInfo.policy) || {}; + const existingPolicy: InheritancePolicy = IKSigner.inheritanceKeyInfo.policy; const updatedPolicy: InheritancePolicy = { ...existingPolicy, @@ -42,29 +52,18 @@ function IKSAddEmailPhone() { }; const { updated } = await InheritanceKeyServer.updateInheritancePolicy( - ikSigner.signerId, - { - alert: updatedPolicy.alert, - }, + ikVaultKey.xfp, + updatedPolicy, thresholdDescriptors ); if (updated) { - const updatedIKSigner: VaultSigner = { - ...ikSigner, - inheritanceKeyInfo: { - ...ikSigner.inheritanceKeyInfo, - policy: updatedPolicy, - }, + const updateInheritanceKeyInfo = { + ...IKSigner.inheritanceKeyInfo, + policy: updatedPolicy, }; - const updatedSigners = vault.signers.map((signer) => { - if (signer.type === SignerType.INHERITANCEKEY) return updatedIKSigner; - return signer; - }); - dbManager.updateObjectById(RealmSchema.Vault, vault.id, { - signers: updatedSigners, - }); + dispatch(updateSignerDetails(IKSigner, 'inheritanceKeyInfo', updateInheritanceKeyInfo)); showToast('Email added', ); navigtaion.goBack(); } else showToast('Failed to add email'); @@ -76,28 +75,48 @@ function IKSAddEmailPhone() { return ( - + + + + Consent Note: + + + By providing your email address/phone number, you consent to us using this information to + send you alerts and notifications about Inheritance Key requests, notify you of account + activity, and contact you for customer support purposes if needed. You can withdraw your + consent at any time by disabling this from App settings or clicking the unsubscribe link + in our emails. We will protect your data as outlined in our privacy policy.{' '} + { - updateIKSPolicy(email); + if (!emailCheck(email)) { + setEmailStatusFail(true); + } else { + updateIKSPolicy(email); + } }} /> @@ -113,5 +132,8 @@ const styles = StyleSheet.create({ fontSize: 14, paddingLeft: 5, }, + consentNotes: { fontWeight: '500' }, + notesDescription: { marginVertical: 10, fontSize: 12, lineHeight: 20 }, + errorStyle: { marginTop: 10 }, }); export default IKSAddEmailPhone; diff --git a/src/screens/Inheritance/InheritanceStatus.tsx b/src/screens/Inheritance/InheritanceStatus.tsx index ccbff7388..9a8409a79 100644 --- a/src/screens/Inheritance/InheritanceStatus.tsx +++ b/src/screens/Inheritance/InheritanceStatus.tsx @@ -18,7 +18,6 @@ import Recovery from 'src/assets/images/recovery.svg'; import ToastErrorIcon from 'src/assets/images/toast_error.svg'; import TickIcon from 'src/assets/images/icon_tick.svg'; import Text from 'src/components/KeeperText'; -// import Note from 'src/components/Note/Note'; import { hp, windowHeight, wp } from 'src/constants/responsive'; import useToastMessage from 'src/hooks/useToastMessage'; import useVault from 'src/hooks/useVault'; @@ -27,12 +26,15 @@ import GenerateRecoveryInstrPDF from 'src/utils/GenerateRecoveryInstrPDF'; import { genrateOutputDescriptors } from 'src/core/utils'; import GenerateSecurityTipsPDF from 'src/utils/GenerateSecurityTipsPDF'; import GenerateLetterToAtternyPDF from 'src/utils/GenerateLetterToAtternyPDF'; +import KeeperHeader from 'src/components/KeeperHeader'; import IKSetupSuccessModal from './components/IKSetupSuccessModal'; import InheritanceDownloadView from './components/InheritanceDownloadView'; import InheritanceSupportView from './components/InheritanceSupportView'; -import KeeperHeader from 'src/components/KeeperHeader'; +import useSignerMap from 'src/hooks/useSignerMap'; +import { Signer } from 'src/core/wallets/interfaces/vault'; -function InheritanceStatus() { +function InheritanceStatus({ route }) { + const { vaultId } = route.params; const { colorMode } = useColorMode(); const { showToast } = useToastMessage(); const navigtaion = useNavigation(); @@ -43,18 +45,19 @@ function InheritanceStatus() { const [visibleModal, setVisibleModal] = useState(false); const [visibleErrorView] = useState(false); - const { activeVault } = useVault(); + const { activeVault } = useVault({ vaultId, getFirst: true }); const fingerPrints = activeVault.signers.map((signer) => signer.masterFingerprint); const descriptorString = genrateOutputDescriptors(activeVault); const [isSetupDone, setIsSetupDone] = useState(false); + const { signerMap } = useSignerMap() as { signerMap: { [key: string]: Signer } }; useEffect(() => { if (activeVault && activeVault.signers) { - const [ikSigner] = activeVault.signers.filter( - (signer) => signer.type === SignerType.INHERITANCEKEY + const [ikVaultKey] = activeVault.signers.filter( + (vaultKey) => signerMap[vaultKey.masterFingerprint].type === SignerType.INHERITANCEKEY ); - if (ikSigner) setIsSetupDone(true); + if (ikVaultKey) setIsSetupDone(true); else setIsSetupDone(false); } }, [activeVault]); @@ -68,10 +71,11 @@ function InheritanceStatus() { learnMorePressed={() => { dispatch(setInheritance(true)); }} + learnTextColor={`${colorMode}.white`} /> @@ -83,23 +87,9 @@ function InheritanceStatus() { subTitle={ disableInheritance ? 'Please create a 3 of 5 vault to proceed with adding inheritance support' - : 'Add an assisted key to create a 3 of 6 Vault' + : 'Add an assisted key to create a 3 of 6 vault' } - isSetupDone={isSetupDone} - onPress={() => { - if (isSetupDone) { - showToast('You have successfully added the Inheritance Key.', ); - return; - } - navigtaion.dispatch( - CommonActions.navigate({ - name: 'AddSigningDevice', - merge: true, - params: { isInheritance: true }, - }) - ); - }} - disableCallback={disableInheritance} + disableCallback={true} /> Tips @@ -120,7 +110,7 @@ function InheritanceStatus() { {/* Error view - Need to add condition for this */} {visibleErrorView && ( - Signing Devices have been changed  + Signers have been changed  )} diff --git a/src/screens/Inheritance/SetupInheritance.tsx b/src/screens/Inheritance/SetupInheritance.tsx index 2f2c3ce9d..9b51d4a6a 100644 --- a/src/screens/Inheritance/SetupInheritance.tsx +++ b/src/screens/Inheritance/SetupInheritance.tsx @@ -2,7 +2,7 @@ import React, { useContext } from 'react'; import Text from 'src/components/KeeperText'; import { Box, useColorMode } from 'native-base'; -import { CommonActions, useNavigation } from '@react-navigation/native'; +import { useNavigation } from '@react-navigation/native'; import { wp, hp, windowHeight } from 'src/constants/responsive'; import KeeperHeader from 'src/components/KeeperHeader'; import Note from 'src/components/Note/Note'; @@ -17,53 +17,46 @@ import Recovery from 'src/assets/images/recovery.svg'; import Inheritance from 'src/assets/images/icon_inheritance.svg'; import ScreenWrapper from 'src/components/ScreenWrapper'; import openLink from 'src/utils/OpenLink'; -import { SubscriptionTier } from 'src/models/enums/SubscriptionTier'; -import usePlan from 'src/hooks/usePlan'; import { StyleSheet, TouchableOpacity } from 'react-native'; -import useVault from 'src/hooks/useVault'; import GradientIcon from 'src/screens/WalletDetails/components/GradientIcon'; import { KEEPER_KNOWLEDGEBASE } from 'src/core/config'; import { LocalizationContext } from 'src/context/Localization/LocContext'; -function SetupInheritance() { +function SetupInheritance({ route }) { const { colorMode } = useColorMode(); const navigtaion = useNavigation(); const { translations } = useContext(LocalizationContext); const { inheritence, vault: vaultTranslation, common } = translations; const dispatch = useAppDispatch(); const introModal = useAppSelector((state) => state.settings.inheritanceModal); - const { plan } = usePlan(); - const { activeVault } = useVault(); - - const shouldActivateInheritance = () => plan === SubscriptionTier.L3.toUpperCase() && activeVault; - + const { vaultId = '' } = route.params || {}; const inheritanceData = [ { title: 'Safeguarding Tips', subTitle: 'For yourself', description: - 'Consists of tips on things to consider while storing your signing devices for the purpose of inheritance (when it will be needed by someone else)', + 'Consists of tips on things to consider while storing your signers for the purpose of inheritance (when it will be needed by someone else)', Icon: Vault, }, { title: 'Setup Inheritance Key', subTitle: 'Keeper will have one of your Keys', description: - 'This would transform your 3-of-5 Vault to a 3-of-6 with Keeper custodying one key.', + 'This would transform your 3-of-5 vault to a 3-of-6 with Keeper custodying one key.', Icon: LetterIKS, }, { title: 'Letter to the Attorney', subTitle: 'For the estate management company', description: - 'A partly pre-filled pdf template uniquely identifying the Vault and ability to add the beneficiary details', + 'A partly pre-filled pdf template uniquely identifying the vault and ability to add the beneficiary details', Icon: Letter, }, { title: 'Recovery Instructions', subTitle: 'For the heir or beneficiary', description: - 'A document that will help the beneficiary recover the Vault with or without the Keeper app', + 'A document that will help the beneficiary recover the vault with or without the Keeper app', Icon: Recovery, }, ]; @@ -119,23 +112,7 @@ function SetupInheritance() { const proceedCallback = () => { dispatch(setInheritance(false)); - if (shouldActivateInheritance()) navigtaion.navigate('InheritanceStatus'); - }; - - const toSetupInheritance = () => { - if (shouldActivateInheritance()) navigtaion.navigate('InheritanceStatus'); - else if (plan !== SubscriptionTier.L3.toUpperCase()) - navigtaion.navigate('ChoosePlan', { planPosition: 2 }); - else if (!activeVault) - navigtaion.dispatch( - CommonActions.navigate({ - name: 'AddSigningDevice', - merge: true, - params: { scheme: { m: 3, n: 5 } }, - }) - ); - else if (activeVault.scheme.m !== 3 || activeVault.scheme.n !== 5) - navigtaion.dispatch(CommonActions.navigate({ name: 'VaultSetup' })); + navigtaion.navigate('InheritanceStatus', { vaultId }); }; return ( @@ -145,6 +122,7 @@ function SetupInheritance() { learnMorePressed={() => { dispatch(setInheritance(true)); }} + learnTextColor={`${colorMode}.white`} /> 600 ? 50 : 20} /> @@ -166,19 +144,17 @@ function SetupInheritance() { - {shouldActivateInheritance() - ? vaultTranslation.manageInheritance - : `This can be activated once you are on ${SubscriptionTier.L3} and create a 3 of 5 Vault to add this key`} + {vaultTranslation.manageInheritance} 700 ? hp(50) : hp(20) }} testID="btn_ISContinue"> - toSetupInheritance()}> + proceedCallback()}> - {shouldActivateInheritance() ? common.proceed : common.upgradeNow} + {common.proceed} diff --git a/src/screens/Inheritance/components/IKSetupSuccessModal.tsx b/src/screens/Inheritance/components/IKSetupSuccessModal.tsx index 730410869..32eaf3284 100644 --- a/src/screens/Inheritance/components/IKSetupSuccessModal.tsx +++ b/src/screens/Inheritance/components/IKSetupSuccessModal.tsx @@ -15,8 +15,8 @@ function InitiateContent() { - When a Signing Device is changed and a new Vault is created, some aspects of Inheritance - documents may change + When a signer is changed and a new vault is created, some aspects of Inheritance documents + may change ); @@ -29,7 +29,7 @@ function IKSetupSuccessModal({ visible, closeModal }: modalParams) { close={() => closeModal()} title="Inheritance Support Setup Successful" subTitle="You have visited all sections of the Inheritance Support feature" - modalBackground={'#F7F2EC'} + modalBackground="#F7F2EC" buttonBackground={`${colorMode}.gradientStart`} buttonText="View Inhetitance" buttonTextColor="#FAFAFA" diff --git a/src/screens/LoginScreen/CreatePin.tsx b/src/screens/LoginScreen/CreatePin.tsx index 0163d1164..e24f8c61f 100644 --- a/src/screens/LoginScreen/CreatePin.tsx +++ b/src/screens/LoginScreen/CreatePin.tsx @@ -15,6 +15,8 @@ import { LocalizationContext } from 'src/context/Localization/LocContext'; import PinInputsView from 'src/components/AppPinInput/PinInputsView'; import DeleteIcon from 'src/assets/images/deleteLight.svg'; import DowngradeToPleb from 'src/assets/images/downgradetopleb.svg'; +import Passwordlock from 'src/assets/images/passwordlock.svg'; + import { storeCreds, switchCredsChanged } from 'src/store/sagaActions/login'; import KeeperModal from 'src/components/KeeperModal'; @@ -25,7 +27,7 @@ export default function CreatePin(props) { const [passcode, setPasscode] = useState(''); const [confirmPasscode, setConfirmPasscode] = useState(''); const [passcodeFlag, setPasscodeFlag] = useState(true); - const [createPassword, setCreatePassword] = useState(false) + const [createPassword, setCreatePassword] = useState(false); const [confirmPasscodeFlag, setConfirmPasscodeFlag] = useState(0); const { oldPasscode } = props.route.params || {}; const dispatch = useAppDispatch(); @@ -148,8 +150,11 @@ export default function CreatePin(props) { function CreatePassModalContent() { return ( + + + - Your app storage is encrypted by the passcode. You will not be able to log in if you forget the passcode and will have to recover your wallet using the recovery flow + You would be locked out of the app if you forget your passcode and will have to recover it ); @@ -178,7 +183,7 @@ export default function CreatePin(props) { borderColor={ passcode !== confirmPasscode && confirmPasscode.length === 4 ? // ? '#FF8F79' - `light.error` + 'light.error' : 'transparent' } /> @@ -234,21 +239,26 @@ export default function CreatePin(props) { { }} - title={''} - subTitle={''} + close={() => {}} + title="Remember your passcode" + subTitle="Please remember your passcode and backup your wallet by writing down the 12-word Recovery + Phrase" modalBackground={`${colorMode}.modalWhiteBackground`} - subTitleColor={`${colorMode}.secondaryText`} + subTitleColor={`${colorMode}.SlateGrey`} textColor={`${colorMode}.modalGreenTitle`} showCloseIcon={false} - buttonText={'Continue'} + buttonText="Continue" + secondaryButtonText="Back" buttonCallback={() => { dispatch(storeCreds(passcode)); setCreatePassword(false); }} + secondaryCallback={() => { + setCreatePassword(false); + }} Content={CreatePassModalContent} showButtons - subTitleWidth={wp(250)} + subTitleWidth={wp(60)} /> ); @@ -271,11 +281,11 @@ const styles = StyleSheet.create({ fontSize: 22, }, labelText: { - fontSize: 12, + fontSize: 14, marginLeft: 18, }, errorText: { - fontSize: 10, + fontSize: 11, fontWeight: '400', width: wp('68%'), textAlign: 'right', @@ -290,6 +300,9 @@ const styles = StyleSheet.create({ modalMessageText: { fontSize: 13, letterSpacing: 0.65, - // width: wp(275), + }, + passImg: { + alignItems: 'center', + paddingVertical: 20, }, }); diff --git a/src/screens/LoginScreen/Login.tsx b/src/screens/LoginScreen/Login.tsx index c12aad2c9..e415d0272 100644 --- a/src/screens/LoginScreen/Login.tsx +++ b/src/screens/LoginScreen/Login.tsx @@ -517,11 +517,11 @@ function LoginScreen({ navigation, route }) { { }} + close={() => {}} title={modelTitle} subTitle={modelSubTitle} modalBackground={`${colorMode}.modalWhiteBackground`} - subTitleColor={`${colorMode}.secondaryText`} + subTitleColor={`${colorMode}.SlateGrey`} textColor={`${colorMode}.modalGreenTitle`} buttonBackground={`${colorMode}.greenButtonBackground`} showCloseIcon={false} @@ -534,7 +534,7 @@ function LoginScreen({ navigation, route }) { { }} + close={() => {}} visible={recepitVerificationError} title="Something went wrong" subTitle="Please check your internet connection and try again." @@ -548,7 +548,7 @@ function LoginScreen({ navigation, route }) { /> { }} + close={() => {}} title={'Incorrect Password'} subTitle={ 'You have entered an incorrect passcode. Please, try again. If you don’t remember your passcode, you will have to recover your wallet through the recovery flow' @@ -646,7 +646,6 @@ const styles = StyleSheet.create({ modalMessageText: { fontSize: 13, letterSpacing: 0.65, - // width: wp(275), }, modalMessageWrapper: { flexDirection: 'row', diff --git a/src/screens/Mix/BroadcastPremix.tsx b/src/screens/Mix/BroadcastPremix.tsx index d74c4dd61..d276fa4fb 100644 --- a/src/screens/Mix/BroadcastPremix.tsx +++ b/src/screens/Mix/BroadcastPremix.tsx @@ -36,6 +36,7 @@ import { bulkUpdateUTXOLabels } from 'src/store/sagaActions/utxos'; import { genrateOutputDescriptors } from 'src/core/utils'; import SwiperModal from './components/SwiperModal'; import UtxoSummary from './UtxoSummary'; +import { CommonActions } from '@react-navigation/native'; export default function BroadcastPremix({ route, navigation }) { const { @@ -237,11 +238,17 @@ export default function BroadcastPremix({ route, navigation }) { const navigateToWalletDetails = () => { setShowBroadcastModal(false); - navigation.navigate('UTXOManagement', { - data: depositWallet, - routeName: 'Wallet', - accountType: WalletType.PRE_MIX, - }); + navigation.dispatch( + CommonActions.navigate({ + name: 'UTXOManagement', + params: { + data: depositWallet, + routeName: 'Wallet', + accountType: WalletType.PRE_MIX, + }, + merge: true, + }) + ); }; return ( diff --git a/src/screens/Mix/MixProgress.tsx b/src/screens/Mix/MixProgress.tsx index 6e2c6c002..0bc3ac99c 100644 --- a/src/screens/Mix/MixProgress.tsx +++ b/src/screens/Mix/MixProgress.tsx @@ -43,6 +43,7 @@ import useLabelsNew from 'src/hooks/useLabelsNew'; import { genrateOutputDescriptors } from 'src/core/utils'; import { bulkUpdateUTXOLabels } from 'src/store/sagaActions/utxos'; import { useQuery } from '@realm/react'; +import { CommonActions } from '@react-navigation/native'; const getBackgroungColor = (completed: boolean, error: boolean): string => { if (error) { @@ -66,6 +67,7 @@ function MixProgress({ walletPoolMap: any; isRemix: boolean; remixingToVault: boolean; + vaultId: string; }; }; navigation: any; @@ -85,7 +87,7 @@ function MixProgress({ }); const styles = getStyles(clock); - const { selectedUTXOs, depositWallet, isRemix, remixingToVault } = route.params; + const { selectedUTXOs, depositWallet, isRemix, remixingToVault, vaultId } = route.params; const statusData = [ { title: 'Subscribing', @@ -132,7 +134,7 @@ function MixProgress({ { title: isRemix ? remixingToVault - ? 'Remix to Vault successful' + ? 'Remix to vault successful' : 'Remix completed successful' : 'Mix completed successfully', subTitle: 'Mixed UTXO available in Postmix', @@ -155,7 +157,7 @@ function MixProgress({ const { postmixWallet, premixWallet } = useWhirlpoolWallets({ wallets: [depositWallet] })[ depositWallet.id ]; - const { activeVault } = useVault(); + const { activeVault } = useVault({ vaultId }); const source = isRemix ? postmixWallet : premixWallet; const destination = isRemix && remixingToVault ? activeVault : postmixWallet; const { labels } = useLabelsNew({ utxos: selectedUTXOs, wallet: depositWallet }); @@ -361,11 +363,17 @@ function MixProgress({ } finally { setTimeout(async () => { dispatch(refreshWallets(walletsToRefresh, { hardRefresh: true })); - navigation.navigate('UTXOManagement', { - data: depositWallet, - accountType: WalletType.POST_MIX, - routeName: 'Wallet', - }); + navigation.dispatch( + CommonActions.navigate({ + name: 'UTXOManagement', + params: { + data: depositWallet, + accountType: WalletType.POST_MIX, + routeName: 'Wallet', + }, + merge: true, + }) + ); }, 3000); } } @@ -426,7 +434,7 @@ function MixProgress({ const initiateWhirlpoolMix = async () => { try { - // To-Do: Instead of taking pool_denomination from the lets create a switch case to get it based on UTXO value + // ToDo: Instead of taking pool_denomination from the lets create a switch case to get it based on UTXO value const { height } = await ElectrumClient.getBlockchainHeaders(); for (const utxo of selectedUTXOs) { setCurrentUtxo(`${utxo.txId}:${utxo.vout}`); @@ -485,7 +493,7 @@ function MixProgress({ /> - {`Current UTXO: `} + {'Current UTXO: '} } title={`or share on Tap${isIos ? ' to Anroid' : ''}`} - subtitle="Bring device close to use NFC" + subtitle="Bring devices close to use NFC" callback={shareWithNFC} /> diff --git a/src/screens/NewKeeperAppScreen/NewKeeperAppScreen.tsx b/src/screens/NewKeeperAppScreen/NewKeeperAppScreen.tsx index db94f38a1..0c1ccfec2 100644 --- a/src/screens/NewKeeperAppScreen/NewKeeperAppScreen.tsx +++ b/src/screens/NewKeeperAppScreen/NewKeeperAppScreen.tsx @@ -14,13 +14,13 @@ import messaging from '@react-native-firebase/messaging'; import { setupKeeperApp } from 'src/store/sagaActions/storage'; import useToastMessage from 'src/hooks/useToastMessage'; import { Box, Pressable, useColorMode } from 'native-base'; -import KeeperHeader from 'src/components/KeeperHeader'; import openLink from 'src/utils/OpenLink'; import LoadingAnimation from 'src/components/Loader'; import { updateFCMTokens } from 'src/store/sagaActions/notifications'; import Fonts from 'src/constants/Fonts'; import { KEEPER_WEBSITE_BASE_URL } from 'src/core/config'; import BounceLoader from 'src/components/BounceLoader'; +import KeeperHeader from 'src/components/KeeperHeader'; export function Tile({ title, subTitle, onPress, Icon = null, loading = false }) { const { colorMode } = useColorMode(); @@ -133,7 +133,7 @@ function NewKeeperApp({ navigation }: { navigation }) { const getSignUpModalContent = () => ({ title: 'Setting up your app', - subTitle: 'Keeper allows you to create single sig wallets and a multisig Vault', + subTitle: 'Keeper allows you to create single sig wallets and multisig wallets called Vaults', message: 'Stack sats, whirlpool them, hodl long term and plan your inheritance with Keeper.', }); @@ -165,17 +165,18 @@ function NewKeeperApp({ navigation }: { navigation }) { return ( - + - + } onPress={() => { setInitiating(true); @@ -186,17 +187,18 @@ function NewKeeperApp({ navigation }: { navigation }) { - + - + } onPress={() => { navigation.navigate('LoginStack', { screen: 'EnterSeedScreen' }); @@ -207,8 +209,12 @@ function NewKeeperApp({ navigation }: { navigation }) { - - Terms of Service + + Note @@ -218,13 +224,13 @@ function NewKeeperApp({ navigation }: { navigation }) { openLink(`${KEEPER_WEBSITE_BASE_URL}terms-of-service/`)} > - + Terms of Service {' '} - and{' '} + and our{' '} openLink(`${KEEPER_WEBSITE_BASE_URL}privacy-policy/`)}> @@ -237,7 +243,7 @@ function NewKeeperApp({ navigation }: { navigation }) { { }} + close={() => {}} visible={appCreationError} title="Something went wrong" subTitle="Please check your internet connection and try again." @@ -252,7 +258,7 @@ function NewKeeperApp({ navigation }: { navigation }) { /> { }} + close={() => {}} visible={modalVisible} title={getSignUpModalContent().title} subTitle={getSignUpModalContent().subTitle} @@ -268,7 +274,7 @@ function NewKeeperApp({ navigation }: { navigation }) { /> { }} + close={() => {}} visible={appCreationError} title="Something went wrong" subTitle="Please check your internet connection and try again." @@ -294,7 +300,7 @@ const styles = StyleSheet.create({ flexDirection: 'row-reverse', }, tileContainer: { - marginTop: hp(20), + marginTop: hp(5), }, headerContainer: { // width: wp(280), @@ -310,7 +316,7 @@ const styles = StyleSheet.create({ width: wp(290), }, title: { - fontSize: 15, + fontSize: 14, letterSpacing: 1.12, }, subTitle: { @@ -329,17 +335,26 @@ const styles = StyleSheet.create({ modalMessageWrapper: { flexDirection: 'row', width: '100%', - alignItems: 'center' + alignItems: 'center', }, modalMessageText: { fontSize: 13, letterSpacing: 0.65, - paddingTop: 5 + paddingTop: 5, }, contentText: { fontSize: 13, letterSpacing: 0.65, - } + }, + addWalletText: { + lineHeight: 26, + letterSpacing: 0.8, + }, + addWalletDescription: { + fontSize: 12, + lineHeight: 20, + letterSpacing: 0.5, + }, }); export default NewKeeperApp; diff --git a/src/screens/QRScreens/RegisterWithChannel.tsx b/src/screens/QRScreens/RegisterWithChannel.tsx index 11a0a0a9f..3aa8d1958 100644 --- a/src/screens/QRScreens/RegisterWithChannel.tsx +++ b/src/screens/QRScreens/RegisterWithChannel.tsx @@ -16,7 +16,7 @@ import { REGISTRATION_SUCCESS, } from 'src/services/channel/constants'; import { captureError } from 'src/services/sentry'; -import { updateSignerDetails } from 'src/store/sagaActions/wallets'; +import { updateKeyDetails } from 'src/store/sagaActions/wallets'; import { useDispatch } from 'react-redux'; import { useNavigation, useRoute } from '@react-navigation/native'; import useVault from 'src/hooks/useVault'; @@ -24,6 +24,7 @@ import Text from 'src/components/KeeperText'; import { SignerType } from 'src/core/wallets/enums'; import crypto from 'crypto'; import { createCipheriv, createDecipheriv } from 'src/core/utils'; +import useSignerFromKey from 'src/hooks/useSignerFromKey'; const ScanAndInstruct = ({ onBarCodeRead }) => { const { colorMode } = useColorMode(); @@ -58,7 +59,8 @@ const ScanAndInstruct = ({ onBarCodeRead }) => { function RegisterWithChannel() { const { params } = useRoute(); - const { signer } = params as { signer: VaultSigner }; + const { vaultKey, vaultId } = params as { vaultKey: VaultSigner; vaultId: string }; + const { signer } = useSignerFromKey(vaultKey); const [channel] = useState(io(config.CHANNEL_URL)); const decryptionKey = useRef(); @@ -66,7 +68,7 @@ function RegisterWithChannel() { const dispatch = useDispatch(); const navgation = useNavigation(); - const { activeVault: vault } = useVault(); + const { activeVault: vault } = useVault({ vaultId }); const onBarCodeRead = ({ data }) => { decryptionKey.current = data; @@ -79,12 +81,17 @@ function RegisterWithChannel() { useEffect(() => { channel.on(BITBOX_REGISTER, async ({ room }) => { try { - const walletConfig = getWalletConfigForBitBox02({ vault }); + const walletConfig = getWalletConfigForBitBox02({ vault, signer }); channel.emit(BITBOX_REGISTER, { data: createCipheriv(JSON.stringify(walletConfig), decryptionKey.current), room, }); - dispatch(updateSignerDetails(signer, 'registered', true)); + dispatch( + updateKeyDetails(vaultKey, 'registered', { + registered: true, + vaultId: vault.id, + }) + ); navgation.goBack(); } catch (error) { captureError(error); @@ -106,9 +113,12 @@ function RegisterWithChannel() { switch (signerType) { case SignerType.LEDGER: dispatch( - updateSignerDetails(signer, 'deviceInfo', { registeredWallet: policy.policyHmac }) + updateKeyDetails(vaultKey, 'registered', { + registered: true, + vaultId: vault.id, + registrationInfo: JSON.stringify({ registeredWallet: policy.policyHmac }), + }) ); - dispatch(updateSignerDetails(signer, 'registered', true)); navgation.goBack(); } }); diff --git a/src/screens/QRScreens/RegisterWithQR.tsx b/src/screens/QRScreens/RegisterWithQR.tsx index 459fed251..9698def28 100644 --- a/src/screens/QRScreens/RegisterWithQR.tsx +++ b/src/screens/QRScreens/RegisterWithQR.tsx @@ -1,37 +1,60 @@ import React from 'react'; - import { Box } from 'native-base'; import KeeperHeader from 'src/components/KeeperHeader'; import ScreenWrapper from 'src/components/ScreenWrapper'; -import { StyleSheet } from 'react-native'; +import { Dimensions, StyleSheet } from 'react-native'; import { VaultSigner } from 'src/core/wallets/interfaces/vault'; import { getWalletConfig } from 'src/hardware'; import { useDispatch } from 'react-redux'; -import { updateSignerDetails } from 'src/store/sagaActions/wallets'; +import { updateKeyDetails } from 'src/store/sagaActions/wallets'; import Buttons from 'src/components/Buttons'; import useVault from 'src/hooks/useVault'; import DisplayQR from './DisplayQR'; +import { SignerType } from 'src/core/wallets/enums'; +import { genrateOutputDescriptors } from 'src/core/utils'; +import useSignerFromKey from 'src/hooks/useSignerFromKey'; +import QRCode from 'react-native-qrcode-svg'; +const { width } = Dimensions.get('window'); + +const SPECTER_PREFIX = 'addwallet keeper vault&'; function RegisterWithQR({ route, navigation }: any) { - const { signer }: { signer: VaultSigner } = route.params; + const { vaultKey, vaultId = '' }: { vaultKey: VaultSigner; vaultId: string } = route.params; const dispatch = useDispatch(); - const { activeVault } = useVault(); - const walletConfig = getWalletConfig({ vault: activeVault }); + const { activeVault } = useVault({ vaultId }); + const { signer } = useSignerFromKey(vaultKey); + const walletConfig = + signer.type === SignerType.SPECTER + ? SPECTER_PREFIX + + `${genrateOutputDescriptors(activeVault, false).replaceAll('/**', '')}${ + activeVault.isMultiSig ? ' )' : '' + }` + : getWalletConfig({ vault: activeVault }); const qrContents = Buffer.from(walletConfig, 'ascii').toString('hex'); - const markAsregistered = () => { - dispatch(updateSignerDetails(signer, 'registered', true)); + const markAsRegistered = () => { + dispatch( + updateKeyDetails(vaultKey, 'registered', { + registered: true, + vaultId: activeVault.id, + }) + ); navigation.goBack(); }; + return ( - + {signer.type === SignerType.SPECTER ? ( + + ) : ( + + )} - + ); } diff --git a/src/screens/QRScreens/ScanQR.tsx b/src/screens/QRScreens/ScanQR.tsx index f0b29e403..e95eb6476 100644 --- a/src/screens/QRScreens/ScanQR.tsx +++ b/src/screens/QRScreens/ScanQR.tsx @@ -22,6 +22,7 @@ import useNfcModal from 'src/hooks/useNfcModal'; import { globalStyles } from 'src/constants/globalStyles'; import MockWrapper from 'src/screens/Vault/MockWrapper'; import NFCOption from '../NFCChannel/NFCOption'; +import { InteracationMode } from '../Vault/HardwareModalMap'; let decoder = new URRegistryDecoder(); @@ -37,9 +38,10 @@ function ScanQR() { onQrScan = () => {}, setup = false, type, - isHealthcheck = false, + mode, signer, disableMockFlow = false, + addSignerFlow = false, } = route.params as any; const { translations } = useContext(LocalizationContext); @@ -56,7 +58,7 @@ function ScanQR() { useEffect(() => { if (qrData) { - if (isHealthcheck) { + if (mode === InteracationMode.HEALTH_CHECK) { onQrScan(qrData, resetQR, signer); } else { onQrScan(qrData, resetQR); @@ -113,7 +115,13 @@ function ScanQR() { return ( - + diff --git a/src/screens/QRScreens/SignWithChannel.tsx b/src/screens/QRScreens/SignWithChannel.tsx index f0c5df9b1..a30962a01 100644 --- a/src/screens/QRScreens/SignWithChannel.tsx +++ b/src/screens/QRScreens/SignWithChannel.tsx @@ -30,6 +30,8 @@ import { healthCheckSigner } from 'src/store/sagaActions/bhr'; import Text from 'src/components/KeeperText'; import crypto from 'crypto'; import { createCipheriv, createDecipheriv } from 'src/core/utils'; +import useToastMessage from 'src/hooks/useToastMessage'; +import useSignerFromKey from 'src/hooks/useSignerFromKey'; const ScanAndInstruct = ({ onBarCodeRead }) => { const { colorMode } = useColorMode(); @@ -64,18 +66,25 @@ const ScanAndInstruct = ({ onBarCodeRead }) => { function SignWithChannel() { const { params } = useRoute(); - const { signer, collaborativeWalletId = '' } = params as { - signer: VaultSigner; + const { + vaultKey, + collaborativeWalletId = '', + vaultId = '', + } = params as { + vaultKey: VaultSigner; collaborativeWalletId: string; + vaultId: string; }; - const { activeVault } = useVault(collaborativeWalletId); + const { signer } = useSignerFromKey(vaultKey); + const { activeVault } = useVault({ collaborativeWalletId, vaultId }); const { isMultiSig: isMultisig } = activeVault; const serializedPSBTEnvelops: SerializedPSBTEnvelop[] = useAppSelector( (state) => state.sendAndReceive.sendPhaseTwo.serializedPSBTEnvelops ); const { serializedPSBT, signingPayload } = serializedPSBTEnvelops.filter( - (envelop) => signer.signerId === envelop.signerId + (envelop) => vaultKey.xfp === envelop.xfp )[0]; + const { showToast } = useToastMessage(); const [channel] = useState(io(config.CHANNEL_URL)); const decryptionKey = useRef(); @@ -96,9 +105,10 @@ function SignWithChannel() { const data = await getTxForBitBox02( serializedPSBT, signingPayload, - signer, + vaultKey, isMultisig, - activeVault + activeVault, + signer ); channel.emit(BITBOX_SIGN, { data: createCipheriv(JSON.stringify(data), decryptionKey.current), @@ -107,7 +117,7 @@ function SignWithChannel() { }); channel.on(TREZOR_SIGN, ({ room }) => { try { - const data = getTxForTrezor(serializedPSBT, signingPayload, signer, activeVault); + const data = getTxForTrezor(serializedPSBT, signingPayload, vaultKey, activeVault); channel.emit(TREZOR_SIGN, { data: createCipheriv(JSON.stringify(data), decryptionKey.current), room, @@ -118,10 +128,22 @@ function SignWithChannel() { }); channel.on(LEDGER_SIGN, ({ room }) => { try { + const registerationInfo = vaultKey.registeredVaults.find( + (info) => info.vaultId === activeVault.id + )?.registrationInfo; + if (!registerationInfo) { + showToast('Please register the wallet before signing', null, 1000); + return; + } + const hmac = JSON.parse(registerationInfo)?.registeredWallet; + if (!hmac) { + showToast('Please register the wallet before signing', null, 1000); + return; + } const data = { serializedPSBT, vault: activeVault, - registeredWallet: activeVault.isMultiSig ? signer.deviceInfo.registeredWallet : undefined, + registeredWallet: activeVault.isMultiSig ? hmac : undefined, }; channel.emit(LEDGER_SIGN, { data: createCipheriv(JSON.stringify(data), decryptionKey.current), @@ -136,7 +158,7 @@ function SignWithChannel() { const decrypted = createDecipheriv(data, decryptionKey.current); if (signer.type === SignerType.TREZOR) { const { serializedTx: txHex } = decrypted; - dispatch(updatePSBTEnvelops({ txHex, signerId: signer.signerId })); + dispatch(updatePSBTEnvelops({ txHex, xfp: vaultKey.xfp })); dispatch(healthCheckSigner([signer])); navgation.dispatch( CommonActions.navigate({ name: 'SignTransactionScreen', merge: true }) @@ -147,7 +169,7 @@ function SignWithChannel() { decrypted, signingPayload ); - dispatch(updatePSBTEnvelops({ signedSerializedPSBT, signerId: signer.signerId })); + dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp: vaultKey.xfp })); dispatch(healthCheckSigner([signer])); navgation.dispatch( CommonActions.navigate({ name: 'SignTransactionScreen', merge: true }) @@ -158,7 +180,7 @@ function SignWithChannel() { signingPayload, decrypted ); - dispatch(updatePSBTEnvelops({ signedSerializedPSBT, signerId: signer.signerId })); + dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp: vaultKey.xfp })); dispatch(healthCheckSigner([signer])); navgation.dispatch( CommonActions.navigate({ name: 'SignTransactionScreen', merge: true }) diff --git a/src/screens/Recovery/EnterSeedScreen.tsx b/src/screens/Recovery/EnterSeedScreen.tsx index 732f78aa0..f8d0dede8 100644 --- a/src/screens/Recovery/EnterSeedScreen.tsx +++ b/src/screens/Recovery/EnterSeedScreen.tsx @@ -39,23 +39,19 @@ import { captureError } from 'src/services/sentry'; import { generateSignerFromMetaData } from 'src/hardware'; import { Colors } from 'react-native/Libraries/NewAppScreen'; import Fonts from 'src/constants/Fonts'; -import { VaultSigner, XpubDetailsType } from 'src/core/wallets/interfaces/vault'; +import { Signer, VaultSigner, XpubDetailsType } from 'src/core/wallets/interfaces/vault'; import TickIcon from 'src/assets/images/icon_tick.svg'; import ActivityIndicatorView from 'src/components/AppActivityIndicator/ActivityIndicatorView'; +import { InteracationMode } from '../Vault/HardwareModalMap'; +import useUnkownSigners from 'src/hooks/useUnkownSigners'; function EnterSeedScreen({ route }) { const navigation = useNavigation(); const { translations } = useContext(LocalizationContext); const { seed } = translations; - const { - isSoftKeyRecovery = false, - type, - isHealthCheck, - signer, - isMultisig, - setupSeedWordsBasedSigner, - } = route.params || {}; + const { type, mode, signer, isMultisig, setupSeedWordsBasedSigner, mapUnknownSigner } = + route.params || {}; const { appImageRecoverd, appRecoveryLoading, appImageError } = useAppSelector( (state) => state.bhr ); @@ -132,6 +128,7 @@ function EnterSeedScreen({ route }) { const [suggestedWords, setSuggestedWords] = useState([]); const [onChangeIndex, setOnChangeIndex] = useState(-1); const inputRef = useRef([]); + const isHealthCheck = mode === InteracationMode.HEALTH_CHECK; const openInvalidSeedsModal = () => { setRecoveryLoading(false); @@ -178,68 +175,6 @@ function EnterSeedScreen({ route }) { return seedWord.trim(); }; - const setupSeedWordsBasedKey = (mnemonic: string) => { - try { - const networkType = config.NETWORK_TYPE; - // fetched multi-sig seed words based key - const { - xpub: multiSigXpub, - derivationPath: multiSigPath, - masterFingerprint, - } = generateSeedWordsKey(mnemonic, networkType, EntityKind.VAULT); - // fetched single-sig seed words based key - const { xpub: singleSigXpub, derivationPath: singleSigPath } = generateSeedWordsKey( - mnemonic, - networkType, - EntityKind.WALLET - ); - - const xpubDetails: XpubDetailsType = {}; - xpubDetails[XpubTypes.P2WPKH] = { xpub: singleSigXpub, derivationPath: singleSigPath }; - xpubDetails[XpubTypes.P2WSH] = { xpub: multiSigXpub, derivationPath: multiSigPath }; - - const softSigner = generateSignerFromMetaData({ - xpub: isMultisig ? multiSigXpub : singleSigXpub, - derivationPath: isMultisig ? multiSigPath : singleSigPath, - xfp: masterFingerprint, - signerType: SignerType.SEED_WORDS, - storageType: SignerStorage.WARM, - isMultisig, - xpubDetails, - }); - dispatch(setSigningDevices(softSigner)); - navigation.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }); - } catch (err) { - navigation.dispatch(CommonActions.navigate('SignersList')); - captureError(err); - } - }; - - const onPressNextSoftReocvery = () => { - if (isSeedFilled(6)) { - if (isSeedFilled(12)) { - const seedWord = getSeedWord(); - if (type === SignerType.SEED_WORDS) { - setupSeedWordsBasedKey(seedWord); - } else if (type === SignerType.MOBILE_KEY) { - Alert.alert('Warning', 'Entire app will be restored', [ - { - text: 'OK', - onPress: () => { - setRecoveryLoading(true); - dispatch(getAppImage(seedWord)); - }, - }, - ]); - } - } else { - ref.current.scrollToIndex({ index: 5, animated: true }); - } - } else { - showToast('Enter correct seedwords', ); - } - }; - const onPressNextSeedReocvery = async () => { if (isSeedFilled(6)) { if (isSeedFilled(12)) { @@ -256,17 +191,38 @@ function EnterSeedScreen({ route }) { const onPressHealthCheck = () => { setHcLoading(true); + + const handleSuccess = () => { + dispatch(healthCheckSigner([signer])); + showToast(`Seed Key health check successfull`, ); + navigation.dispatch(CommonActions.goBack()); + }; + + const handleFailure = () => { + showToast(`Health check failed`); + }; + try { if (isSeedFilled(6)) { if (isSeedFilled(12)) { const seedWord = getSeedWord(); - const softSigner: VaultSigner = setupSeedWordsBasedSigner(seedWord, isMultisig); - if (softSigner.xpub === signer.xpub) { - dispatch(healthCheckSigner([signer])); - showToast(`Seed Key health check successfull`, ); - navigation.dispatch(CommonActions.goBack()); + const { signer: softSigner } = setupSeedWordsBasedSigner(seedWord, isMultisig); + if (mode === InteracationMode.IDENTIFICATION) { + const mapped = mapUnknownSigner({ + masterFingerprint: softSigner.masterFingerprint, + type: SignerType.COLDCARD, + }); + if (mapped) { + handleSuccess(); + } else { + handleFailure(); + } } else { - showToast(`Health check failed`, ); + if (softSigner.masterFingerprint === signer.masterFingerprint) { + handleSuccess(); + } else { + handleFailure(); + } } } } @@ -349,14 +305,7 @@ function EnterSeedScreen({ route }) { > - {isSoftKeyRecovery ? ( - - navigation.navigate('LoginStack', { screen: 'SigningDeviceListRecovery' }) - } - /> - ) : isHealthCheck ? ( + {isHealthCheck ? ( navigation.reset({ index: 0, routes: [{ name: 'NewKeeperApp' }] }) } @@ -443,6 +392,7 @@ function EnterSeedScreen({ route }) { setSuggestedWords([]); Keyboard.dismiss(); }} + testID={`input_seedWord${getPlaceholder(index)}`} /> )} @@ -458,6 +408,7 @@ function EnterSeedScreen({ route }) { ]} keyboardShouldPersistTaps="handled" nestedScrollEnabled + testID={'view_suggestionView'} > {suggestedWords.map((word, wordIndex) => ( @@ -483,8 +434,8 @@ function EnterSeedScreen({ route }) { ) : null} - - {seed.seedDescription} + + {seed.enterRecoveryPhraseNote} @@ -492,22 +443,12 @@ function EnterSeedScreen({ route }) { - {isSoftKeyRecovery ? ( - - ) : isHealthCheck ? ( + {isHealthCheck ? ( ) : ( { - navigation.navigate('LoginStack', { screen: 'OtherRecoveryMethods' }); - }} - secondaryText="Other Methods" primaryLoading={recoveryLoading} /> )} diff --git a/src/screens/Recovery/ScanQRFileRecovery.tsx b/src/screens/Recovery/ScanQRFileRecovery.tsx index f396059fd..8554890e3 100644 --- a/src/screens/Recovery/ScanQRFileRecovery.tsx +++ b/src/screens/Recovery/ScanQRFileRecovery.tsx @@ -62,8 +62,8 @@ function ScanQRFileRecovery({ route }) { ({ - index: 5, - routes: [ - { name: 'NewKeeperApp' }, - { name: 'EnterSeedScreen', params: { isSoftKeyRecovery: false, type } }, - { name: 'OtherRecoveryMethods' }, - { name: 'VaultRecoveryAddSigner' }, - { name: 'SigningDeviceListRecovery' }, - { name: 'EnterSeedScreen', params: { isSoftKeyRecovery: true, type } }, - ], -}); - -function SigningDeviceListRecovery({ navigation }) { - const { colorMode } = useColorMode(); - const { translations } = useContext(LocalizationContext); - const dispatch = useAppDispatch(); - const { signingDevices } = useAppSelector((state) => state.bhr); - const { inheritanceRequestId } = useAppSelector((state) => state.storage); - const sdModal = useAppSelector((state) => state.vault.sdIntroModal); - const [isNfcSupported, setNfcSupport] = useState(true); - const [signersLoaded, setSignersLoaded] = useState(false); - - const isMultisig = signingDevices.length >= 1; - - const { vault } = translations; - - const getNfcSupport = async () => { - const isSupported = await NFC.isNFCSupported(); - setNfcSupport(isSupported); - setSignersLoaded(true); - }; - - const getDeviceStatus = ( - type: SignerType, - isNfcSupported, - signingDevices, - inheritanceRequestId - ) => { - switch (type) { - case SignerType.COLDCARD: - case SignerType.TAPSIGNER: - return { - message: !isNfcSupported ? 'NFC is not supported in your device' : '', - disabled: config.ENVIRONMENT !== APP_STAGE.DEVELOPMENT && !isNfcSupported, - }; - case SignerType.POLICY_SERVER: - if (signingDevices.length < 2) { - return { - message: 'Add two other devices first to recover', - disabled: true, - }; - } - return { - message: '', - disabled: false, - }; - case SignerType.INHERITANCEKEY: - if (signingDevices.length < 2 || inheritanceRequestId) { - return { - message: 'Add two other devices first to recover', - disabled: true, - }; - } - return { - message: '', - disabled: false, - }; - case SignerType.SEED_WORDS: - case SignerType.MOBILE_KEY: - case SignerType.KEEPER: - case SignerType.JADE: - case SignerType.PASSPORT: - case SignerType.SEEDSIGNER: - case SignerType.KEYSTONE: - case SignerType.LEDGER: - default: - return { - message: '', - disabled: false, - }; - } - }; - - useEffect(() => { - getNfcSupport(); - }, []); - - const sortedSigners = [ - SignerType.COLDCARD, - SignerType.LEDGER, - SignerType.TREZOR, - SignerType.TAPSIGNER, - SignerType.SEEDSIGNER, - SignerType.BITBOX02, - SignerType.PASSPORT, - SignerType.JADE, - SignerType.KEYSTONE, - SignerType.OTHER_SD, - SignerType.MOBILE_KEY, - SignerType.POLICY_SERVER, - SignerType.KEEPER, - SignerType.SEED_WORDS, - SignerType.INHERITANCEKEY, - ]; - - function VaultSetupContent() { - return ( - - - - - - {`In the ${SubscriptionTier.L1} tier, you can add one signing device to activate your vault. This can be upgraded to three signing devices and five signing devices on ${SubscriptionTier.L2} and ${SubscriptionTier.L3} tiers\n\nIf a particular signing device is not supported, it will be indicated.`} - - - ); - } - - function HardWareWallet({ type, disabled, message, first = false, last = false }: HWProps) { - const [visible, setVisible] = useState(false); - - const onPress = () => { - open(); - }; - - const open = () => setVisible(true); - const close = () => setVisible(false); - - return ( - - - - - {SDIcons(type, colorMode === 'dark').Icon} - - - {SDIcons(type).Logo} - - {message} - - - - - - - - - ); - } - - return ( - - { - dispatch(setSdIntroModal(true)); - }} - onPressHandler={() => - navigation.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }) - } - /> - - - {!signersLoaded ? ( - - ) : ( - - {sortedSigners?.map((type: SignerType, index: number) => { - const { disabled, message } = getDeviceStatus( - type, - isNfcSupported, - signingDevices, - inheritanceRequestId - ); - return ( - - ); - })} - - )} - - - { - dispatch(setSdIntroModal(false)); - }} - title="Signing Devices" - subTitle="A signing device is a hardware or software that stores one of the private keys needed for your Vault" - modalBackground={`${colorMode}.modalGreenBackground`} - buttonTextColor={colorMode === 'light' ? `${colorMode}.greenText2` : `${colorMode}.white`} - buttonBackground={`${colorMode}.modalWhiteButton`} - buttonText="Add Now" - buttonCallback={() => { - dispatch(setSdIntroModal(false)); - }} - textColor={`${colorMode}.modalGreenContent`} - Content={VaultSetupContent} - DarkCloseIcon - learnMore - learnMoreCallback={() => - openLink(`${KEEPER_KNOWLEDGEBASE}knowledge-base-category/recovery-why-keeper/`) - } - /> - - ); -} - -const styles = StyleSheet.create({ - modalText: { - letterSpacing: 0.65, - fontSize: 13, - marginTop: 5, - padding: 1, - }, - scrollViewContainer: { - alignItems: 'center', - justifyContent: 'center', - paddingBottom: '2%', - }, - scrollViewWrapper: { - height: windowHeight > 800 ? '76%' : '74%', - }, - contactUsText: { - fontSize: 12, - letterSpacing: 0.6, - width: wp(300), - lineHeight: 20, - marginTop: hp(20), - }, - walletMapContainer: { - alignItems: 'center', - height: windowHeight * 0.08, - flexDirection: 'row', - paddingLeft: wp(40), - }, - walletMapWrapper: { - marginRight: wp(20), - alignItems: 'center', - width: wp(15), - }, - walletMapLogoWrapper: { - marginLeft: wp(23), - justifyContent: 'flex-end', - marginTop: hp(20), - }, - messageText: { - fontSize: 10, - fontWeight: '400', - letterSpacing: 1.3, - marginTop: hp(5), - }, - dividerStyle: { - opacity: 0.1, - width: windowWidth * 0.8, - height: 0.5, - }, - divider: { - opacity: 0.5, - height: hp(26), - width: 1.5, - }, - italics: { - fontStyle: 'italic', - }, -}); -export default SigningDeviceListRecovery; diff --git a/src/screens/Recovery/SigningDeviceConfigRecovery.tsx b/src/screens/Recovery/SigningDeviceConfigRecovery.tsx index cc006b013..745944f86 100644 --- a/src/screens/Recovery/SigningDeviceConfigRecovery.tsx +++ b/src/screens/Recovery/SigningDeviceConfigRecovery.tsx @@ -1,5 +1,5 @@ import { Box, ScrollView, View } from 'native-base'; -import { useNavigation } from '@react-navigation/native'; +import { CommonActions, useNavigation } from '@react-navigation/native'; import React, { useEffect, useState } from 'react'; import { hp, windowHeight, windowWidth, wp } from 'src/constants/responsive'; import Text from 'src/components/KeeperText'; @@ -15,6 +15,7 @@ import NFC from 'src/services/nfc'; import { useAppSelector } from 'src/store/hooks'; import { SDIcons } from '../Vault/SigningDeviceIcons'; +import { InteracationMode } from '../Vault/HardwareModalMap'; export const getDeviceStatus = (type: SignerType, isNfcSupported, signingDevices) => { switch (type) { @@ -59,7 +60,9 @@ function ColdCardSetupContent() { - {`Export the Vault config by going to Setting > Multisig > Then select the wallet > Export `} + { + 'Export the vault config by going to Setting > Multisig > Then select the wallet > Export ' + } @@ -81,7 +84,9 @@ function PassportSetupContent() { marginLeft: wp(10), }} > - {`\u2022 Export the xPub from the Account section > Manage Account > Connect Wallet > Keeper > Multisig > QR Code.\n`} + { + '\u2022 Export the xPub from the Account section > Manage Account > Connect Wallet > Keeper > Multisig > QR Code.\n' + } @@ -156,10 +161,12 @@ function SigningDeviceConfigRecovery({ navigation }) { buttonText="Proceed" buttonTextColor="light.white" buttonCallback={() => { - navigate('LoginStack', { - screen: 'ColdCardReocvery', - params: { isConfigRecovery: true }, - }); + navigation.dispatch( + CommonActions.navigate({ + name: 'AddColdCard', + params: { mode: InteracationMode.CONFIG_RECOVERY }, + }) + ); close(); }} textColor="light.primaryText" @@ -191,9 +198,9 @@ function SigningDeviceConfigRecovery({ navigation }) { return ( navigation.navigate('LoginStack', { screen: 'OtherRecoveryMethods' })} + title="Select signer" + subtitle="To recover your vault" + onPressHandler={() => navigation.goBack()} /> diff --git a/src/screens/Send/AddSendAmount.tsx b/src/screens/Send/AddSendAmount.tsx index 51d3c7e0e..0fea77aeb 100644 --- a/src/screens/Send/AddSendAmount.tsx +++ b/src/screens/Send/AddSendAmount.tsx @@ -1,15 +1,15 @@ import Text from 'src/components/KeeperText'; import { Box, - HStack, + // HStack, Input, KeyboardAvoidingView, Pressable, useColorMode, - VStack, + // VStack, } from 'native-base'; import { Platform, ScrollView, StyleSheet } from 'react-native'; -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { calculateSendMaxFee, sendPhaseOne } from 'src/store/sagaActions/send_and_receive'; import { hp, windowWidth, wp } from 'src/constants/responsive'; @@ -34,19 +34,26 @@ import useCurrencyCode from 'src/store/hooks/state-selectors/useCurrencyCode'; import CurrencyKind from 'src/models/enums/CurrencyKind'; import { Satoshis } from 'src/models/types/UnitAliases'; import BTCIcon from 'src/assets/images/btc_black.svg'; +import CollaborativeIcon from 'src/assets/images/collaborative_vault_white.svg'; +import WalletIcon from 'src/assets/images/daily_wallet.svg'; +import VaultIcon from 'src/assets/images/vault_icon.svg'; import { UTXO } from 'src/core/wallets/interfaces'; import config from 'src/core/config'; -import { EntityKind, TxPriority } from 'src/core/wallets/enums'; +import { EntityKind, TxPriority, VaultType } from 'src/core/wallets/enums'; import idx from 'idx'; import useLabelsNew from 'src/hooks/useLabelsNew'; import CurrencyTypeSwitch from 'src/components/Switch/CurrencyTypeSwitch'; import WalletSendInfo from './WalletSendInfo'; -import LabelItem from '../UTXOManagement/components/LabelItem'; +// import LabelItem from '../UTXOManagement/components/LabelItem'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import Fonts from 'src/constants/Fonts'; function AddSendAmount({ route }) { const { colorMode } = useColorMode(); const navigation = useNavigation(); const dispatch = useDispatch(); + const { translations } = useContext(LocalizationContext); + const { wallet: walletTranslation } = translations; const { sender, recipient, @@ -66,7 +73,7 @@ function AddSendAmount({ route }) { const [amount, setAmount] = useState(prefillAmount || ''); const [amountToSend, setAmountToSend] = useState(''); const [note, setNote] = useState(''); - const [label, setLabel] = useState(''); + // const [label, setLabel] = useState(''); const [labelsToAdd, setLabelsToAdd] = useState([]); const [errorMessage, setErrorMessage] = useState(''); // this state will handle error @@ -147,9 +154,10 @@ function AddSendAmount({ route }) { sender, recipient, address, - amount: parseInt(amountToSend, 10), + amount: parseInt(amountToSend, 10), // in sats transferType, note, + selectedUTXOs, label: labelsToAdd.filter( (item) => !(item.name === idx(recipient, (_) => _.presentationData.name) && item.isSystem) // remove wallet labels are they are internal refrerences ), @@ -212,17 +220,24 @@ function AddSendAmount({ route }) { setLabelsToAdd(initialLabels); }, []); - const onAdd = () => { - if (label) { - labelsToAdd.push({ name: label, isSystem: false }); - setLabelsToAdd(labelsToAdd); - setLabel(''); + // const onAdd = () => { + // if (label) { + // labelsToAdd.push({ name: label, isSystem: false }); + // setLabelsToAdd(labelsToAdd); + // setLabel(''); + // } + // }; + // const onCloseClick = (index) => { + // labelsToAdd.splice(index, 1); + // setLabelsToAdd([...labelsToAdd]); + // }; + const getWalletIcon = (wallet) => { + if (wallet.entityKind === EntityKind.VAULT) { + return wallet.type === VaultType.COLLABORATIVE ? : ; + } else { + return ; } }; - const onCloseClick = (index) => { - labelsToAdd.splice(index, 1); - setLabelsToAdd([...labelsToAdd]); - }; return ( + + + {walletTranslation.sendingFrom} + + - + @@ -376,7 +402,7 @@ function AddSendAmount({ route }) { }} /> - - + */} { - navigation.dispatch( - CommonActions.navigate('UTXOSelection', { sender, amount, address }) - ); - }} - secondaryDisable={Boolean(!amount || errorMessage)} + // secondaryText="Select UTXOs" + // secondaryCallback={() => { + // navigation.dispatch( + // CommonActions.navigate('UTXOSelection', { sender, amount, address }) + // ); + // }} + // secondaryDisable={Boolean(!amount || errorMessage)} primaryText="Send" primaryDisable={Boolean(!amount || errorMessage)} primaryCallback={executeSendPhaseOne} @@ -479,6 +505,7 @@ const styles = StyleSheet.create({ paddingHorizontal: hp(10), paddingVertical: hp(3), borderRadius: 5, + borderWidth: 1, }, sendMaxText: { fontSize: 12, @@ -537,5 +564,13 @@ const styles = StyleSheet.create({ justifyContent: 'center', width: '25%', }, + sendingFromWrapper: { + marginLeft: wp(20), + }, + sendingFromText: { + fontSize: 12, + fontFamily: Fonts.FiraSansCondensedRegular, + letterSpacing: 0.8, + }, }); export default AddSendAmount; diff --git a/src/screens/Send/CustomPriorityModal.tsx b/src/screens/Send/CustomPriorityModal.tsx index b20ca14b6..23a5de78d 100644 --- a/src/screens/Send/CustomPriorityModal.tsx +++ b/src/screens/Send/CustomPriorityModal.tsx @@ -2,12 +2,19 @@ import Text from 'src/components/KeeperText'; import { Box, Modal, Input, useColorMode } from 'native-base'; import { Platform, StyleSheet, TouchableOpacity } from 'react-native'; -import Close from 'src/assets/images/modal_close.svg'; -import React, { useState } from 'react'; +// import Close from 'src/assets/images/modal_close.svg'; +import React, { useContext, useEffect, useState } from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import KeyPadView from 'src/components/AppNumPad/KeyPadView'; import { windowHeight, windowWidth } from 'src/constants/responsive'; import { useAppSelector } from 'src/store/hooks'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import CurrencyTypeSwitch from 'src/components/Switch/CurrencyTypeSwitch'; +import useBalance from 'src/hooks/useBalance'; +import BitcoinInput from 'src/assets/images/btc_input.svg'; +import { calculateCustomFee } from 'src/store/sagaActions/send_and_receive'; +import { useDispatch } from 'react-redux'; +import useToastMessage from 'src/hooks/useToastMessage'; function CustomPriorityModal(props) { const { colorMode } = useColorMode(); @@ -18,16 +25,27 @@ function CustomPriorityModal(props) { subTitle = null, info = null, buttonBackground = [`${colorMode}.gradientStart`, `${colorMode}.gradientEnd`], - buttonText = 'Button text', + buttonText = 'Confirm', buttonTextColor = 'white', buttonCallback, + secondaryButtonText, + secondaryCallback, textColor = '#000', network, + recipients, + sender, + selectedUTXOs, } = props; const { bottom } = useSafeAreaInsets(); const [customPriorityFee, setCustomPriorityFee] = useState(''); - const [customEstBlocks, setCustomEstBlock] = useState(''); + const [customEstBlocks, setCustomEstBlock] = useState(); + const [estimationSign, setEstimationSign] = useState('~'); const averageTxFees = useAppSelector((state) => state.network.averageTxFees); + const { translations } = useContext(LocalizationContext); + const { common, wallet: walletTranslation } = translations; + const { getCurrencyIcon } = useBalance(); + const dispatch = useDispatch(); + const { showToast } = useToastMessage(); const onPressNumber = (text) => { let currentFee = customPriorityFee; @@ -40,30 +58,26 @@ function CustomPriorityModal(props) { }; const updateFeeAndBlock = (value) => { - if (averageTxFees && averageTxFees[network].feeRates) { - const { feeRates } = averageTxFees[network]; + setEstimationSign('~'); + if (averageTxFees && averageTxFees[network]) { + const { high, medium, low } = averageTxFees[network]; const customFeeRatePerByte = parseInt(value); let customEstimatedBlock = 0; - // handling extremes - if (customFeeRatePerByte > feeRates['2']) { - customEstimatedBlock = 1; - } else if (customFeeRatePerByte < feeRates['144']) { - customEstimatedBlock = 200; + if (customFeeRatePerByte >= high.feePerByte) { + customEstimatedBlock = high.estimatedBlocks; + if (customFeeRatePerByte > high.feePerByte) setEstimationSign('<'); + } else if (customFeeRatePerByte <= low.feePerByte) { + customEstimatedBlock = low.estimatedBlocks; + if (customFeeRatePerByte < low.feePerByte) setEstimationSign('>'); } else { - const closestFeeRatePerByte = Object.values(feeRates).reduce((prev, curr) => - Math.abs(curr - customFeeRatePerByte) < Math.abs(prev - customFeeRatePerByte) - ? curr - : prev - ); - - const etimatedBlock = Object.keys(feeRates).find( - (key) => feeRates[key] === closestFeeRatePerByte - ); - customEstimatedBlock = parseInt(etimatedBlock); + customEstimatedBlock = medium.estimatedBlocks; } - if (parseInt(value) >= 1) setCustomEstBlock(`${customEstimatedBlock}`); - else setCustomPriorityFee(''); + if (parseInt(value) >= 1) setCustomEstBlock(customEstimatedBlock); + else { + setCustomPriorityFee(''); + setCustomEstBlock(''); + } } setCustomPriorityFee(value); @@ -73,6 +87,31 @@ function CustomPriorityModal(props) { updateFeeAndBlock(customPriorityFee.slice(0, customPriorityFee.length - 1)); }; + const handleCustomFee = () => { + dispatch( + calculateCustomFee({ + wallet: sender, + recipients, + feePerByte: customPriorityFee, + customEstimatedBlocks: customEstBlocks, + selectedUTXOs, + }) + ); + }; + + const customSendPhaseOneResults = useAppSelector( + (state) => state.sendAndReceive.customPrioritySendPhaseOne + ); + + useEffect(() => { + if (customSendPhaseOneResults.failedErrorMessage) { + showToast(customSendPhaseOneResults.failedErrorMessage); + buttonCallback(false); + } else if (customSendPhaseOneResults.isSuccessful) { + buttonCallback(true); + } + }, [customSendPhaseOneResults]); + const bottomMargin = Platform.select({ ios: bottom, android: '5%' }); return ( @@ -84,37 +123,55 @@ function CustomPriorityModal(props) { justifyContent="flex-end" > - - + + {/* - + */} - - {title} - - - {subTitle} - + + + {title} + + + {subTitle} + + + + + + } backgroundColor={`${colorMode}.seashellWhite`} mx="3" placeholder="Enter Amount" + h={windowHeight * 0.05} width="100%" variant="unstyled" value={customPriorityFee} /> - - - {info} - + + {walletTranslation.estimateArrvlTime} + {customEstBlocks ? `${estimationSign} ${customEstBlocks * 10} mins` : ''} - { - setCustomPriorityFee(''); - }} - > + - Start Over + {secondaryButtonText} - { - buttonCallback(customPriorityFee, customEstBlocks); - }} - > + + ) : ( @@ -208,28 +216,27 @@ function SendingCard({ function Transaction({ txFeeInfo, transactionPriority }) { const { colorMode } = useColorMode(); return ( - 570 ? 3 : 1 - }> + 570 ? 3 : 1}> Transaction Priority {txFeeInfo[transactionPriority?.toLowerCase()]?.amount} sats - + ); } -function TextValue({ amt, unit }) { +function TextValue({ amt, getValueIcon }) { return ( - {amt} sats + {amt} {getValueIcon() === 'sats' ? 'sats' : '$'} ); } @@ -239,19 +246,24 @@ function SendingPriority({ transactionPriority, setTransactionPriority, availableTransactionPriorities, + setVisibleCustomPriorityModal, + getBalance, + getSatUnit, }) { + const { translations } = useContext(LocalizationContext); + const { settings, wallet: walletTranslation } = translations; const { colorMode } = useColorMode(); return ( - - - + + {/* */} + Priority @@ -260,91 +272,106 @@ function SendingPriority({ - - {availableTransactionPriorities?.map((priority) => ( - { - setTransactionPriority(priority); - }} - > - - - { - setTransactionPriority(priority); - }} - /> - - {String(priority)} - - - + {availableTransactionPriorities?.map((priority) => { + if (txFeeInfo[priority?.toLowerCase()].estimatedBlocksBeforeConfirmation !== 0) { + return ( + { + setTransactionPriority(priority); }} > - ~{txFeeInfo[priority?.toLowerCase()]?.estimatedBlocksBeforeConfirmation * 10} mins - - - - - ))} + + + { + setTransactionPriority(priority); + }} + /> + + {String(priority)} + + + + ~{txFeeInfo[priority?.toLowerCase()]?.estimatedBlocksBeforeConfirmation * 10}{' '} + mins + + + + + ); + } + })} + + + {colorMode === 'light' ? : } + + {walletTranslation.addCustomPriority} + + + ); } -function FeeInfo({ txFeeInfo, transactionPriority, transferType, sendMaxFee }) { - return ( - - - - Fees - - - ~ 10 - 30 mins - - - - -   - {transferType === TransferType.WALLET_TO_VAULT - ? sendMaxFee - : txFeeInfo[transactionPriority?.toLowerCase()]?.amount} - - - ); -} +// function FeeInfo({ txFeeInfo, transactionPriority, transferType, sendMaxFee }) { +// return ( +// +// +// +// Fees +// +// +// ~ 10 - 30 mins +// +// +// +// +//   +// {transferType === TransferType.WALLET_TO_VAULT +// ? sendMaxFee +// : txFeeInfo[transactionPriority?.toLowerCase()]?.amount} +// +// +// ); +// } function SendSuccessfulContent() { const { colorMode } = useColorMode(); @@ -385,6 +412,103 @@ function ApproveTransVaultContent({ setVisibleTransVaultModal, onTransferNow }) ); } +function TransactionPriorityDetails({ transactionPriority, txFeeInfo, getBalance, getSatUnit }) { + const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { wallet: walletTransactions } = translations; + + return ( + + + + {walletTransactions.transactionPriority} + + + + + {walletTransactions.PRIORITY} + {walletTransactions.ARRIVALTIME} + {walletTransactions.FEE} + + + {transactionPriority.toUpperCase()} + + ~{' '} + {txFeeInfo[transactionPriority?.toLowerCase()]?.estimatedBlocksBeforeConfirmation * 10}{' '} + mins + + + + {getSatUnit() === 'sats' ? : $} +   + + {getBalance(txFeeInfo[transactionPriority?.toLowerCase()]?.amount)} + + + + + + ... + + + + ); +} +function AmountDetails(props) { + return ( + + + + {props.title} + + + + + {props.fiatAmount} + + + + {props.satsAmount} + + + ); +} + +function HighFeeAlert({ transactionPriority, txFeeInfo, amountToSend, getBalance }) { + const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { wallet: walletTransactions } = translations; + + const selectedFee = txFeeInfo[transactionPriority?.toLowerCase()].amount; + return ( + <> + + {walletTransactions.networkFee} + + {selectedFee}   + {getBalance(selectedFee)} + + + + {walletTransactions.amtBeingSent} + + {amountToSend}   + {getBalance(amountToSend)} + + + + ); +} function SendConfirmation({ route }) { const { colorMode } = useColorMode(); @@ -400,6 +524,7 @@ function SendConfirmation({ route }) { uaiSetActionFalse, note, label, + selectedUTXOs, }: { sender: Wallet | Vault; recipient: Wallet | Vault; @@ -414,6 +539,7 @@ function SendConfirmation({ route }) { name: string; isSystem: boolean; }[]; + selectedUTXOs: UTXO[]; } = route.params; const txFeeInfo = useAppSelector((state) => state.sendAndReceive.transactionFeeInfo); @@ -425,7 +551,7 @@ function SendConfirmation({ route }) { const [transactionPriority, setTransactionPriority] = useState(TxPriority.LOW); const { wallets } = useWallets({ getAll: true }); const sourceWallet = wallets.find((item) => item.id === walletId); - const { activeVault: defaultVault } = useVault(); + const { activeVault: defaultVault } = useVault({ includeArchived: false, getFirst: true }); const availableTransactionPriorities = useAvailableTransactionPriorities(); const { translations } = useContext(LocalizationContext); @@ -438,8 +564,12 @@ function SendConfirmation({ route }) { const [visibleModal, setVisibleModal] = useState(false); const [visibleTransVaultModal, setVisibleTransVaultModal] = useState(false); const [title, setTitle] = useState('Sending to address'); - const [subTitle, setSubTitle] = useState('Choose priority and fee'); + const [subTitle, setSubTitle] = useState('Review the transaction setup'); const [confirmPassVisible, setConfirmPassVisible] = useState(false); + const [transPriorityModalVisible, setTransPriorityModalVisible] = useState(false); + const [highFeeAlertVisible, setHighFeeAlertVisible] = useState(false); + const [visibleCustomPriorityModal, setVisibleCustomPriorityModal] = useState(false); + const [feePercentage, setFeePercentage] = useState(0); useEffect(() => { if (vaultTransfers.includes(transferType)) { @@ -458,6 +588,17 @@ function SendConfirmation({ route }) { } }, []); + useEffect(() => { + let hasHighFee = false; + const selectedFee = txFeeInfo[transactionPriority?.toLowerCase()].amount; + if (selectedFee > amount / 10) hasHighFee = true; // if fee is greater than 10% of the amount being sent + + setFeePercentage(Math.trunc((selectedFee / amount) * 100)); + + if (hasHighFee) setHighFeeAlertVisible(true); + else setHighFeeAlertVisible(false); + }, [transactionPriority, amount]); + const onTransferNow = () => { setVisibleTransVaultModal(false); dispatch( @@ -473,7 +614,6 @@ function SendConfirmation({ route }) { useEffect(() => { if (inProgress) { - // TODO: Remove this timeout until we optimise the crypto setTimeout(() => { dispatch(sendPhaseTwoReset()); dispatch( @@ -519,12 +659,10 @@ function SendConfirmation({ route }) { (state) => state.sendAndReceive.sendPhaseTwo ); const navigation = useNavigation(); - let collaborativeWalletId; - if (transferType !== TransferType.WALLET_TO_VAULT) { - sender.entityKind === EntityKind.VAULT && sender.type === VaultType.COLLABORATIVE + const collaborativeWalletId = + sender?.entityKind === EntityKind.VAULT && sender.type === VaultType.COLLABORATIVE ? sender.collaborativeWalletId : ''; - } useEffect(() => { if (serializedPSBTEnvelops && serializedPSBTEnvelops.length) { @@ -534,6 +672,7 @@ function SendConfirmation({ route }) { note, label, collaborativeWalletId, + vaultId: sender.id, }) ); } @@ -541,12 +680,18 @@ function SendConfirmation({ route }) { const viewDetails = () => { setVisibleModal(false); - if (vaultTransfers.includes(transferType)) { + if (vaultTransfers.includes(transferType) && collaborativeWalletId) { const navigationState = { index: 1, routes: [ { name: 'Home' }, - { name: 'VaultDetails', params: { autoRefresh: true, collaborativeWalletId } }, + { + name: 'VaultDetails', + params: { + autoRefresh: true, + collaborativeWalletId, + }, + }, ], }; navigation.dispatch(CommonActions.reset(navigationState)); @@ -584,10 +729,15 @@ function SendConfirmation({ route }) { } } }, [crossTransferSuccess]); + return ( - - + } + /> + - + setTransPriorityModalVisible(true)}> + + + + + + + {/* {customFeeOptionTransfers.includes(transferType) ? ( )} - - + */} + {transferType === TransferType.VAULT_TO_VAULT ? ( - + ) : null} { navigation.goBack(); @@ -676,7 +843,7 @@ function SendConfirmation({ route }) { close={() => setConfirmPassVisible(false)} title={walletTransactions.confirmPassTitle} subTitleWidth={wp(240)} - subTitle={''} + subTitle="" modalBackground={`${colorMode}.modalWhiteBackground`} subTitleColor={`${colorMode}.secondaryText`} textColor={`${colorMode}.primaryText`} @@ -690,6 +857,83 @@ function SendConfirmation({ route }) { /> )} /> + {/* Transaction Priority Modal */} + setTransPriorityModalVisible(false)} + showCloseIcon={false} + title={walletTransactions.transactionPriority} + subTitleWidth={wp(240)} + subTitle="" + modalBackground={`${colorMode}.modalWhiteBackground`} + subTitleColor={`${colorMode}.secondaryText`} + textColor={`${colorMode}.primaryText`} + buttonTextColor={`${colorMode}.white`} + buttonText={common.confirm} + buttonCallback={() => { + setTransPriorityModalVisible(false), setTransactionPriority; + }} + secondaryButtonText={common.cancel} + secondaryCallback={() => setTransPriorityModalVisible(false)} + Content={() => ( + { + setTransPriorityModalVisible(false); + dispatch(customPrioritySendPhaseOneReset()); + setVisibleCustomPriorityModal(true); + }} + /> + )} + /> + {/* High fee alert Modal */} + setHighFeeAlertVisible(false)} + showCloseIcon={false} + title={walletTransactions.highFeeAlert} + subTitleWidth={wp(240)} + subTitle={`Network fee is greater than ${feePercentage}% of the amount being sent`} + modalBackground={`${colorMode}.modalWhiteBackground`} + subTitleColor={`${colorMode}.secondaryText`} + textColor={`${colorMode}.primaryText`} + buttonTextColor={`${colorMode}.white`} + buttonText={common.proceed} + buttonCallback={() => { + setHighFeeAlertVisible(false); + }} + Content={() => ( + + )} + /> + {visibleCustomPriorityModal && ( + setVisibleCustomPriorityModal(false)} + title={vault.CustomPriority} + secondaryButtonText={common.cancel} + secondaryCallback={() => setVisibleCustomPriorityModal(false)} + subTitle="Enter sats to pay per vbyte" + network={sender.networkType} + recipients={[{ address, amount }]} // TODO: rewire for Batch Send + sender={sender} + selectedUTXOs={selectedUTXOs} + buttonCallback={(setCustomTxPriority) => { + setVisibleCustomPriorityModal(false); + if (setCustomTxPriority) setTransactionPriority(TxPriority.CUSTOM); + }} + /> + )} ); } @@ -743,4 +987,99 @@ const styles = StyleSheet.create({ customPriority: { fontStyle: 'italic', }, + transPriorityWrapper: { + flexDirection: 'row', + borderRadius: 10, + padding: windowHeight * 0.019, + alignItems: 'center', + justifyContent: 'space-between', + }, + transTitleWrapper: { + marginVertical: 10, + }, + transTitleText: { + fontSize: 14, + letterSpacing: 1.12, + }, + transLabelText: { + fontSize: 12, + fontFamily: Fonts.FiraSansCondensedRegular, + }, + transFiatFeeText: { + fontSize: 16, + fontWeight: '300', + fontFamily: Fonts.FiraSansCondensedMedium, + }, + transSatsFeeText: { + fontSize: 12, + }, + transSatsFeeWrapper: { + width: '60%', + alignItems: 'center', + flexDirection: 'row', + }, + addTransPriority: { + height: 60, + borderRadius: 10, + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + marginVertical: hp(30), + borderWidth: 0.8, + }, + addPriorityText: { + fontSize: 15, + fontWeight: '400', + letterSpacing: 0.6, + }, + amountDetailsWrapper: { + flexDirection: 'row', + width: '100%', + marginTop: 20, + }, + amtDetailsTitleWrapper: { + width: '30%', + justifyContent: 'flex-start', + }, + amtFiatSatsTitleWrapper: { + width: '35%', + alignItems: 'flex-end', + }, + amtDetailsText: { + fontSize: 12, + fontFamily: Fonts.FiraSansCondensedRegular, + letterSpacing: 0.55, + }, + horizontalLineStyle: { + borderBottomWidth: 0.3, + marginTop: hp(20), + opacity: 0.5, + }, + highFeeTitle: { + fontSize: 14, + fontFamily: Fonts.FiraSansCondensedRegular, + letterSpacing: 0.55, + }, + highFeeDetailsWrapper: { + flexDirection: 'row', + width: '100%', + }, + highFeeDetailsContainer: { + width: windowWidth * 0.8, + padding: 10, + marginVertical: 10, + }, + highAlertFiatFee: { + fontSize: 16, + fontFamily: Fonts.FiraSansCondensedRegular, + }, + highAlertSatsFee: { + fontSize: 12, + fontFamily: Fonts.FiraSansCondensedRegular, + }, + currentTypeSwitchWrapper: { + alignItems: 'center', + justifyContent: 'center', + width: '25%', + }, }); diff --git a/src/screens/Send/SendScreen.tsx b/src/screens/Send/SendScreen.tsx index 7261b48d2..3456a5458 100644 --- a/src/screens/Send/SendScreen.tsx +++ b/src/screens/Send/SendScreen.tsx @@ -19,10 +19,13 @@ import { QRreader } from 'react-native-qr-decode-image-camera'; import Text from 'src/components/KeeperText'; import Colors from 'src/theme/Colors'; import KeeperHeader from 'src/components/KeeperHeader'; -import IconWallet from 'src/assets/images/icon_wallet.svg'; +import WalletIcon from 'src/assets/images/daily_wallet.svg'; +import CollaborativeIcon from 'src/assets/images/collaborative_vault_white.svg'; +import VaultIcon from 'src/assets/images/vault_icon.svg'; + import { LocalizationContext } from 'src/context/Localization/LocContext'; import Note from 'src/components/Note/Note'; -import { PaymentInfoKind } from 'src/core/wallets/enums'; +import { EntityKind, PaymentInfoKind, VaultType, VisibilityType } from 'src/core/wallets/enums'; import { RNCamera } from 'react-native-camera'; import ScreenWrapper from 'src/components/ScreenWrapper'; import { Wallet } from 'src/core/wallets/interfaces/wallet'; @@ -40,6 +43,9 @@ import ToastErrorIcon from 'src/assets/images/toast_error.svg'; import WalletOperations from 'src/core/wallets/operations'; import useWallets from 'src/hooks/useWallets'; import { UTXO } from 'src/core/wallets/interfaces'; +import useVault from 'src/hooks/useVault'; +import HexagonIcon from 'src/components/HexagonIcon'; +import EmptyWalletIcon from 'src/assets/images/empty_wallet_illustration.svg'; function SendScreen({ route }) { const { colorMode } = useColorMode(); @@ -58,10 +64,15 @@ function SendScreen({ route }) { const [paymentInfo, setPaymentInfo] = useState(''); const network = WalletUtilities.getNetworkByType(sender.networkType); - const { wallets: allWallets } = useWallets(); - const otherWallets: Wallet[] = allWallets.filter( - (existingWallet) => existingWallet.id !== sender.id + const { wallets } = useWallets({ getAll: true }); + const { allVaults } = useVault({ includeArchived: false }); + const nonHiddenWallets = wallets.filter( + (wallet) => wallet.presentationData.visibility !== VisibilityType.HIDDEN + ); + const allWallets: (Wallet | Vault)[] = [...nonHiddenWallets, ...allVaults].filter( + (item) => item !== null ); + const otherWallets = allWallets.filter((existingWallet) => existingWallet.id !== sender.id); useEffect(() => { InteractionManager.runAfterInteractions(() => { @@ -138,6 +149,14 @@ function SendScreen({ route }) { }); }; + const getWalletIcon = (wallet) => { + if (wallet.entityKind === EntityKind.VAULT) { + return wallet.type === VaultType.COLLABORATIVE ? : ; + } else { + return ; + } + }; + const handleTextChange = (info: string) => { info = info.trim(); const { type: paymentInfoKind, address, amount } = WalletUtilities.addressDiff(info, network); @@ -164,7 +183,7 @@ function SendScreen({ route }) { const renderWallets = ({ item }: { item: Wallet }) => { const onPress = () => { - if (sender.entityKind === 'VAULT') { + if (sender.entityKind === EntityKind.VAULT) { navigateToNext( WalletOperations.getNextFreeAddress(item), TransferType.VAULT_TO_WALLET, @@ -187,8 +206,13 @@ function SendScreen({ route }) { style={{ marginRight: wp(10) }} width={wp(60)} > - - + + @@ -244,6 +268,16 @@ function SendScreen({ route }) { keyExtractor={(item) => item.id} horizontal showsHorizontalScrollIndicator={false} + ListEmptyComponent={ + + + + + You don't have any wallets yet + + + + } /> @@ -257,7 +291,7 @@ function SendScreen({ route }) { title={sender.entityKind === 'VAULT' ? 'Security Tip' : common.note} subtitle={ sender.entityKind === 'VAULT' - ? 'Check the send-to address on a signing device you are going to use to sign the transaction.' + ? 'Check the send-to address on a signer you are going to use to sign the transaction.' : 'Make sure the address or QR is the one where you want to send the funds to' } subtitleColor="GreyText" @@ -328,14 +362,6 @@ const styles = StyleSheet.create({ paddingHorizontal: wp(25), marginTop: hp(5), }, - buttonBackground: { - backgroundColor: '#FAC48B', - width: 40, - height: 40, - borderRadius: 40, - alignItems: 'center', - justifyContent: 'center', - }, noteWrapper: { marginLeft: wp(20), position: 'absolute', @@ -345,5 +371,14 @@ const styles = StyleSheet.create({ sendToWalletWrapper: { marginTop: windowHeight > 680 ? hp(20) : hp(10), }, + emptyWalletsContainer: { + alignItems: 'center', + justifyContent: 'center', + }, + emptyWalletText: { + position: 'absolute', + width: 100, + opacity: 0.8, + }, }); export default SendScreen; diff --git a/src/screens/Send/UTXOSelection.tsx b/src/screens/Send/UTXOSelection.tsx index 3712fab59..f246173cc 100644 --- a/src/screens/Send/UTXOSelection.tsx +++ b/src/screens/Send/UTXOSelection.tsx @@ -1,8 +1,8 @@ import { useNavigation } from '@react-navigation/native'; import { Box, Text, useColorMode } from 'native-base'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { StyleSheet } from 'react-native'; -import { BtcToSats } from 'src/constants/Bitcoin'; +import { BtcToSats, SatsToBtc } from 'src/constants/Bitcoin'; import useBalance from 'src/hooks/useBalance'; import { hp, wp, windowWidth } from 'src/constants/responsive'; @@ -19,25 +19,42 @@ import { TxPriority, WalletType } from 'src/core/wallets/enums'; import UTXOList from 'src/components/UTXOsComponents/UTXOList'; import NoTransactionIcon from 'src/assets/images/no_transaction_icon.svg'; import UTXOSelectionTotal from 'src/components/UTXOsComponents/UTXOSelectionTotal'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { AppStackParams } from 'src/navigation/types'; -function UTXOSelection({ route }: any) { +type ScreenProps = NativeStackScreenProps; +const UTXOSelection = ({ route }: ScreenProps) => { const navigation = useNavigation(); - const { sender, amount, address } = route.params; + const { sender, amount, address } = route.params || {}; const utxos = _.clone(sender.specs.confirmedUTXOs); const { colorMode } = useColorMode(); - const { getSatUnit, getBalance, getCurrencyIcon } = useBalance(); const { showToast } = useToastMessage(); const dispatch = useDispatch(); const { averageTxFees } = useAppSelector((state) => state.network); const [selectionTotal, setSelectionTotal] = useState(0); const [selectedUTXOMap, setSelectedUTXOMap] = useState({}); + const { satsEnabled } = useAppSelector((state) => state.settings); + + const [areEnoughUTXOsSelected, setAreEnoughUTXOsSelected] = useState(false); + const [showFeeErrorMessage, setShowFeeErrorMessage] = useState(false); + + useEffect(() => { + let minimumAvgFeeRequired = averageTxFees[config.NETWORK_TYPE][TxPriority.LOW].averageTxFee; + + let outgoingAmount = Number(amount); + // all comparisons are done in sats + if (satsEnabled === false) { + outgoingAmount = Number(BtcToSats(amount)); + } + const enoughSelected = selectionTotal >= outgoingAmount + minimumAvgFeeRequired; + setAreEnoughUTXOsSelected(enoughSelected); + + const showFeeErr = + outgoingAmount <= selectionTotal && selectionTotal < outgoingAmount + minimumAvgFeeRequired; + + setShowFeeErrorMessage(showFeeErr); + }, [satsEnabled, selectionTotal, amount]); - const minimumAvgFeeRequired = averageTxFees[config.NETWORK_TYPE][TxPriority.LOW].averageTxFee; - const areEnoughUTXOsSelected = - selectionTotal >= Number(BtcToSats(amount)) + Number(minimumAvgFeeRequired); - const showFeeErrorMessage = - selectionTotal >= Number(BtcToSats(amount)) && - selectionTotal < Number(BtcToSats(amount)) + Number(minimumAvgFeeRequired); const executeSendPhaseOne = () => { const recipients = []; if (!selectionTotal) { @@ -46,7 +63,7 @@ function UTXOSelection({ route }: any) { } recipients.push({ address, - amount: BtcToSats(amount), + amount: satsEnabled ? amount : BtcToSats(amount), }); dispatch( sendPhaseOne({ @@ -102,7 +119,7 @@ function UTXOSelection({ route }: any) { ); -} +}; const styles = StyleSheet.create({ wrapper: { backgroundColor: '#FDF7F0', diff --git a/src/screens/Send/WalletSendInfo.tsx b/src/screens/Send/WalletSendInfo.tsx index d5cca05bc..1508839ab 100644 --- a/src/screens/Send/WalletSendInfo.tsx +++ b/src/screens/Send/WalletSendInfo.tsx @@ -1,14 +1,17 @@ -import React from 'react'; +import React, { useContext } from 'react'; import Text from 'src/components/KeeperText'; -import { Box, useColorMode } from 'native-base'; +import { Box, Pressable, useColorMode } from 'native-base'; import { StyleSheet, TouchableOpacity } from 'react-native'; import { hp, wp } from 'src/constants/responsive'; import EditIcon from 'src/assets/images/edit.svg'; import BTCIcon from 'src/assets/images/btc_black.svg'; import BTCWhite from 'src/assets/images/btc_white.svg'; -import IconWallet from 'src/assets/images/icon_wallet.svg'; + import { SatsToBtc } from 'src/constants/Bitcoin'; -import CurrencyInfo from '../HomeScreen/components/CurrencyInfo'; +import CurrencyInfo from '../Home/components/CurrencyInfo'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import Colors from 'src/theme/Colors'; +import HexagonIcon from 'src/components/HexagonIcon'; function WalletSendInfo({ availableAmt = '', @@ -17,29 +20,27 @@ function WalletSendInfo({ isSats = false, currencyIcon = BTCIcon, selectedUTXOs = [], + icon, }) { const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { wallet: walletTranslation } = translations; + return ( - - - - - - + + + + + + + + {walletName} {selectedUTXOs.length ? ( - Sending from selected UTXOs of   + {walletTranslation.sendingFromUtxo}   {colorMode === 'light' ? : }   @@ -47,9 +48,9 @@ function WalletSendInfo({ ) : ( - + - Available to spend  + {walletTranslation.AvailableToSpend} )} + + {/* console.log('pressed')} + backgroundColor={`${colorMode}.accent`} + borderColor={`${colorMode}.learnMoreBorder`} + style={styles.advanceWrapper} + > + + {walletTranslation.advanced} + + */} + {isEditable && ( wallet && wallet.id === collaborativeWalletId ); } - const keeper: KeeperApp = useQuery(RealmSchema.KeeperApp).map(getJSONFromRealmObject)[0]; const { translations } = useContext(LocalizationContext); - const { wallet: walletTransactions, common, vault } = translations; + const { wallet: walletTransactions, common } = translations; const [coldCardModal, setColdCardModal] = useState(false); const [tapsignerModal, setTapsignerModal] = useState(false); const [ledgerModal, setLedgerModal] = useState(false); const [passportModal, setPassportModal] = useState(false); const [seedSignerModal, setSeedSignerModal] = useState(false); + const [specterModal, setSpecterModal] = useState(false); const [keystoneModal, setKeystoneModal] = useState(false); const [jadeModal, setJadeModal] = useState(false); const [keeperModal, setKeeperModal] = useState(false); @@ -90,7 +94,7 @@ function SignTransactionScreen() { const [otpModal, showOTPModal] = useState(false); const [passwordModal, setPasswordModal] = useState(false); - const [activeSignerId, setActiveSignerId] = useState(); + const [activeXfp, setActiveXfp] = useState(); const { showToast } = useToastMessage(); const navigation = useNavigation(); @@ -101,6 +105,7 @@ function SignTransactionScreen() { (state) => state.bhr ); const isMigratingNewVault = useAppSelector((state) => state.vault.isMigratingNewVault); + const intrimVault = useAppSelector((state) => state.vault.intrimVault); const sendSuccessful = useAppSelector((state) => state.sendAndReceive.sendPhaseThree.txid); const sendFailedMessage = useAppSelector( (state) => state.sendAndReceive.sendPhaseThree.failedErrorMessage @@ -120,13 +125,16 @@ function SignTransactionScreen() { { name: 'Home' }, { name: 'VaultDetails', - params: { vaultTransferSuccessful: true, autoRefresh: true, collaborativeWalletId }, + params: { + vaultTransferSuccessful: true, + autoRefresh: true, + vaultId: intrimVault?.id || '', + }, }, ], }; navigation.dispatch(CommonActions.reset(navigationState)); dispatch(resetRealyVaultState()); - dispatch(clearSigningDevice()); } if (relayVaultError) { showToast(`Vault Creation Failed ${realyVaultErrorMessage}`, ); @@ -141,27 +149,20 @@ function SignTransactionScreen() { } } else if (sendSuccessful) { setVisibleModal(true); - // navigation.dispatch( - // CommonActions.reset({ - // index: 1, - // routes: [ - // { name: 'Home' }, - // { name: 'VaultDetails', params: { autoRefresh: true, collaborativeWalletId } }, - // ], - // }) - // ); } }, [sendSuccessful, isMigratingNewVault]); useEffect(() => { - defaultVault.signers.forEach((signer) => { - const isCoSignerMyself = signer.masterFingerprint === collaborativeWalletId; + defaultVault.signers.forEach((vaultKey) => { + const isCoSignerMyself = vaultKey.masterFingerprint === collaborativeWalletId; if (isCoSignerMyself) { // self sign PSBT - signTransaction({ signerId: signer.signerId }); + signTransaction({ xfp: vaultKey.xfp }); } }); - return () => dispatch(sendPhaseThreeReset()); + return () => { + dispatch(sendPhaseThreeReset()); + }; }, []); useEffect(() => { @@ -190,40 +191,42 @@ function SignTransactionScreen() { const signTransaction = useCallback( async ({ - signerId, + xfp, signingServerOTP, seedBasedSingerMnemonic, thresholdDescriptors, }: { - signerId?: string; + xfp?: string; signingServerOTP?: string; seedBasedSingerMnemonic?: string; thresholdDescriptors?: string[]; } = {}) => { - const activeId = signerId || activeSignerId; - const currentSigner = signers.filter((signer) => signer.signerId === activeId)[0]; + const activeId = xfp || activeXfp; + const currentKey = vaultKeys.filter((vaultKey) => vaultKey.xfp === activeId)[0]; + const signer = signerMap[currentKey.masterFingerprint]; if (serializedPSBTEnvelops && serializedPSBTEnvelops.length) { const serializedPSBTEnvelop = serializedPSBTEnvelops.filter( - (envelop) => envelop.signerId === activeId + (envelop) => envelop.xfp === activeId )[0]; const copySerializedPSBTEnvelop = cloneDeep(serializedPSBTEnvelop); - const { signerType, serializedPSBT, signingPayload, signerId } = copySerializedPSBTEnvelop; + const { signerType, serializedPSBT, signingPayload, xfp } = copySerializedPSBTEnvelop; if (SignerType.TAPSIGNER === signerType) { const { signingPayload: signedPayload, signedSerializedPSBT } = await signTransactionWithTapsigner({ setTapsignerModal, signingPayload, - currentSigner, + currentKey, withModal, defaultVault, serializedPSBT, card, cvc: textRef.current, + signer, }); dispatch( - updatePSBTEnvelops({ signedSerializedPSBT, signerId, signingPayload: signedPayload }) + updatePSBTEnvelops({ signedSerializedPSBT, xfp, signingPayload: signedPayload }) ); - dispatch(healthCheckSigner([currentSigner])); + dispatch(healthCheckSigner([signer])); } else if (SignerType.COLDCARD === signerType) { await signTransactionWithColdCard({ setColdCardModal, @@ -237,62 +240,57 @@ function SignTransactionScreen() { signingPayload, defaultVault, serializedPSBT, - signerId, + xfp, }); - dispatch(updatePSBTEnvelops({ signedSerializedPSBT, signerId })); - dispatch(healthCheckSigner([currentSigner])); + dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp })); + dispatch(healthCheckSigner([signer])); } else if (SignerType.POLICY_SERVER === signerType) { const { signedSerializedPSBT } = await signTransactionWithSigningServer({ - signerId, + xfp, signingPayload, signingServerOTP, serializedPSBT, showOTPModal, + showToast, }); - dispatch(updatePSBTEnvelops({ signedSerializedPSBT, signerId })); - dispatch(healthCheckSigner([currentSigner])); + dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp })); + dispatch(healthCheckSigner([signer])); } else if (SignerType.INHERITANCEKEY === signerType) { const { signedSerializedPSBT } = await signTransactionWithInheritanceKey({ signingPayload, serializedPSBT, - signerId, + xfp, thresholdDescriptors, }); - dispatch(updatePSBTEnvelops({ signedSerializedPSBT, signerId })); + dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp })); } else if (SignerType.SEED_WORDS === signerType) { const { signedSerializedPSBT } = await signTransactionWithSeedWords({ signingPayload, defaultVault, seedBasedSingerMnemonic, serializedPSBT, - signerId, + xfp, isMultisig: defaultVault.isMultiSig, }); - dispatch(updatePSBTEnvelops({ signedSerializedPSBT, signerId })); - dispatch(healthCheckSigner([currentSigner])); + dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp })); + dispatch(healthCheckSigner([signer])); } else if (SignerType.KEEPER === signerType) { const signedSerializedPSBT = signCosignerPSBT(parentCollaborativeWallet, serializedPSBT); - dispatch(updatePSBTEnvelops({ signedSerializedPSBT, signerId })); - dispatch(healthCheckSigner([currentSigner])); + dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp })); + dispatch(healthCheckSigner([signer])); } } }, - [activeSignerId, serializedPSBTEnvelops] + [activeXfp, serializedPSBTEnvelops] ); - const callbackForSigners = ({ - type, - signerId, - signerPolicy, - inheritanceKeyInfo, - masterFingerprint, - }: VaultSigner) => { - setActiveSignerId(signerId); + const callbackForSigners = (vaultKey: VaultSigner, signer: Signer) => { + setActiveXfp(vaultKey.xfp); if (areSignaturesSufficient()) { showToast('We already have enough signatures, you can now broadcast.'); return; } - switch (type) { + switch (signer.type) { case SignerType.TAPSIGNER: setTapsignerModal(true); break; @@ -306,17 +304,17 @@ function SignTransactionScreen() { setPasswordModal(true); break; case SignerType.POLICY_SERVER: - if (signerPolicy) { + if (signer.signerPolicy) { const serializedPSBTEnvelop = serializedPSBTEnvelops.filter( - (envelop) => envelop.signerId === signerId + (envelop) => envelop.xfp === vaultKey.xfp )[0]; const outgoing = idx(serializedPSBTEnvelop, (_) => _.signingPayload[0].outgoing); if ( - !signerPolicy.exceptions.none && - outgoing <= signerPolicy.exceptions.transactionAmount + !signer.signerPolicy.exceptions.none && + outgoing <= signer.signerPolicy.exceptions.transactionAmount ) { showToast('Auto-signing, send amount smaller than max no-check amount'); - signTransaction({ signerId }); // case: OTP not required + signTransaction({ xfp: vaultKey.xfp }); // case: OTP not required } else showOTPModal(true); } else showOTPModal(true); break; @@ -325,7 +323,7 @@ function SignTransactionScreen() { CommonActions.navigate({ name: 'InputSeedWordSigner', params: { - signerId, + xfp: vaultKey.xfp, onSuccess: signTransaction, }, }) @@ -334,6 +332,9 @@ function SignTransactionScreen() { case SignerType.PASSPORT: setPassportModal(true); break; + case SignerType.SPECTER: + setSpecterModal(true); + break; case SignerType.SEEDSIGNER: setSeedSignerModal(true); break; @@ -344,8 +345,8 @@ function SignTransactionScreen() { setJadeModal(true); break; case SignerType.KEEPER: - if (masterFingerprint === collaborativeWalletId) { - signTransaction({ signerId }); + if (vaultKey.masterFingerprint === collaborativeWalletId) { + signTransaction({ xfp: vaultKey.xfp }); return; } setKeeperModal(true); @@ -371,7 +372,7 @@ function SignTransactionScreen() { showToast('Signing via Inheritance Key is not available', ); break; default: - showToast(`action not set for ${type}`); + showToast(`action not set for ${signer.type}`); break; } }; @@ -395,26 +396,28 @@ function SignTransactionScreen() { index: 1, routes: [ { name: 'Home' }, - { name: 'VaultDetails', params: { autoRefresh: true, collaborativeWalletId } }, + { name: 'VaultDetails', params: { autoRefresh: true, collaborativeWalletId, vaultId } }, ], }) ); }; return ( + item.signerId} + data={vaultKeys} + keyExtractor={(item) => item.xfp} renderItem={({ item }) => ( callbackForSigners(item)} + vaultKey={item} + callback={() => callbackForSigners(item, signerMap[item.masterFingerprint])} envelops={serializedPSBTEnvelops} + signerMap={signerMap} /> )} /> @@ -435,19 +438,20 @@ function SignTransactionScreen() { }) ); } else { - showToast(`Sorry there aren't enough signatures!`); + showToast("Sorry there aren't enough signatures!"); } }} /> diff --git a/src/screens/SignTransaction/SignWithColdCard.tsx b/src/screens/SignTransaction/SignWithColdCard.tsx index 6aad6fb77..51ff20d7c 100644 --- a/src/screens/SignTransaction/SignWithColdCard.tsx +++ b/src/screens/SignTransaction/SignWithColdCard.tsx @@ -4,7 +4,6 @@ import { Linking, TouchableOpacity } from 'react-native'; import React, { useState } from 'react'; import { VaultSigner } from 'src/core/wallets/interfaces/vault'; import { hp, wp } from 'src/constants/responsive'; - import Arrow from 'src/assets/images/rightarrow.svg'; import KeeperHeader from 'src/components/KeeperHeader'; import KeeperModal from 'src/components/KeeperModal'; @@ -17,10 +16,11 @@ import { } from 'src/hardware/coldcard'; import { useDispatch } from 'react-redux'; import { updatePSBTEnvelops } from 'src/store/reducers/send_and_receive'; -import { updateSignerDetails } from 'src/store/sagaActions/wallets'; +import { updateKeyDetails } from 'src/store/sagaActions/wallets'; import useNfcModal from 'src/hooks/useNfcModal'; import { healthCheckSigner } from 'src/store/sagaActions/bhr'; import useVault from 'src/hooks/useVault'; +import useSignerFromKey from 'src/hooks/useSignerFromKey'; function Card({ message, buttonText, buttonCallBack }) { return ( @@ -63,34 +63,47 @@ function Card({ message, buttonText, buttonCallBack }) { function SignWithColdCard({ route }: { route }) { const { nfcVisible, closeNfc, withNfcModal } = useNfcModal(); const [mk4Helper, showMk4Helper] = useState(false); - const { activeVault: Vault } = useVault(); - const { signer, signTransaction, isMultisig } = route.params as { - signer: VaultSigner; + const { vaultKey, signTransaction, isMultisig, vaultId } = route.params as { + vaultKey: VaultSigner; signTransaction; isMultisig: boolean; + vaultId: string; }; - const { registered } = signer; + const { activeVault } = useVault({ vaultId }); + const { signer } = useSignerFromKey(vaultKey); + const { registered = false } = + vaultKey.registeredVaults.find((info) => info.vaultId === activeVault.id) || {}; const dispatch = useDispatch(); const receiveFromColdCard = async () => withNfcModal(async () => { if (!isMultisig) { const { txn } = await receiveTxHexFromColdCard(); - dispatch(updatePSBTEnvelops({ signerId: signer.signerId, txHex: txn })); + dispatch(updatePSBTEnvelops({ xfp: vaultKey.xfp, txHex: txn })); dispatch(healthCheckSigner([signer])); } else { const { psbt } = await receivePSBTFromColdCard(); - dispatch(updatePSBTEnvelops({ signedSerializedPSBT: psbt, signerId: signer.signerId })); - dispatch(updateSignerDetails(signer, 'registered', true)); + dispatch(updatePSBTEnvelops({ signedSerializedPSBT: psbt, xfp: vaultKey.xfp })); + dispatch( + updateKeyDetails(vaultKey, 'registered', { + registered: true, + vaultId: activeVault.id, + }) + ); dispatch(healthCheckSigner([signer])); } }); const registerCC = async () => withNfcModal(async () => { - await registerToColcard({ vault: Vault }); - dispatch(updateSignerDetails(signer, 'registered', true)); + await registerToColcard({ vault: activeVault }); + dispatch( + updateKeyDetails(vaultKey, 'registered', { + registered: true, + vaultId: activeVault.id, + }) + ); }); const { colorMode } = useColorMode(); return ( @@ -161,7 +174,7 @@ function SignWithColdCard({ route }: { route }) { > Manually Register Mk4 - Please resigister the Vault if not already registered + Please resigister the vault if not already registered diff --git a/src/screens/SignTransaction/SignWithQR.tsx b/src/screens/SignTransaction/SignWithQR.tsx index 8b77af605..1c13cb7ce 100644 --- a/src/screens/SignTransaction/SignWithQR.tsx +++ b/src/screens/SignTransaction/SignWithQR.tsx @@ -16,10 +16,11 @@ import { updateInputsForSeedSigner } from 'src/hardware/seedsigner'; import { updatePSBTEnvelops } from 'src/store/reducers/send_and_receive'; import useVault from 'src/hooks/useVault'; import { getTxHexFromKeystonePSBT } from 'src/hardware/keystone'; -import { updateSignerDetails } from 'src/store/sagaActions/wallets'; +import { updateKeyDetails } from 'src/store/sagaActions/wallets'; import { healthCheckSigner } from 'src/store/sagaActions/bhr'; import DisplayQR from '../QRScreens/DisplayQR'; import ShareWithNfc from '../NFCChannel/ShareWithNfc'; +import useSignerFromKey from 'src/hooks/useSignerFromKey'; function SignWithQR() { const serializedPSBTEnvelops = useAppSelector( @@ -29,14 +30,20 @@ function SignWithQR() { const navigation = useNavigation(); const dispatch = useDispatch(); const { - signer, + vaultKey, collaborativeWalletId = '', - }: { signer: VaultSigner; collaborativeWalletId: string } = route.params as any; + vaultId = '', + }: { + vaultKey: VaultSigner; + collaborativeWalletId: string; + vaultId: string; + } = route.params as any; const { serializedPSBT } = serializedPSBTEnvelops.filter( - (envelop) => signer.signerId === envelop.signerId + (envelop) => vaultKey.xfp === envelop.xfp )[0]; - const { activeVault } = useVault(collaborativeWalletId); + const { activeVault } = useVault({ collaborativeWalletId, vaultId }); const isSingleSig = activeVault.scheme.n === 1; + const { signer } = useSignerFromKey(vaultKey); const signTransaction = (signedSerializedPSBT, resetQR) => { try { @@ -47,18 +54,21 @@ function SignWithQR() { serializedPSBT, signedSerializedPSBT, }); - dispatch( - updatePSBTEnvelops({ signedSerializedPSBT: signedPsbt, signerId: signer.signerId }) - ); + dispatch(updatePSBTEnvelops({ signedSerializedPSBT: signedPsbt, xfp: vaultKey.xfp })); } else if (signer.type === SignerType.KEYSTONE) { const tx = getTxHexFromKeystonePSBT(serializedPSBT, signedSerializedPSBT); - dispatch(updatePSBTEnvelops({ signerId: signer.signerId, txHex: tx.toHex() })); + dispatch(updatePSBTEnvelops({ xfp: vaultKey.xfp, txHex: tx.toHex() })); } else { - dispatch(updatePSBTEnvelops({ signerId: signer.signerId, signedSerializedPSBT })); + dispatch(updatePSBTEnvelops({ xfp: vaultKey.xfp, signedSerializedPSBT })); } } else { - dispatch(updatePSBTEnvelops({ signedSerializedPSBT, signerId: signer.signerId })); - dispatch(updateSignerDetails(signer, 'registered', true)); + dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp: vaultKey.xfp })); + dispatch( + updateKeyDetails(vaultKey, 'registered', { + registered: true, + vaultId: activeVault.id, + }) + ); } dispatch(healthCheckSigner([signer])); navigation.dispatch(CommonActions.navigate({ name: 'SignTransactionScreen', merge: true })); @@ -75,7 +85,7 @@ function SignWithQR() { CommonActions.navigate({ name: 'ScanQR', params: { - title: `Scan Signed Transaction`, + title: 'Scan Signed Transaction', subtitle: 'Please scan until all the QR data has been retrieved', onQrScan: signTransaction, type: signer.type, @@ -85,18 +95,18 @@ function SignWithQR() { const encodeToBytes = signer.type === SignerType.PASSPORT; const navigateToVaultRegistration = () => - navigation.dispatch(CommonActions.navigate('RegisterWithQR', { signer })); + navigation.dispatch(CommonActions.navigate('RegisterWithQR', { vaultKey, vaultId })); return ( - + + {signer.type === SignerType.KEEPER ? ( + + + + ) : null} - {signer.type === SignerType.KEEPER ? ( - - - - ) : null} {}, signer: null } as any } = useRoute(); - const { signTransaction, textRef } = params; + const { params = { signTransaction: () => {} } as any } = useRoute(); + const { signTransaction, textRef, vaultId = '' } = params; const onPressHandler = (digit) => { let temp = cvc; diff --git a/src/screens/SignTransaction/SignerList.tsx b/src/screens/SignTransaction/SignerList.tsx index 1cbc59074..ef3485b5b 100644 --- a/src/screens/SignTransaction/SignerList.tsx +++ b/src/screens/SignTransaction/SignerList.tsx @@ -6,24 +6,27 @@ import CheckIcon from 'src/assets/images/checked.svg'; import Next from 'src/assets/images/icon_arrow.svg'; import React from 'react'; import { SerializedPSBTEnvelop } from 'src/core/wallets/interfaces'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Signer, VaultSigner } from 'src/core/wallets/interfaces/vault'; import moment from 'moment'; import { SDIcons } from '../Vault/SigningDeviceIcons'; const { width } = Dimensions.get('screen'); function SignerList({ - signer, + vaultKey, callback, envelops, + signerMap, }: { - signer: VaultSigner; + vaultKey: VaultSigner; callback: any; envelops: SerializedPSBTEnvelop[]; + signerMap: { [key: string]: Signer }; }) { const hasSignerSigned = !!envelops.filter( - (psbt) => psbt.signerId === signer.signerId && psbt.isSigned + (envelop) => envelop.xfp === vaultKey.xfp && envelop.isSigned ).length; + const signer = signerMap[vaultKey.masterFingerprint]; return ( diff --git a/src/screens/SignTransaction/SignerModals.tsx b/src/screens/SignTransaction/SignerModals.tsx index 707314f75..bf2fd7bb8 100644 --- a/src/screens/SignTransaction/SignerModals.tsx +++ b/src/screens/SignTransaction/SignerModals.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/rules-of-hooks */ import { Alert } from 'react-native'; import Text from 'src/components/KeeperText'; import { Box } from 'native-base'; @@ -19,6 +18,7 @@ import LoginMethod from 'src/models/enums/LoginMethod'; import PassportSVG from 'src/assets/images/illustration_passport.svg'; import ReactNativeBiometrics from 'react-native-biometrics'; import SeedSignerSetup from 'src/assets/images/seedsigner_setup.svg'; +import SpecterSetupImage from 'src/assets/images/illustration_spectre.svg'; import { SignerType } from 'src/core/wallets/enums'; import TapsignerSetupSVG from 'src/assets/images/TapsignerSetup.svg'; import { credsAuthenticated } from 'src/store/reducers/login'; @@ -28,7 +28,7 @@ import BitoxImage from 'src/assets/images/bitboxSetup.svg'; import OtherSDImage from 'src/assets/images/illustration_othersd.svg'; import TrezorSetup from 'src/assets/images/trezor_setup.svg'; import LedgerImage from 'src/assets/images/ledger_image.svg'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Signer, VaultSigner } from 'src/core/wallets/interfaces/vault'; import * as SecureStore from 'src/storage/secure-store'; import Buttons from 'src/components/Buttons'; import useAsync from 'src/hooks/useAsync'; @@ -40,9 +40,11 @@ function ColdCardContent({ register, isMultisig }: { register: boolean; isMultis let message = ''; if (register) { - message = `\u2022 Since this is the first time you are signing with this device, the Mk4 requires for us to register the multisig wallet data before it can sign transactions.`; + message = + '\u2022 Since this is the first time you are signing with this device, the Mk4 requires for us to register the multisig wallet data before it can sign transactions.'; } else if (isMultisig) { - message = `\u2022 Make sure the multisig wallet is registered with the Mk4 before signing the transaction`; + message = + '\u2022 Make sure the multisig wallet is registered with the Mk4 before signing the transaction'; } return ( @@ -54,8 +56,8 @@ function ColdCardContent({ register, isMultisig }: { register: boolean; isMultis {register - ? `` - : `\u2022 On the Mk4 main menu, choose the 'Ready to sign' option and choose the nfc option.`} + ? '' + : "\u2022 On the Mk4 main menu, choose the 'Ready to sign' option and choose the nfc option."} @@ -73,7 +75,7 @@ function PassportContent({ isMultisig }: { isMultisig: boolean }) { }the right bitcoin network is set before signing the transaction`} - {`\u2022 On the Passport main menu, choose the 'Sign with QR Code' option.`} + {"\u2022 On the Passport main menu, choose the 'Sign with QR Code' option."} @@ -87,11 +89,31 @@ function SeedSignerContent({ isMultisig }: { isMultisig: boolean }) { {isMultisig ? ( - {`\u2022 The change address verification step (wallet registration) with SeedSigner shows up at the time of PSBT verification.`} + { + '\u2022 The change address verification step (wallet registration) with SeedSigner shows up at the time of PSBT verification.' + } ) : null} - {`\u2022 On the SeedSigner main menu, choose the 'Scan' option and wait for the QR to be scanned.`} + { + "\u2022 On the SeedSigner main menu, choose the 'Scan' option and wait for the QR to be scanned." + } + + + + ); +} + +function SpecterContent({ isMultisig }: { isMultisig: boolean }) { + return ( + + + + {`\u2022 Make sure ${ + isMultisig ? 'the multisig wallet is registered with the Specter and ' : '' + }the right bitcoin network is set before signing the transaction`} + + {`\u2022 On the Specter main menu, choose the 'Scan QR code' option and wait for the QR to be scanned.`} @@ -124,7 +146,9 @@ function JadeContent() { - {`\u2022 On the Jade main menu, choose the 'Scan' option and wait for the QR to be scanned.`} + { + "\u2022 On the Jade main menu, choose the 'Scan' option and wait for the QR to be scanned." + } @@ -137,7 +161,9 @@ function TrezorContent() { - {`\u2022 The Keeper Harware Interface will exchange the signed/unsigned PSBT from/to the Keeper app and the signing device.`} + { + '\u2022 The Keeper Harware Interface will exchange the signed/unsigned PSBT from/to the Keeper app and the signer.' + } @@ -150,7 +176,9 @@ function BitBox02Content() { - {`\u2022 The Keeper Harware Interface will exchange the signed/unsigned PSBT from/to the Keeper app and the signing device.`} + { + '\u2022 The Keeper Harware Interface will exchange the signed/unsigned PSBT from/to the Keeper app and the signer.' + } @@ -163,7 +191,9 @@ function LedgerContent() { - {`\u2022 The Keeper Harware Interface will exchange the signed/unsigned PSBT from/to the Keeper app and the signing device.`} + { + '\u2022 The Keeper Harware Interface will exchange the signed/unsigned PSBT from/to the Keeper app and the signer.' + } @@ -176,7 +206,7 @@ function OtherSDContent() { - {`\u2022 Either scan or use the export option to transfer the PSBT to the signer.`} + {'\u2022 Either scan or use the export option to transfer the PSBT to the signer.'} @@ -188,7 +218,7 @@ export function KeeperContent() { - {`\u2022 Choose the wallet that was used as a co-signer and select signing PSBT option\n`} + {'\u2022 Choose the wallet that was used as a co-signer and select signing PSBT option\n'} @@ -333,8 +363,7 @@ function OtpContent({ signTransaction }) { color="light.greenText" marginTop={2} > - If you lose your authenticator app, use the other Signing Devices to reset the Signing - Server + If you lose your authenticator app, use the other signers to reset the signer @@ -358,7 +387,8 @@ function OtpContent({ signTransaction }) { } function SignerModals({ - activeSignerId, + vaultId, + activeXfp, coldCardModal, tapsignerModal, ledgerModal, @@ -387,11 +417,15 @@ function SignerModals({ showOTPModal, signTransaction, textRef, - signers, + vaultKeys, isMultisig, collaborativeWalletId, + signerMap, + specterModal, + setSpecterModal, }: { - activeSignerId: string; + vaultId: string; + activeXfp: string; coldCardModal: boolean; tapsignerModal: boolean; ledgerModal: boolean; @@ -420,45 +454,65 @@ function SignerModals({ showOTPModal: any; signTransaction: any; textRef: any; - signers: VaultSigner[]; + vaultKeys: VaultSigner[]; isMultisig: boolean; collaborativeWalletId: string; + signerMap: { [key: string]: Signer }; + specterModal: boolean; + setSpecterModal: any; }) { const navigation = useNavigation(); - const navigateToQrSigning = (signer) => { + const navigateToQrSigning = (vaultKey: VaultSigner) => { setPassportModal(false); setSeedSignerModal(false); setKeeperModal(false); setOtherSDModal(false); setJadeModal(false); + setSpecterModal(false); navigation.dispatch( - CommonActions.navigate('SignWithQR', { signTransaction, signer, collaborativeWalletId }) + CommonActions.navigate('SignWithQR', { + signTransaction, + vaultKey, + collaborativeWalletId, + vaultId, + }) ); }; - const navigateToChannelSigning = (signer) => { + const navigateToChannelSigning = (vaultKey: VaultSigner) => { setTrezorModal(false); setBitbox02Modal(false); setLedgerModal(false); navigation.dispatch( - CommonActions.navigate('SignWithChannel', { signTransaction, signer, collaborativeWalletId }) + CommonActions.navigate('SignWithChannel', { + signTransaction, + vaultKey, + collaborativeWalletId, + vaultId, + }) ); }; return ( <> - {signers.map((signer) => { - const currentSigner = signer.signerId === activeSignerId; + {vaultKeys.map((vaultKey) => { + const signer = signerMap[vaultKey.masterFingerprint]; + const currentSigner = vaultKey.xfp === activeXfp; if (signer.type === SignerType.TAPSIGNER) { const navigateToSignWithTapsigner = () => { setTapsignerModal(false); navigation.dispatch( - CommonActions.navigate('SignWithTapsigner', { signTransaction, signer, textRef }) + CommonActions.navigate('SignWithTapsigner', { + signTransaction, + vaultKey, + textRef, + vaultId, + }) ); }; return ( setTapsignerModal(false)} title="Keep your TAPSIGNER ready" @@ -470,17 +524,22 @@ function SignerModals({ ); } if (signer.type === SignerType.COLDCARD) { - const { registered } = signer; + const info = vaultKey.registeredVaults.find((info) => info.vaultId === vaultId); const navigateToSignWithColdCard = () => { setColdCardModal(false); navigation.dispatch( - CommonActions.navigate('SignWithColdCard', { signTransaction, signer, isMultisig }) + CommonActions.navigate('SignWithColdCard', { + signTransaction, + vaultKey, + isMultisig, + vaultId, + }) ); }; - const shouldRegister = !registered && isMultisig; + const shouldRegister = isMultisig && !info?.registered; return ( setColdCardModal(false)} title={shouldRegister ? 'Register Coldcard' : 'Keep your Mk4 ready'} @@ -494,7 +553,7 @@ function SignerModals({ if (signer.type === SignerType.LEDGER) { return ( { setLedgerModal(false); @@ -504,14 +563,14 @@ function SignerModals({ textColor="light.primaryText" Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToChannelSigning(signer)} + buttonCallback={() => navigateToChannelSigning(vaultKey)} /> ); } if (signer.type === SignerType.MOBILE_KEY) { return ( { setPasswordModal(false); @@ -519,20 +578,25 @@ function SignerModals({ title="Enter your password" subTitle="" textColor="light.primaryText" - Content={() => } + Content={() => ( + + )} /> ); } if (signer.type === SignerType.POLICY_SERVER) { return ( { showOTPModal(false); }} title="Confirm OTP to sign transaction" - subTitle="To sign using signing server key" + subTitle="To sign using signer key" textColor="light.primaryText" Content={() => } /> @@ -541,7 +605,7 @@ function SignerModals({ if (signer.type === SignerType.PASSPORT) { return ( { setPassportModal(false); @@ -551,14 +615,14 @@ function SignerModals({ textColor="light.primaryText" Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToQrSigning(signer)} + buttonCallback={() => navigateToQrSigning(vaultKey)} /> ); } if (signer.type === SignerType.SEEDSIGNER) { return ( { setSeedSignerModal(false); @@ -568,14 +632,31 @@ function SignerModals({ textColor="light.primaryText" Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToQrSigning(signer)} + buttonCallback={() => navigateToQrSigning(vaultKey)} + /> + ); + } + if (signer.type === SignerType.SPECTER) { + return ( + { + setSpecterModal(false); + }} + title="Keep Specter Ready" + subTitle="Keep your Specter ready before proceeding" + textColor="light.primaryText" + Content={() => } + buttonText="Proceed" + buttonCallback={() => navigateToQrSigning(vaultKey)} /> ); } if (signer.type === SignerType.KEYSTONE) { return ( { setKeystoneModal(false); @@ -585,14 +666,14 @@ function SignerModals({ textColor="light.primaryText" Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToQrSigning(signer)} + buttonCallback={() => navigateToQrSigning(vaultKey)} /> ); } if (signer.type === SignerType.JADE) { return ( { setJadeModal(false); @@ -602,14 +683,14 @@ function SignerModals({ textColor="light.primaryText" Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToQrSigning(signer)} + buttonCallback={() => navigateToQrSigning(vaultKey)} /> ); } if (signer.type === SignerType.TREZOR) { return ( { setTrezorModal(false); @@ -619,14 +700,14 @@ function SignerModals({ textColor="light.primaryText" Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToChannelSigning(signer)} + buttonCallback={() => navigateToChannelSigning(vaultKey)} /> ); } if (signer.type === SignerType.BITBOX02) { return ( { setBitbox02Modal(false); @@ -636,14 +717,14 @@ function SignerModals({ textColor="light.primaryText" Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToChannelSigning(signer)} + buttonCallback={() => navigateToChannelSigning(vaultKey)} /> ); } if (signer.type === SignerType.OTHER_SD) { return ( { setOtherSDModal(false); @@ -653,24 +734,24 @@ function SignerModals({ textColor="light.primaryText" Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToQrSigning(signer)} + buttonCallback={() => navigateToQrSigning(vaultKey)} /> ); } if (signer.type === SignerType.KEEPER) { return ( { setKeeperModal(false); }} title="Keep your Device Ready" - subTitle="Keep your Keeper Signing Device ready before proceeding" + subTitle="Keep your Collaborative Key ready before proceeding" textColor="light.primaryText" Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToQrSigning(signer)} + buttonCallback={() => navigateToQrSigning(vaultKey)} /> ); } diff --git a/src/screens/SignTransaction/signWithSD.ts b/src/screens/SignTransaction/signWithSD.ts index d045bb244..0f778575f 100644 --- a/src/screens/SignTransaction/signWithSD.ts +++ b/src/screens/SignTransaction/signWithSD.ts @@ -14,19 +14,20 @@ import SigningServer from 'src/services/operations/SigningServer'; export const signTransactionWithTapsigner = async ({ setTapsignerModal, signingPayload, - currentSigner, + currentKey, withModal, defaultVault, serializedPSBT, card, cvc, + signer, }) => { setTapsignerModal(false); const { inputsToSign } = signingPayload[0]; // AMF flow for signing - if (isSignerAMF(currentSigner)) { + if (isSignerAMF(signer)) { await withModal(() => readTapsigner(card, cvc))(); - const { xpriv } = currentSigner; + const { xpriv } = currentKey; const inputs = idx(signingPayload, (_) => _[0].inputs); if (!inputs) throw new Error('Invalid signing payload, inputs missing'); const { signedSerializedPSBT } = WalletOperations.internallySignVaultPSBT( @@ -38,7 +39,7 @@ export const signTransactionWithTapsigner = async ({ return { signedSerializedPSBT, signingPayload: null }; } return withModal(async () => { - const signedInput = await signWithTapsigner(card, inputsToSign, cvc, currentSigner); + const signedInput = await signWithTapsigner(card, inputsToSign, cvc, currentKey); signingPayload.forEach((payload) => { payload.inputsToSign = signedInput; }); @@ -65,57 +66,17 @@ export const signTransactionWithColdCard = async ({ } }; -export const signTransactionWithLedger = async ({ - setLedgerModal, - currentSigner, - signingPayload, - defaultVault, - serializedPSBT, -}) => { - try { - setLedgerModal(false); - if (isSignerAMF(currentSigner)) { - const { xpriv } = currentSigner; - const inputs = idx(signingPayload, (_) => _[0].inputs); - if (!inputs) throw new Error('Invalid signing payload, inputs missing'); - const { signedSerializedPSBT } = WalletOperations.internallySignVaultPSBT( - defaultVault, - inputs, - serializedPSBT, - xpriv - ); - return { signedSerializedPSBT }; - } - } catch (error) { - switch (error.message) { - case 'Ledger device: UNKNOWN_ERROR (0x6b0c)': - Alert.alert('Unlock the device to connect.'); - break; - case 'Ledger device: UNKNOWN_ERROR (0x6a15)': - Alert.alert('Navigate to the correct app in the Ledger.'); - break; - case 'Ledger device: UNKNOWN_ERROR (0x6511)': - Alert.alert('Open up the correct app in the Ledger.'); // no app selected - break; - // unknown error - default: - captureError(error); - Alert.alert('Something went wrong! Please try again'); - } - } -}; - export const signTransactionWithMobileKey = async ({ setPasswordModal, signingPayload, defaultVault, serializedPSBT, - signerId, + xfp, }) => { setPasswordModal(false); const inputs = idx(signingPayload, (_) => _[0].inputs); if (!inputs) throw new Error('Invalid signing payload, inputs missing'); - const [signer] = defaultVault.signers.filter((signer) => signer.signerId === signerId); + const [signer] = defaultVault.signers.filter((signer) => signer.xfp === xfp); const { signedSerializedPSBT } = WalletOperations.internallySignVaultPSBT( defaultVault, inputs, @@ -126,11 +87,12 @@ export const signTransactionWithMobileKey = async ({ }; export const signTransactionWithSigningServer = async ({ - signerId, + xfp, signingPayload, signingServerOTP, serializedPSBT, showOTPModal, + showToast, }) => { try { showOTPModal(false); @@ -139,24 +101,24 @@ export const signTransactionWithSigningServer = async ({ if (!childIndexArray) throw new Error('Invalid signing payload'); const { signedPSBT } = await SigningServer.signPSBT( - signerId, + xfp, signingServerOTP ? Number(signingServerOTP) : null, serializedPSBT, childIndexArray, outgoing ); - if (!signedPSBT) throw new Error('signing server: failed to sign'); + if (!signedPSBT) throw new Error('signer: failed to sign'); return { signedSerializedPSBT: signedPSBT }; } catch (error) { captureError(error); - Alert.alert(error.message); + showToast(`${error.message}`); } }; export const signTransactionWithInheritanceKey = async ({ signingPayload, serializedPSBT, - signerId, + xfp, thresholdDescriptors, }) => { try { @@ -164,7 +126,7 @@ export const signTransactionWithInheritanceKey = async ({ if (!childIndexArray) throw new Error('Invalid signing payload'); const { signedPSBT } = await InheritanceKeyServer.signPSBT( - signerId, + xfp, serializedPSBT, childIndexArray, thresholdDescriptors @@ -182,13 +144,13 @@ export const signTransactionWithSeedWords = async ({ defaultVault, seedBasedSingerMnemonic, serializedPSBT, - signerId, + xfp, isMultisig, }) => { try { const inputs = idx(signingPayload, (_) => _[0].inputs); if (!inputs) throw new Error('Invalid signing payload, inputs missing'); - const [signer] = defaultVault.signers.filter((signer) => signer.signerId === signerId); + const [signer] = defaultVault.signers.filter((signer) => signer.xfp === xfp); const networkType = config.NETWORK_TYPE; // we need this to generate xpriv that's not stored const { xpub, xpriv } = generateSeedWordsKey( diff --git a/src/screens/SigningDevices/AddIKS.tsx b/src/screens/SigningDevices/AddIKS.tsx index b915b1054..1258387ec 100644 --- a/src/screens/SigningDevices/AddIKS.tsx +++ b/src/screens/SigningDevices/AddIKS.tsx @@ -18,11 +18,11 @@ import Instruction from 'src/components/Instruction'; const config = { Illustration: , Instructions: [ - 'Manually provide the signing device details', - `The hardened part of the derivation path of the xpub has to be denoted with a " h " or " ' ". Please do not use any other charecter`, + 'Manually provide the signer details', + 'The hardened part of the derivation path of the xpub has to be denoted with a " h " or " \' ". Please do not use any other charecter', ], title: 'Setting up the Inheritance Key', - subTitle: 'Keep your signing device ready before proceeding', + subTitle: 'Keep your signer ready before proceeding', }; function AddIKS({ vault, visible, close }: { vault: Vault; visible: boolean; close: () => void }) { const dispatch = useDispatch(); @@ -58,7 +58,7 @@ function AddIKS({ vault, visible, close }: { vault: Vault; visible: boolean; clo }} /> )} - Backup Vault Config (BSMS) + Backup vault Config (BSMS) {config.Instructions.map((instruction) => ( @@ -76,21 +76,21 @@ function AddIKS({ vault, visible, close }: { vault: Vault; visible: boolean; clo setInProgress(true); const { setupData } = await InheritanceKeyServer.initializeIKSetup(); const { id, inheritanceXpub: xpub, derivationPath, masterFingerprint } = setupData; - const inheritanceKey = generateSignerFromMetaData({ + const { signer: inheritanceKey } = generateSignerFromMetaData({ xpub, derivationPath, - xfp: masterFingerprint, + masterFingerprint, signerType: SignerType.INHERITANCEKEY, storageType: SignerStorage.WARM, - signerId: id, + xfp: id, isMultisig: true, }); setInProgress(false); - dispatch(addSigningDevice(inheritanceKey)); + dispatch(addSigningDevice([inheritanceKey])); showToast(`${inheritanceKey.signerName} added successfully`, ); } catch (err) { console.log({ err }); - showToast(`Failed to add inheritance key`, ); + showToast('Failed to add inheritance key', ); } }; diff --git a/src/screens/SigningDevices/InputSeedWordSigner.tsx b/src/screens/SigningDevices/InputSeedWordSigner.tsx index d5f730060..da675f0d0 100644 --- a/src/screens/SigningDevices/InputSeedWordSigner.tsx +++ b/src/screens/SigningDevices/InputSeedWordSigner.tsx @@ -30,7 +30,7 @@ function InputSeedWordSigner({ route }: { route: any }) { const { translations } = useContext(LocalizationContext); const { seed } = translations; const { common } = translations; - const { onSuccess, signerId } = route.params; + const { onSuccess, xfp } = route.params; const [seedData, setSeedData] = useState([ { id: 1, @@ -116,7 +116,7 @@ function InputSeedWordSigner({ route }: { route: any }) { const onPressNext = async () => { const mnemonic = getSeedWord(); if (bip39.validateMnemonic(mnemonic)) { - onSuccess({ signerId, seedBasedSingerMnemonic: mnemonic }); + onSuccess({ xfp, seedBasedSingerMnemonic: mnemonic }); navigation.goBack(); } else Alert.alert('Invalid Mnemonic'); }; @@ -206,8 +206,8 @@ function InputSeedWordSigner({ route }: { route: any }) { styles.input, item.invalid ? { - borderColor: '#F58E6F', - } + borderColor: '#F58E6F', + } : { borderColor: '#FDF7F0' }, ]} placeholder={`enter ${getPlaceholder(index)} word`} diff --git a/src/screens/SigningDevices/ManageSigners.tsx b/src/screens/SigningDevices/ManageSigners.tsx new file mode 100644 index 000000000..e4fcb89aa --- /dev/null +++ b/src/screens/SigningDevices/ManageSigners.tsx @@ -0,0 +1,161 @@ +import React, { useEffect } from 'react'; +import { StyleSheet } from 'react-native'; +import { Box, ScrollView, VStack, useColorMode } from 'native-base'; +import KeeperHeader from 'src/components/KeeperHeader'; +import useSigners from 'src/hooks/useSigners'; +import SignerCard from '../AddSigner/SignerCard'; +import { SDIcons } from 'src/screens/Vault/SigningDeviceIcons'; +import { windowWidth } from 'src/constants/responsive'; +import AddCard from 'src/components/AddCard'; +import { CommonActions, useNavigation } from '@react-navigation/native'; +import useSignerMap from 'src/hooks/useSignerMap'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { AppStackParams } from 'src/navigation/types'; +import ScreenWrapper from 'src/components/ScreenWrapper'; +import HexagonIcon from 'src/components/HexagonIcon'; +import Colors from 'src/theme/Colors'; +import VaultIcon from 'src/assets/images/vault_icon.svg'; +import { UNVERIFYING_SIGNERS } from 'src/hardware'; +import useVault from 'src/hooks/useVault'; +import { Signer, Vault, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { useAppSelector } from 'src/store/hooks'; +import useToastMessage from 'src/hooks/useToastMessage'; +import { resetSignersUpdateState } from 'src/store/reducers/bhr'; +import { useDispatch } from 'react-redux'; + +type ScreenProps = NativeStackScreenProps; +const ManageSigners = ({ route }: ScreenProps) => { + const { colorMode } = useColorMode(); + const navigation = useNavigation(); + const { vaultId = '' } = route.params || {}; + const { activeVault } = useVault({ vaultId }); + const { signers: vaultKeys } = activeVault ? activeVault : { signers: [] }; + const { signerMap } = useSignerMap(); + const { signers } = useSigners(); + const { realySignersUpdateErrorMessage } = useAppSelector((state) => state.bhr); + const { showToast } = useToastMessage(); + const dispatch = useDispatch(); + + useEffect(() => { + if (realySignersUpdateErrorMessage) { + showToast(realySignersUpdateErrorMessage); + dispatch(resetSignersUpdateState()); + } + return () => { + dispatch(resetSignersUpdateState()); + }; + }, [realySignersUpdateErrorMessage]); + + const handleCardSelect = (signer, item) => { + navigation.dispatch( + CommonActions.navigate('SigningDeviceDetails', { + signer, + vaultId, + vaultKey: vaultKeys.length ? item : undefined, + vaultSigners: vaultKeys, + }) + ); + }; + + const handleAddSigner = () => { + navigation.dispatch(CommonActions.navigate('SigningDeviceList', { addSignerFlow: true })); + }; + + return ( + + } + /> + } + /> + + + ); +}; + +const SignersList = ({ + colorMode, + vaultKeys, + signers, + signerMap, + handleCardSelect, + handleAddSigner, + vault, +}: { + colorMode: string; + vaultKeys: VaultSigner[]; + signers: Signer[]; + signerMap: any; + handleCardSelect: any; + handleAddSigner: any; + vault: Vault; +}) => ( + + + + {(vaultKeys.length ? vaultKeys : signers).map((item, i) => { + const signer = vaultKeys.length ? signerMap[item.masterFingerprint] : item; + const isRegistered = vaultKeys.length + ? item.registeredVaults.find((info) => info.vaultId === vault.id) + : false; + + const showDot = + vaultKeys.length && + !UNVERIFYING_SIGNERS.includes(signer.type) && + !isRegistered && + !signer.isMock && + vault.isMultiSig; + + return ( + handleCardSelect(signer, item)} + name={signer.signerName} + description={signer.signerDescription || signer.type} + icon={SDIcons(signer.type, colorMode !== 'dark').Icon} + isSelected={false} + showSelection={false} + showDot={showDot} + /> + ); + })} + + + + +); + +const styles = StyleSheet.create({ + signerContainer: { + marginTop: 30, + }, + scrollContainer: { + gap: 40, + }, + addedSignersContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + }, + addCard: { + height: 125, + width: windowWidth / 3 - windowWidth * 0.05, + margin: 3, + }, +}); + +export default ManageSigners; diff --git a/src/screens/SigningDevices/SetupColdCard.tsx b/src/screens/SigningDevices/SetupColdCard.tsx index ebdf109ce..dcc4bba47 100644 --- a/src/screens/SigningDevices/SetupColdCard.tsx +++ b/src/screens/SigningDevices/SetupColdCard.tsx @@ -1,7 +1,7 @@ import { StyleSheet } from 'react-native'; import { CommonActions, useNavigation } from '@react-navigation/native'; import { SignerStorage, SignerType } from 'src/core/wallets/enums'; -import { getColdcardDetails } from 'src/hardware/coldcard'; +import { getColdcardDetails, getConfigDetails } from 'src/hardware/coldcard'; import { Box, useColorMode } from 'native-base'; import KeeperHeader from 'src/components/KeeperHeader'; @@ -21,11 +21,23 @@ import useAsync from 'src/hooks/useAsync'; import NfcManager from 'react-native-nfc-manager'; import DeviceInfo from 'react-native-device-info'; import { healthCheckSigner } from 'src/store/sagaActions/bhr'; -import { checkSigningDevice } from '../Vault/AddSigningDevice'; import MockWrapper from 'src/screens/Vault/MockWrapper'; -import { InteracationMode } from '../Vault/HardwareModalMap'; import { setSigningDevices } from 'src/store/reducers/bhr'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Signer } from 'src/core/wallets/interfaces/vault'; +import useConfigRecovery from 'src/hooks/useConfigReocvery'; +import useUnkownSigners from 'src/hooks/useUnkownSigners'; +import { InteracationMode } from '../Vault/HardwareModalMap'; + +const getTitle = (mode) => { + switch (mode) { + case InteracationMode.CONFIG_RECOVERY: + return 'Recover Using Configuration'; + case InteracationMode.VAULT_ADDITION: + return 'Setting up Coldcard'; + case InteracationMode.HEALTH_CHECK || InteracationMode.IDENTIFICATION: + return 'Verify Coldcard'; + } +}; function SetupColdCard({ route }) { const { colorMode } = useColorMode(); @@ -35,19 +47,26 @@ function SetupColdCard({ route }) { mode, signer, isMultisig, + addSignerFlow = false, }: { mode: InteracationMode; - signer: VaultSigner; + signer: Signer; isMultisig: boolean; + addSignerFlow?: boolean; } = route.params; const { nfcVisible, withNfcModal, closeNfc } = useNfcModal(); const { showToast } = useToastMessage(); + const { initateRecovery } = useConfigRecovery(); + const { mapUnknownSigner } = useUnkownSigners(); const { start } = useAsync(); + const isConfigRecovery = mode === InteracationMode.CONFIG_RECOVERY; useEffect(() => { NfcManager.isSupported().then((supported) => { if (supported) { if (mode === InteracationMode.HEALTH_CHECK) verifyColdCardWithProgress(); + else if (mode === InteracationMode.CONFIG_RECOVERY) recoverConfigforCC(); + else if (mode === InteracationMode.IDENTIFICATION) verifyColdCardWithProgress(); else { addColdCardWithProgress(); } @@ -70,17 +89,22 @@ function SetupColdCard({ route }) { }; const verifyColdCardWithProgress = async () => { - await start(verifyColdCard); + await start(() => verifyColdCard(mode)); + }; + + const recoverConfigforCC = async () => { + const config = await withNfcModal(async () => getConfigDetails()); + initateRecovery(config); }; const addColdCard = async (mode) => { try { const ccDetails = await withNfcModal(async () => getColdcardDetails(isMultisig)); - const { xpub, derivationPath, xfp, xpubDetails } = ccDetails; - const coldcard = generateSignerFromMetaData({ + const { xpub, derivationPath, masterFingerprint, xpubDetails } = ccDetails; + const { signer: coldcard } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, isMultisig, signerType: SignerType.COLDCARD, storageType: SignerStorage.COLD, @@ -93,30 +117,43 @@ function SetupColdCard({ route }) { CommonActions.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }) ); } else { - dispatch(addSigningDevice(coldcard)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); + dispatch(addSigningDevice([coldcard])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); } showToast(`${coldcard.signerName} added successfully`, ); - const exists = await checkSigningDevice(coldcard.signerId); - if (exists) showToast('Warning: Vault with this signer already exists', ); } catch (error) { handleNFCError(error); } }; - - const verifyColdCard = async () => { + const verifyColdCard = async (mode) => { try { const ccDetails = await withNfcModal(async () => getColdcardDetails(isMultisig)); - const { xpub } = ccDetails; - if (xpub === signer.xpub) { + const { masterFingerprint } = ccDetails; + const ColdCardVerified = () => { dispatch(healthCheckSigner([signer])); navigation.dispatch(CommonActions.goBack()); showToast(`ColdCard verified successfully`, ); - } else { + }; + const showVerificationError = () => { showToast('Something went wrong!', , 3000); + }; + if (mode === InteracationMode.IDENTIFICATION) { + const mapped = mapUnknownSigner({ masterFingerprint, type: SignerType.COLDCARD }); + if (mapped) { + ColdCardVerified(); + } else { + showVerificationError(); + } + } else { + if (masterFingerprint === signer.masterFingerprint) { + ColdCardVerified(); + } else { + showVerificationError(); + } } } catch (error) { if (error instanceof HWError) { @@ -127,17 +164,19 @@ function SetupColdCard({ route }) { } }; - const instructions = `Export the xPub by going to Advanced/Tools > Export wallet > Generic JSON. From here choose the account number and transfer over NFC. Make sure you remember the account you had chosen (This is important for recovering your Vault).\n`; + const instructions = isConfigRecovery + ? `Export the Vault config by going to Setting > Multisig > Then select the wallet > Export` + : `Export the xPub by going to Advanced/Tools > Export wallet > Generic JSON. From here choose the account number and transfer over NFC. Make sure you remember the account you had chosen (This is important for recovering your Vault).\n`; return ( - + - + diff --git a/src/screens/SigningDevices/SetupCollaborativeWallet.tsx b/src/screens/SigningDevices/SetupCollaborativeWallet.tsx index 16ca085fa..f3783696e 100644 --- a/src/screens/SigningDevices/SetupCollaborativeWallet.tsx +++ b/src/screens/SigningDevices/SetupCollaborativeWallet.tsx @@ -3,8 +3,7 @@ import Text from 'src/components/KeeperText'; import { Box, FlatList, HStack, useColorMode, VStack } from 'native-base'; import { CommonActions, useNavigation, useRoute } from '@react-navigation/native'; import React, { useCallback, useEffect, useState } from 'react'; -import { VaultSigner, XpubDetailsType } from 'src/core/wallets/interfaces/vault'; - +import { Signer, VaultSigner } from 'src/core/wallets/interfaces/vault'; import AddIcon from 'src/assets/images/green_add.svg'; import KeeperHeader from 'src/components/KeeperHeader'; import IconArrowBlack from 'src/assets/images/icon_arrow_black.svg'; @@ -13,17 +12,14 @@ import { hp, windowHeight } from 'src/constants/responsive'; import moment from 'moment'; import { useDispatch } from 'react-redux'; import { crossInteractionHandler, getPlaceholder } from 'src/utils/utilities'; -import { generateSignerFromMetaData } from 'src/hardware'; +import { extractKeyFromDescriptor, generateSignerFromMetaData } from 'src/hardware'; import { getCosignerDetails, signCosignerPSBT } from 'src/core/wallets/factories/WalletFactory'; -import { RealmSchema } from 'src/storage/realm/enum'; -import { getJSONFromRealmObject } from 'src/storage/realm/utils'; -import { KeeperApp } from 'src/models/interfaces/KeeperApp'; import { SignerStorage, SignerType, VaultType, XpubTypes } from 'src/core/wallets/enums'; import useToastMessage from 'src/hooks/useToastMessage'; import ToastErrorIcon from 'src/assets/images/toast_error.svg'; import OptionCTA from 'src/components/OptionCTA'; import { NewVaultInfo } from 'src/store/sagas/wallets'; -import { addNewVault } from 'src/store/sagaActions/vaults'; +import { addNewVault, addSigningDevice } from 'src/store/sagaActions/vaults'; import { captureError } from 'src/services/sentry'; import { Wallet } from 'src/core/wallets/interfaces/wallet'; import { useAppSelector } from 'src/store/hooks'; @@ -32,30 +28,33 @@ import { resetVaultFlags } from 'src/store/reducers/vaults'; import { resetRealyVaultState } from 'src/store/reducers/bhr'; import { SDIcons } from '../Vault/SigningDeviceIcons'; import DescriptionModal from '../Vault/components/EditDescriptionModal'; -import { useQuery } from '@realm/react'; import { globalStyles } from 'src/constants/globalStyles'; import FloatingCTA from 'src/components/FloatingCTA'; +import useSignerMap from 'src/hooks/useSignerMap'; const { width } = Dimensions.get('screen'); function SignerItem({ - signer, + vaultKey, index, onQRScan, removeSigner, updateSigner, coSignerFingerprint, + signerMap, }: { - signer: VaultSigner | undefined; + vaultKey: VaultSigner | undefined; index: number; onQRScan: any; removeSigner: any; updateSigner: any; coSignerFingerprint: string; + signerMap: { [key: string]: Signer }; }) { const { colorMode } = useColorMode(); const navigation = useNavigation(); const [visible, setVisible] = useState(false); + const signer = vaultKey ? signerMap[vaultKey.masterFingerprint] : null; const navigateToAddQrBasedSigner = () => { navigation.dispatch( @@ -81,7 +80,7 @@ function SignerItem({ const openDescriptionModal = () => setVisible(true); const closeDescriptionModal = () => setVisible(false); - if (!signer) { + if (!signer || !vaultKey) { return ( @@ -167,11 +166,16 @@ function SignerItem({ - removeSigner(index)} disabled={index === 0}> - - Remove - - + {index !== 0 && ( + removeSigner(index)}> + + Remove + + + )} { const newSigners = coSigners.filter((_, i) => i !== index || index === 0); @@ -258,7 +262,7 @@ function SetupCollaborativeWallet() { const updateSigner = ({ signer, key, value }) => { const newSigners = coSigners.map((item) => { - if (item && item.signerId === signer.signerId) { + if (item && item.xfp === signer.xfp) { return { ...item, [key]: value }; } return item; @@ -287,45 +291,30 @@ function SetupCollaborativeWallet() { } }; - const pushSigner = ( - coSigner: { - deviceId: string; - mfp: string; - xpubDetails: XpubDetailsType; - }, - goBack = true - ) => { + const pushSigner = (xpub, derivationPath, masterFingerprint, goBack = true) => { try { - if (!coSigner.xpubDetails || !coSigner.xpubDetails[XpubTypes.P2WSH].xpub) { - coSigner = JSON.parse(coSigner as any); - if (!coSigner.xpubDetails && !coSigner.xpubDetails[XpubTypes.P2WSH].xpub) { - showToast('Please scan a vaild QR', , 4000); - return; - } - } - const { mfp, xpubDetails } = coSigner; // duplicate check - if (coSigners.find((item) => item && item.xpub === xpubDetails[XpubTypes.P2WSH].xpub)) { + if (coSigners.find((item) => item && item.xpub === xpub)) { showToast('This co-signer has already been added', ); return; } - const ksd = generateSignerFromMetaData({ - xpub: xpubDetails[XpubTypes.P2WSH].xpub, - derivationPath: xpubDetails[XpubTypes.P2WSH].derivationPath, - xfp: mfp, + const { key, signer } = generateSignerFromMetaData({ + xpub, + derivationPath, + masterFingerprint, signerType: SignerType.KEEPER, storageType: SignerStorage.WARM, isMultisig: true, - xpubDetails, }); let addedSigner = false; const newSigners = coSigners.map((item) => { if (!addedSigner && !item) { addedSigner = true; - return ksd; + return key; } return item; }); + dispatch(addSigningDevice([signer])); setCoSigners(newSigners); if (goBack) navigation.goBack(); } catch (err) { @@ -337,8 +326,13 @@ function SetupCollaborativeWallet() { useEffect(() => { setTimeout(() => { - const details = getCosignerDetails(coSigner, keeper.id); - pushSigner(details, false); + const details = getCosignerDetails(coSigner); + pushSigner( + details.xpubDetails[XpubTypes.P2WSH].xpub, + details.xpubDetails[XpubTypes.P2WSH].derivationPath, + details.mfp, + false + ); }, 200); return () => { dispatch(resetVaultFlags()); @@ -353,7 +347,7 @@ function SetupCollaborativeWallet() { index: 1, routes: [ { name: 'Home' }, - { name: 'VaultDetails', params: { collaborativeWalletId: walletId } }, + { name: 'VaultDetails', params: { vaultId: collaborativeWallet.id } }, ], }; navigation.dispatch(CommonActions.reset(navigationState)); @@ -369,12 +363,16 @@ function SetupCollaborativeWallet() { const renderSigner = ({ item, index }) => ( { + const { xpub, masterFingerprint, derivationPath } = extractKeyFromDescriptor(data); + pushSigner(xpub, derivationPath, masterFingerprint); + }} updateSigner={updateSigner} removeSigner={removeSigner} coSignerFingerprint={coSigner.id} + signerMap={signerMap} /> ); @@ -408,7 +406,7 @@ function SetupCollaborativeWallet() { keyboardShouldPersistTaps="always" showsVerticalScrollIndicator={false} data={coSigners} - keyExtractor={(item, index) => item?.signerId ?? index} + keyExtractor={(item, index) => item?.xfp ?? index} renderItem={renderSigner} style={{ marginTop: hp(52), diff --git a/src/screens/SigningDevices/SetupOtherSDScreen.tsx b/src/screens/SigningDevices/SetupOtherSDScreen.tsx index ba47a4a06..5f5994ad0 100644 --- a/src/screens/SigningDevices/SetupOtherSDScreen.tsx +++ b/src/screens/SigningDevices/SetupOtherSDScreen.tsx @@ -14,8 +14,6 @@ import useToastMessage from 'src/hooks/useToastMessage'; import TickIcon from 'src/assets/images/icon_tick.svg'; import ToastErrorIcon from 'src/assets/images/toast_error.svg'; import HWError from 'src/hardware/HWErrorState'; -import { checkSigningDevice } from '../Vault/AddSigningDevice'; -import { InteracationMode } from '../Vault/HardwareModalMap'; import { setSigningDevices } from 'src/store/reducers/bhr'; import { healthCheckSigner } from 'src/store/sagaActions/bhr'; import KeeperTextInput from 'src/components/KeeperTextInput'; @@ -23,11 +21,11 @@ import { pickDocument } from 'src/services/documents'; import { extractColdCardExport } from 'src/hardware/coldcard'; import { getPassportDetails } from 'src/hardware/passport'; import { HWErrorType } from 'src/models/enums/Hardware'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; import OptionCard from 'src/components/OptionCard'; import { getKeystoneDetails, getKeystoneDetailsFromFile } from 'src/hardware/keystone'; import { getSeedSignerDetails } from 'src/hardware/seedsigner'; import { getJadeDetails } from 'src/hardware/jade'; +import { InteracationMode } from '../Vault/HardwareModalMap'; function SetupOtherSDScreen({ route }) { const { colorMode } = useColorMode(); @@ -37,17 +35,17 @@ function SetupOtherSDScreen({ route }) { const dispatch = useDispatch(); const navigation = useNavigation(); const { showToast } = useToastMessage(); - const { mode, signer: hcSigner, isMultisig } = route.params; + const { mode, signer: hcSigner, isMultisig, addSignerFlow = false } = route.params; const validateAndAddSigner = async () => { try { if (!xpub.match(/^([xyYzZtuUvV]pub[1-9A-HJ-NP-Za-km-z]{79,108})$/)) { throw new Error('Please check the xPub format'); } - const signer = generateSignerFromMetaData({ + const { signer, key } = generateSignerFromMetaData({ xpub, derivationPath: derivationPath.replaceAll('h', "'"), - xfp: masterFingerprint, + masterFingerprint, isMultisig, signerType: SignerType.OTHER_SD, storageType: SignerStorage.COLD, @@ -57,20 +55,17 @@ function SetupOtherSDScreen({ route }) { navigation.dispatch( CommonActions.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }) ); - } else if (mode === InteracationMode.SIGNING) { - dispatch(addSigningDevice(signer)); + } else if (mode === InteracationMode.VAULT_ADDITION) { + dispatch(addSigningDevice([signer])); navigation.dispatch( CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) ); showToast(`${signer.signerName} added successfully`, ); - const exsists = await checkSigningDevice(signer.signerId); - if (exsists) - showToast('Warning: Vault with this signer already exisits', ); } else if (mode === InteracationMode.HEALTH_CHECK) { - if (signer.xpub === hcSigner.xpub) { + if (key.xpub === hcSigner.xpub) { dispatch(healthCheckSigner([signer])); navigation.dispatch(CommonActions.goBack()); - showToast(`Other SD verified successfully`, ); + showToast('Other SD verified successfully', ); } else { showToast('Something went wrong!', , 3000); } @@ -90,17 +85,17 @@ function SetupOtherSDScreen({ route }) { try { hw = getPassportDetails(qrData); } catch (e) { - // ignore + // ignore and try other type } try { hw = getSeedSignerDetails(qrData); } catch (error) { - // ignore + // ignore and try other type } try { hw = getKeystoneDetails(qrData); } catch (error) { - // ignore + // ignore and try other type } try { hw = getJadeDetails(qrData); @@ -109,21 +104,22 @@ function SetupOtherSDScreen({ route }) { } if (hw) { - const { xpub, derivationPath, xfp, forMultiSig, forSingleSig } = hw; + const { xpub, derivationPath, masterFingerprint, forMultiSig, forSingleSig } = hw; if ((isMultisig && forMultiSig) || (!isMultisig && forSingleSig)) { - const signer = generateSignerFromMetaData({ + const { signer } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, isMultisig, signerType: SignerType.OTHER_SD, storageType: SignerStorage.COLD, }); if (signer) { - dispatch(addSigningDevice(signer)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); + dispatch(addSigningDevice([signer])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); showToast(`signer added successfully`, ); resetQR(); } @@ -157,20 +153,21 @@ function SetupOtherSDScreen({ route }) { // file export from coldcard or passport(single sig) try { const ccDetails = extractColdCardExport(data, isMultisig); - const { xpub, derivationPath, xfp, xpubDetails } = ccDetails; - const coldcard = generateSignerFromMetaData({ + const { xpub, derivationPath, masterFingerprint, xpubDetails } = ccDetails; + const { signer: coldcard } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, isMultisig, signerType: SignerType.OTHER_SD, storageType: SignerStorage.COLD, xpubDetails, }); - dispatch(addSigningDevice(coldcard)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); + dispatch(addSigningDevice([coldcard])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); return; } catch (e) { error = e; @@ -178,24 +175,22 @@ function SetupOtherSDScreen({ route }) { if (!(error instanceof HWError) || error.type === HWErrorType.INCORRECT_HW) { // file export from passport(multisig) try { - const { xpub, derivationPath, xfp, forMultiSig, forSingleSig } = getPassportDetails(data); + const { xpub, derivationPath, masterFingerprint, forMultiSig, forSingleSig } = + getPassportDetails(data); if ((isMultisig && forMultiSig) || (!isMultisig && forSingleSig)) { - const passport: VaultSigner = generateSignerFromMetaData({ + const { signer: passport } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, signerType: SignerType.OTHER_SD, storageType: SignerStorage.COLD, isMultisig, }); - dispatch(addSigningDevice(passport)); - navigation.dispatch( - CommonActions.navigate({ - name: 'AddSigningDevice', - merge: true, - params: {}, - }) - ); + dispatch(addSigningDevice([passport])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); return; } } catch (e) { @@ -204,25 +199,22 @@ function SetupOtherSDScreen({ route }) { if (!(error instanceof HWError) || error.type === HWErrorType.INCORRECT_HW) { // file export from keystone try { - const { xpub, derivationPath, xfp, forMultiSig, forSingleSig } = + const { xpub, derivationPath, masterFingerprint, forMultiSig, forSingleSig } = getKeystoneDetailsFromFile(data); if ((isMultisig && forMultiSig) || (!isMultisig && forSingleSig)) { - const keystone: VaultSigner = generateSignerFromMetaData({ + const { signer: keystone } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, signerType: SignerType.OTHER_SD, storageType: SignerStorage.COLD, isMultisig, }); - dispatch(addSigningDevice(keystone)); - navigation.dispatch( - CommonActions.navigate({ - name: 'AddSigningDevice', - merge: true, - params: {}, - }) - ); + dispatch(addSigningDevice([keystone])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); return; } } catch (e) { @@ -242,24 +234,22 @@ function SetupOtherSDScreen({ route }) { return ( - + { - const { policy } = route.params; try { const { setupData } = await SigningServer.register(policy); setSetupData(setupData); @@ -53,30 +53,35 @@ function SetupSigningServer({ route }: { route }) { try { const { valid } = await SigningServer.validate(setupData.id, verificationToken); if (valid) setIsSetupValidated(valid); - else showToast('Invalid OTP. Please try again!'); + else { + showValidationModal(false); + showToast('Invalid OTP. Please try again!'); + } } catch (err) { - showToast('Validation failed. Please try again!'); + showValidationModal(false); + showToast(`${err.message}`); } }; const setupSigningServerKey = async () => { const { policy } = route.params; const { id, bhXpub: xpub, derivationPath, masterFingerprint } = setupData; - const signingServerKey = generateSignerFromMetaData({ + const { signer: signingServerKey } = generateSignerFromMetaData({ xpub, derivationPath, - xfp: masterFingerprint, + masterFingerprint, signerType: SignerType.POLICY_SERVER, storageType: SignerStorage.WARM, isMultisig: true, - signerId: id, + xfp: id, signerPolicy: policy, }); - dispatch(addSigningDevice(signingServerKey)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); + dispatch(addSigningDevice([signingServerKey])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); showToast(`${signingServerKey.signerName} added successfully`, ); }; @@ -146,7 +151,7 @@ function SetupSigningServer({ route }: { route }) { - + {validationKey === '' ? ( @@ -177,7 +182,7 @@ function SetupSigningServer({ route }: { route }) { width="100%" numberOfLines={1} > - 2FA Signing Server + 2FA signer @@ -241,7 +246,7 @@ function SetupSigningServer({ route }: { route }) { showValidationModal(false); }} title="Confirm OTP to setup 2FA" - subTitle="To complete setting up the signing server" + subTitle="To complete setting up the signer" textColor="light.primaryText" Content={otpContent} /> diff --git a/src/screens/SigningDevices/SetupTapsigner.tsx b/src/screens/SigningDevices/SetupTapsigner.tsx index 398c4edd3..751f143c1 100644 --- a/src/screens/SigningDevices/SetupTapsigner.tsx +++ b/src/screens/SigningDevices/SetupTapsigner.tsx @@ -1,4 +1,4 @@ -import { Platform, StyleSheet, TextInput } from 'react-native'; +import { Alert, Platform, StyleSheet, TextInput } from 'react-native'; import { Box, useColorMode } from 'native-base'; import { CommonActions, useNavigation } from '@react-navigation/native'; import { ScrollView } from 'react-native-gesture-handler'; @@ -28,15 +28,15 @@ import ScreenWrapper from 'src/components/ScreenWrapper'; import { isTestnet } from 'src/constants/Bitcoin'; import { generateMockExtendedKeyForSigner } from 'src/core/wallets/factories/VaultFactory'; import config from 'src/core/config'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Signer, VaultSigner } from 'src/core/wallets/interfaces/vault'; import useAsync from 'src/hooks/useAsync'; import NfcManager from 'react-native-nfc-manager'; import DeviceInfo from 'react-native-device-info'; import { healthCheckSigner } from 'src/store/sagaActions/bhr'; -import { checkSigningDevice } from '../Vault/AddSigningDevice'; import MockWrapper from 'src/screens/Vault/MockWrapper'; import { InteracationMode } from '../Vault/HardwareModalMap'; import { setSigningDevices } from 'src/store/reducers/bhr'; +import useUnkownSigners from 'src/hooks/useUnkownSigners'; function SetupTapsigner({ route }) { const { colorMode } = useColorMode(); @@ -44,8 +44,21 @@ function SetupTapsigner({ route }) { const navigation = useNavigation(); const card = React.useRef(new CKTapCard()).current; const { withModal, nfcVisible, closeNfc } = useTapsignerModal(card); - const { mode, signer, isMultisig } = route.params; + const { + mode, + signer, + isMultisig, + addSignerFlow = false, + }: { + mode: InteracationMode; + signer: Signer; + isMultisig: boolean; + addSignerFlow?: boolean; + } = route.params; + const { mapUnknownSigner } = useUnkownSigners(); + const isConfigRecovery = mode === InteracationMode.CONFIG_RECOVERY; const isHealthcheck = mode === InteracationMode.HEALTH_CHECK; + const onPressHandler = (digit) => { let temp = cvc; if (digit !== 'x') { @@ -79,20 +92,21 @@ function SetupTapsigner({ route }) { const addTapsigner = React.useCallback(async () => { try { - const { xpub, derivationPath, xfp, xpubDetails } = await withModal(async () => + const { xpub, derivationPath, masterFingerprint, xpubDetails } = await withModal(async () => getTapsignerDetails(card, cvc, isMultisig) )(); - let tapsigner: VaultSigner; + let tapsigner: Signer; + let vaultKey: VaultSigner; if (isAMF) { const { xpub, xpriv, derivationPath, masterFingerprint } = generateMockExtendedKeyForSigner( EntityKind.VAULT, SignerType.TAPSIGNER, config.NETWORK_TYPE ); - tapsigner = generateSignerFromMetaData({ + const { signer, key } = generateSignerFromMetaData({ xpub, derivationPath, - xfp: masterFingerprint, + masterFingerprint, signerType: SignerType.TAPSIGNER, storageType: SignerStorage.COLD, isMultisig, @@ -100,35 +114,34 @@ function SetupTapsigner({ route }) { isMock: false, xpubDetails: { [XpubTypes.AMF]: { xpub, derivationPath } }, }); + tapsigner = signer; + vaultKey = key; } else { - tapsigner = generateSignerFromMetaData({ + const { signer, key } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, signerType: SignerType.TAPSIGNER, storageType: SignerStorage.COLD, isMultisig, xpubDetails, }); + tapsigner = signer; + vaultKey = key; } - if (mode === InteracationMode.SIGNING) { - dispatch(addSigningDevice(tapsigner)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); - } else { + if (mode === InteracationMode.RECOVERY) { dispatch(setSigningDevices(tapsigner)); navigation.dispatch( CommonActions.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }) ); + } else { + dispatch(addSigningDevice([tapsigner])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); } - showToast(`${tapsigner.signerName} added successfully`, ); - if (!isSignerAMF(tapsigner)) { - const exsists = await checkSigningDevice(tapsigner.signerId); - if (exsists) - showToast('Warning: Vault with this signer already exisits', , 3000); - } } catch (error) { const errorMessage = getTapsignerErrorMessage(error); if (errorMessage.includes('cvc retry')) { @@ -150,13 +163,32 @@ function SetupTapsigner({ route }) { const verifyTapsginer = React.useCallback(async () => { try { - const { xpub } = await withModal(async () => getTapsignerDetails(card, cvc, isMultisig))(); - if (xpub === signer.xpub) { + const { masterFingerprint } = await withModal(async () => + getTapsignerDetails(card, cvc, isMultisig) + )(); + const handleSuccess = () => { dispatch(healthCheckSigner([signer])); navigation.dispatch(CommonActions.goBack()); showToast(`Tapsigner verified successfully`, ); - } else { + }; + + const handleFailure = () => { showToast('Something went wrong, please try again!', null, 2000, true); + }; + + if (mode === InteracationMode.IDENTIFICATION) { + const mapped = mapUnknownSigner({ masterFingerprint, type: SignerType.COLDCARD }); + if (mapped) { + handleSuccess(); + } else { + handleFailure(); + } + } else { + if (masterFingerprint === signer.masterFingerprint) { + handleSuccess(); + } else { + handleFailure(); + } } } catch (error) { const errorMessage = getTapsignerErrorMessage(error); @@ -183,7 +215,12 @@ function SetupTapsigner({ route }) { title={isHealthcheck ? 'Verify TAPSIGNER' : 'Setting up TAPSIGNER'} subtitle="Enter the 6-32 digit pin (default one is printed on the back)" /> - + - {`${onboarding.Comprehensive} `} - {onboarding.security} - {` ${onboarding.slide01Title}`} - - ), + title: `${onboarding.Comprehensive} ${onboarding.security} ${onboarding.slide01Title}`, paragraph: onboarding.slide01Paragraph, illustration: , }, { id: 2, - title: ( - <> - {`${onboarding.slide02Title} `} - - {onboarding.privacy} - - - ), + title: <>{`${onboarding.slide02Title} ${onboarding.privacy}`}, paragraph: onboarding.slide02Paragraph, illustration: , }, { id: 3, - title: ( - <> - {`${onboarding.slide07Title} `} - - {onboarding.whirlpool} - - - ), - paragraph: onboarding.slide07Paragraph, - illustration: , + title: onboarding.slide08Title, + paragraph: onboarding.slide08Paragraph, + illustration: , }, ]); @@ -138,7 +118,7 @@ function OnBoardingSlides({ navigation }) { - + {currentPosition < items.length - 1 ? ( items.map((item, index) => ( ); } - -function UTXOManagement({ route, navigation }) { +type ScreenProps = NativeStackScreenProps; +function UTXOManagement({ route, navigation }: ScreenProps) { const { colorMode } = useColorMode(); const dispatch = useAppDispatch(); const styles = getStyles(); - const { - data, - routeName, - accountType, - }: { data: Wallet | Vault; routeName: string; accountType: string } = route.params || {}; + const { data, routeName, accountType, vaultId = '' } = route.params || {}; const [enableSelection, _setEnableSelection] = useState(false); const [selectionTotal, setSelectionTotal] = useState(0); const [selectedUTXOMap, setSelectedUTXOMap] = useState({}); const { id } = data; - const wallet = useWallets({ walletIds: [id] }).wallets[0]; + const wallet = vaultId + ? useVault({ vaultId }).activeVault + : useWallets({ walletIds: [id] }).wallets[0]; const whirlpoolWalletAccountMap: whirlpoolWalletAccountMapInterface = useWhirlpoolWallets({ wallets: [wallet], })?.[wallet.id]; - const isWhirlpoolWallet = Boolean(wallet?.whirlpoolConfig?.whirlpoolWalletDetails); + const isWhirlpoolWallet = vaultId + ? false + : Boolean(wallet?.whirlpoolConfig?.whirlpoolWalletDetails); const [selectedWallet, setSelectedWallet] = useState(wallet); const [selectedAccount, setSelectedAccount] = useState(); const [depositWallet, setDepositWallet] = useState(); @@ -253,7 +257,11 @@ function UTXOManagement({ route, navigation }) { return ( - setLearnModalVisible(true)} /> + setLearnModalVisible(true)} + learnTextColor={`${colorMode}.white`} + /> {isWhirlpoolWallet ? ( ) : null} diff --git a/src/screens/Vault/AddSigningDevice.tsx b/src/screens/Vault/AddSigningDevice.tsx index f88f3f926..b548bb4b4 100644 --- a/src/screens/Vault/AddSigningDevice.tsx +++ b/src/screens/Vault/AddSigningDevice.tsx @@ -1,251 +1,374 @@ -import { Dimensions, Pressable, StyleSheet } from 'react-native'; -import Text from 'src/components/KeeperText'; -import { Box, FlatList, HStack, useColorMode, VStack } from 'native-base'; +import { Dimensions, ScrollView, StyleSheet } from 'react-native'; +import { Box, useColorMode } from 'native-base'; import { CommonActions, useNavigation, useRoute } from '@react-navigation/native'; import React, { useContext, useEffect, useState } from 'react'; -import { VaultScheme, VaultSigner } from 'src/core/wallets/interfaces/vault'; -import { SignerType } from 'src/core/wallets/enums'; import { - addSigningDevice, - removeSigningDevice, - updateSigningDevice, -} from 'src/store/reducers/vaults'; - -import AddIcon from 'src/assets/images/green_add.svg'; + Signer, + Vault, + VaultScheme, + VaultSigner, + signerXpubs, +} from 'src/core/wallets/interfaces/vault'; +import { SignerType, XpubTypes } from 'src/core/wallets/enums'; import Buttons from 'src/components/Buttons'; import KeeperHeader from 'src/components/KeeperHeader'; -import IconArrowBlack from 'src/assets/images/icon_arrow_black.svg'; -import IconArrowGray from 'src/assets/images/icon_arrow_grey.svg'; import { LocalizationContext } from 'src/context/Localization/LocContext'; import Note from 'src/components/Note/Note'; -import Relay from 'src/services/operations/Relay'; import ScreenWrapper from 'src/components/ScreenWrapper'; -import { hp, windowHeight, wp } from 'src/constants/responsive'; -import moment from 'moment'; +import { hp, windowWidth, wp } from 'src/constants/responsive'; import { useAppSelector } from 'src/store/hooks'; -import { useDispatch } from 'react-redux'; -import { getPlaceholder } from 'src/utils/utilities'; -import { getSignerSigTypeInfo } from 'src/hardware'; -import useVault from 'src/hooks/useVault'; import useSignerIntel from 'src/hooks/useSignerIntel'; -import { globalStyles } from 'src/constants/globalStyles'; import { SDIcons } from './SigningDeviceIcons'; -import DescriptionModal from './components/EditDescriptionModal'; import VaultMigrationController from './VaultMigrationController'; -import AddIKS from '../SigningDevices/AddIKS'; +import useSigners from 'src/hooks/useSigners'; +import SignerCard from '../AddSigner/SignerCard'; +import AddCard from 'src/components/AddCard'; +import useToastMessage from 'src/hooks/useToastMessage'; +import useSignerMap from 'src/hooks/useSignerMap'; +import WalletUtilities from 'src/core/wallets/operations/utils'; +import config from 'src/core/config'; +import useVault from 'src/hooks/useVault'; +import VaultIcon from 'src/assets/images/vault_icon.svg'; +import HexagonIcon from 'src/components/HexagonIcon'; +import Colors from 'src/theme/Colors'; +import { useDispatch } from 'react-redux'; +import { resetSignersUpdateState } from 'src/store/reducers/bhr'; const { width } = Dimensions.get('screen'); -export const checkSigningDevice = async (id) => { - try { - const exisits = await Relay.getSignerIdInfo(id); - return exisits; - } catch (err) { - // ignoring temporarily if the network call fails - return true; +const getKeyForScheme = (isMock, isMultisig, signer, msXpub, ssXpub, amfXpub) => { + if (amfXpub) { + return { + ...amfXpub, + masterFingerprint: signer.masterFingerprint, + xfp: WalletUtilities.getFingerprintFromExtendedKey( + amfXpub.xpub, + WalletUtilities.getNetworkByType(config.NETWORK_TYPE) + ), + }; + } + if (isMock || isMultisig) { + return { + ...msXpub, + masterFingerprint: signer.masterFingerprint, + xfp: WalletUtilities.getFingerprintFromExtendedKey( + msXpub.xpub, + WalletUtilities.getNetworkByType(config.NETWORK_TYPE) + ), + }; + } else { + return { + ...ssXpub, + masterFingerprint: signer.masterFingerprint, + xfp: WalletUtilities.getFingerprintFromExtendedKey( + ssXpub.xpub, + WalletUtilities.getNetworkByType(config.NETWORK_TYPE) + ), + }; } }; -function SignerItem({ - signer, - index, - setInheritanceInit, - isInheritance, +const onSignerSelect = ( + selected, + signer: Signer, scheme, -}: { - signer: VaultSigner | undefined; - index: number; - setInheritanceInit: any; - isInheritance: boolean; - scheme: { m: number; n: number }; -}) { - const { colorMode } = useColorMode(); - const dispatch = useDispatch(); - const navigation = useNavigation(); - const [visible, setVisible] = useState(false); + vaultKeys, + setVaultKeys, + selectedSigners, + setSelectedSigners, + showToast +) => { + const amfXpub: signerXpubs[XpubTypes][0] = signer.signerXpubs[XpubTypes.AMF][0]; + const ssXpub: signerXpubs[XpubTypes][0] = signer.signerXpubs[XpubTypes.P2WPKH][0]; + const msXpub: signerXpubs[XpubTypes][0] = signer.signerXpubs[XpubTypes.P2WSH][0]; - const removeSigner = () => dispatch(removeSigningDevice(signer)); - const navigateToSignerList = () => - navigation.dispatch(CommonActions.navigate('SigningDeviceList', { scheme })); + const isMock = signer.isMock; + const isAmf = !!amfXpub; + const isMultisig = msXpub && scheme.n > 1; - const callback = () => { - if (index === 5 && isInheritance) { - setInheritanceInit(true); - } else { - navigateToSignerList(); + if (selected) { + const updated = selectedSigners.delete(signer.masterFingerprint); + if (updated) { + if (isMock) { + const updatedKeys = vaultKeys.filter((key) => msXpub && key.xpub !== msXpub.xpub); + setVaultKeys(updatedKeys); + } else if (isAmf) { + const updatedKeys = vaultKeys.filter((key) => amfXpub && key.xpub !== amfXpub.xpub); + setVaultKeys(updatedKeys); + } else if (isMultisig) { + const updatedKeys = vaultKeys.filter((key) => key.xpub !== msXpub.xpub); + setVaultKeys(updatedKeys); + } else { + const updatedKeys = vaultKeys.filter((key) => key.xpub !== ssXpub.xpub); + setVaultKeys(updatedKeys); + } + setSelectedSigners(new Map(selectedSigners)); } - }; - const openDescriptionModal = () => setVisible(true); - const closeDescriptionModal = () => setVisible(false); + } else { + if (selectedSigners.size >= scheme.n) { + showToast('You have selected the total (n) keys, please proceed with the creation of vault.'); + return; + } + const scriptKey = getKeyForScheme(isMock, isMultisig, signer, msXpub, ssXpub, amfXpub); + vaultKeys.push(scriptKey); + setVaultKeys(vaultKeys); + const updatedSignerMap = selectedSigners.set(signer.masterFingerprint, true); + setSelectedSigners(new Map(updatedSignerMap)); + } +}; - if (!signer) { - return ( - - - - - - - - {`Add ${getPlaceholder(index)} Signing Device`} - - - Select signing device - - - - - {colorMode === 'light' ? : } - - - - - ); +const isSignerValidForScheme = (signer: Signer, scheme, allVaults: Vault[], signerMap) => { + const amfXpub = signer.signerXpubs[XpubTypes.AMF][0]; + const ssXpub = signer.signerXpubs[XpubTypes.P2WPKH][0]; + const msXpub = signer.signerXpubs[XpubTypes.P2WSH][0]; + if ( + (scheme.n > 1 && !msXpub && !amfXpub && !signer.isMock) || + (scheme.n === 1 && !ssXpub && !amfXpub && !signer.isMock) + ) { + return false; } - const { isSingleSig, isMultiSig } = getSignerSigTypeInfo(signer); - let shouldReconfigure = false; - if ((scheme.n === 1 && !isSingleSig) || (scheme.n !== 1 && !isMultiSig)) { - shouldReconfigure = true; + + if (signer.type === SignerType.POLICY_SERVER) { + if (scheme.m < 2 || scheme.n < 3) return false; // signing server key can be added for Vaults w/ m: 2 and n:3 + } else if (signer.type === SignerType.INHERITANCEKEY) { + // inheritance key can be added for Vaults w/ at least 5 keys + if (scheme.m < 3 || scheme.n < 5) return false; + + // TEMP: Disabling multiple IKS + let IKSExists = false; + for (const vault of allVaults) { + vault.signers.forEach((key) => { + if (signerMap[key.masterFingerprint]?.type === SignerType.INHERITANCEKEY) IKSExists = true; + }); + } + if (IKSExists) return false; } - return ( - - - - - {SDIcons(signer.type, true).Icon} - - - - {`${signer.signerName}`} - {` (${signer.masterFingerprint})`} - - - {`Added ${moment(signer.lastHealthCheck).calendar()}`} - - - - - {signer.signerDescription ? signer.signerDescription : 'Add Description'} - - - - - - removeSigner()}> - - {shouldReconfigure ? 'Re-configure' : 'Remove'} - - - - - dispatch(updateSigningDevice({ signer, key: 'signerDescription', value })) + + return true; +}; + +const setInitialKeys = ( + activeVault, + scheme, + allVaults, + signerMap, + setVaultKeys, + setSelectedSigners +) => { + if (activeVault) { + // setting initital keys (update if scheme has changed) + const vaultKeys = activeVault.signers; + const isMultisig = scheme.n > 1; + const modifiedVaultKeysForScriptType = []; + const updatedSignerMap = new Map(); + vaultKeys.forEach((key) => { + const signer = signerMap[key.masterFingerprint]; + if (isSignerValidForScheme(signer, scheme, allVaults, signerMap)) { + if (modifiedVaultKeysForScriptType.length < scheme.n) { + updatedSignerMap.set(key.masterFingerprint, true); + const msXpub: signerXpubs[XpubTypes][0] = signer.signerXpubs[XpubTypes.P2WSH][0]; + const ssXpub: signerXpubs[XpubTypes][0] = signer.signerXpubs[XpubTypes.P2WPKH][0]; + const amfXpub: signerXpubs[XpubTypes][0] = signer.signerXpubs[XpubTypes.AMF][0]; + const scriptKey = getKeyForScheme( + signer.isMock, + isMultisig, + signer, + msXpub, + ssXpub, + amfXpub + ); + if (scriptKey) { + modifiedVaultKeysForScriptType.push(scriptKey); + } } + } + }); + setVaultKeys(modifiedVaultKeysForScriptType); + setSelectedSigners(new Map(updatedSignerMap)); + } +}; + +const Footer = ({ + amfSigners, + invalidSS, + invalidIKS, + trezorIncompatible, + invalidMessage, + areSignersValid, + relayVaultUpdateLoading, + common, + colorMode, + setCreating, + navigation, +}) => { + const renderNotes = () => { + let notes = []; + if (!!amfSigners.length) { + const message = `* ${amfSigners.join( + ' and ' + )} does not support Testnet directly, so the app creates a proxy Testnet key for you in the beta app`; + notes.push( + + + + ); + } + if (invalidSS || invalidIKS) { + const message = invalidMessage; + notes.push( + + + + ); + } + if (trezorIncompatible) { + const message = + 'Trezor multisig is coming soon. Please replace it for now or use it with a sigle sig vault'; + notes.push( + + + + ); + } + if (!notes.length) { + const message = 'You can easily change one or more signers after the vault is setup'; + notes.push( + + + + ); + } + return notes; + }; + return ( + + {renderNotes()} + setCreating(true)} + paddingHorizontal={wp(30)} /> ); -} +}; + +const Signers = ({ + signers, + selectedSigners, + setSelectedSigners, + scheme, + colorMode, + vaultKeys, + setVaultKeys, + showToast, + navigation, + vaultId, + allVaults, + signerMap, +}) => { + const renderSigners = () => { + return signers.map((signer) => { + const disabled = !isSignerValidForScheme(signer, scheme, allVaults, signerMap); + return ( + + onSignerSelect( + selected, + signer, + scheme, + vaultKeys, + setVaultKeys, + selectedSigners, + setSelectedSigners, + showToast + ) + } + /> + ); + }); + }; + return ( + + + {renderSigners()} + + navigation.dispatch( + CommonActions.navigate('SigningDeviceList', { + scheme, + vaultId, + vaultSigners: vaultKeys, + }) + ) + } + /> + + + ); +}; function AddSigningDevice() { const { colorMode } = useColorMode(); const [vaultCreating, setCreating] = useState(false); - const { activeVault } = useVault(); const navigation = useNavigation(); const route = useRoute() as { - params: { isInheritance: boolean; scheme: VaultScheme; name: string; description: string }; + params: { + isInheritance: boolean; + scheme: VaultScheme; + name: string; + description: string; + vaultId: string; + }; }; - const dispatch = useDispatch(); - const vaultSigners = useAppSelector((state) => state.vault.signers); + const { + name = 'Vault', + description = '', + isInheritance = false, + vaultId = '', + scheme, + } = route.params; + const { showToast } = useToastMessage(); const { relayVaultUpdateLoading } = useAppSelector((state) => state.bhr); const { translations } = useContext(LocalizationContext); - const { common } = translations; - const [inheritanceInit, setInheritanceInit] = useState(false); - - const { name = 'Vault', description = 'Secure your sats', isInheritance = false } = route.params; - let { scheme } = route.params; - if (scheme && isInheritance) { - scheme = { m: scheme.m, n: scheme.n + 1 }; - } else if (!scheme && activeVault && !isInheritance) { - scheme = activeVault.scheme; - // added temporarily until we support multiple vaults - } else if (!scheme && activeVault && isInheritance) { - scheme = { m: 3, n: 6 }; - } + const { common, signer } = translations; + const { signers } = useSigners(); + const { signerMap } = useSignerMap(); + const [selectedSigners, setSelectedSigners] = useState(new Map()); + const [vaultKeys, setVaultKeys] = useState([]); + const { activeVault, allVaults } = useVault({ vaultId }); + const { areSignersValid, amfSigners, invalidSS, invalidIKS, invalidMessage } = useSignerIntel({ + scheme, + vaultKeys, + selectedSigners, + existingKeys: activeVault?.signers || [], + }); - const { - signersState, - areSignersValid, - amfSigners, - misMatchedSigners, - invalidSS, - invalidIKS, - invalidMessage, - } = useSignerIntel({ scheme }); + const { realySignersUpdateErrorMessage } = useAppSelector((state) => state.bhr); + const dispatch = useDispatch(); useEffect(() => { - if (activeVault && !vaultSigners.length) { - dispatch(addSigningDevice(activeVault.signers)); + if (realySignersUpdateErrorMessage) { + showToast(realySignersUpdateErrorMessage); + dispatch(resetSignersUpdateState()); } - }, []); + return () => { + dispatch(resetSignersUpdateState()); + }; + }, [realySignersUpdateErrorMessage]); - const triggerVaultCreation = () => { - setCreating(true); - }; - - const renderSigner = ({ item, index }) => ( - - ); - - const preTitle = 'Add Vault Signing Devices'; + useEffect(() => { + setInitialKeys(activeVault, scheme, allVaults, signerMap, setVaultKeys, setSelectedSigners); + }, []); const subtitle = scheme.n > 1 @@ -254,81 +377,67 @@ function AddSigningDevice() { }` : `Vault with ${scheme.m} of ${scheme.n} setup will be created`; - const trezorIncompatible = - scheme.n > 1 && signersState.find((signer) => signer && signer.type === SignerType.TREZOR); + let trezorIncompatible = false; + if (scheme.n > 1) { + for (const mfp of selectedSigners.keys()) { + if (signerMap[mfp].type === SignerType.TREZOR) { + trezorIncompatible = true; + break; + } + } + } + //TODO: add learn more modal return ( - + } + /> + } + //To-Do-Learn-More + /> - item?.signerId ?? index} - renderItem={renderSigner} - style={{ - marginTop: hp(52), - }} + - - {amfSigners.length ? ( - - - - ) : null} - {invalidSS || invalidIKS ? ( - - - - ) : misMatchedSigners.length ? ( - - - - ) : trezorIncompatible ? ( - - - - ) : null} - { - navigation.goBack(); - }} - paddingHorizontal={wp(30)} - /> - - setInheritanceInit(false)} +
); @@ -341,6 +450,10 @@ const styles = StyleSheet.create({ marginHorizontal: 10, marginBottom: hp(25), }, + addedSigners: { + flexDirection: 'row', + flexWrap: 'wrap', + }, signerItem: { alignItems: 'center', justifyContent: 'space-between', @@ -355,6 +468,7 @@ const styles = StyleSheet.create({ }, bottomContainer: { paddingHorizontal: 15, + gap: 20, }, noteContainer: { width: wp(330), @@ -378,6 +492,17 @@ const styles = StyleSheet.create({ width: '15%', alignItems: 'center', }, + signerContainer: { + width: windowWidth, + gap: 40, + paddingBottom: 20, + marginTop: 20, + }, + addCard: { + height: 125, + width: windowWidth / 3 - windowWidth * 0.05, + margin: 3, + }, }); export default AddSigningDevice; diff --git a/src/screens/Vault/AllTransactions.tsx b/src/screens/Vault/AllTransactions.tsx index a32f30cf8..357081a8b 100644 --- a/src/screens/Vault/AllTransactions.tsx +++ b/src/screens/Vault/AllTransactions.tsx @@ -15,11 +15,8 @@ import ScreenWrapper from 'src/components/ScreenWrapper'; function AllTransactions({ route }) { const dispatch = useDispatch(); - const title = route?.params?.title; - const entityKind = route?.params?.entityKind; - const subtitle = route?.params?.subtitle; - const collaborativeWalletId = route?.params?.collaborativeWalletId; - const { activeVault: vault } = useVault(collaborativeWalletId); + const { title, entityKind, subtitle, collaborativeWalletId = '', vaultId = '' } = route?.params; + const { activeVault: vault } = useVault({ collaborativeWalletId, vaultId }); const wallet: Wallet = useQuery(RealmSchema.Wallet) .map(getJSONFromRealmObject) diff --git a/src/screens/Vault/ArchivedVault.tsx b/src/screens/Vault/ArchivedVault.tsx index c62e13c73..b5507d4c5 100644 --- a/src/screens/Vault/ArchivedVault.tsx +++ b/src/screens/Vault/ArchivedVault.tsx @@ -12,17 +12,27 @@ import BTC from 'src/assets/images/btc_black.svg'; import useBalance from 'src/hooks/useBalance'; import { StyleSheet } from 'react-native'; import { useQuery } from '@realm/react'; +import { CommonActions } from '@react-navigation/native'; -function ArchivedVault() { +function ArchivedVault({ navigation }) { const { colorMode } = useColorMode(); const vault: Vault[] = useQuery(RealmSchema.Vault) .map(getJSONFromRealmObject) .filter((vault) => vault.archived); const { getBalance } = useBalance(); - function VaultItem({ vaultItem, index }: { vaultItem: Vault; index: number }) { + function VaultItem({ vaultItem }: { vaultItem: Vault }) { return ( + navigation.dispatch( + CommonActions.navigate({ + name: 'VaultDetails', + params: { vaultId: vaultItem.id }, + merge: true, + }) + ) + } backgroundColor="light.primaryBackground" height={hp(135)} width={wp(300)} @@ -87,12 +97,11 @@ function ArchivedVault() {
- {/* */} ); } - const renderArchiveVaults = ({ item, index }) => ; + const renderArchiveVaults = ({ item }) => ; return ( diff --git a/src/screens/Vault/AssignSignerType.tsx b/src/screens/Vault/AssignSignerType.tsx index 4be99bdb3..bb018e18f 100644 --- a/src/screens/Vault/AssignSignerType.tsx +++ b/src/screens/Vault/AssignSignerType.tsx @@ -5,36 +5,36 @@ import KeeperHeader from 'src/components/KeeperHeader'; import ScreenWrapper from 'src/components/ScreenWrapper'; import { SignerType } from 'src/core/wallets/enums'; import { ActivityIndicator, StyleSheet, TouchableOpacity } from 'react-native'; -import { useDispatch } from 'react-redux'; -import { updateSignerDetails } from 'src/store/sagaActions/wallets'; -import { getDeviceStatus, getSDMessage, getSignerNameFromType } from 'src/hardware'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { getDeviceStatus, getSDMessage } from 'src/hardware'; +import { Vault } from 'src/core/wallets/interfaces/vault'; import { SDIcons } from '../Vault/SigningDeviceIcons'; import usePlan from 'src/hooks/usePlan'; import NFC from 'src/services/nfc'; -import useVault from 'src/hooks/useVault'; import { SubscriptionTier } from 'src/models/enums/SubscriptionTier'; import Text from 'src/components/KeeperText'; +import HardwareModalMap, { InteracationMode } from './HardwareModalMap'; +import useSigners from 'src/hooks/useSigners'; +import { KeeperApp } from 'src/models/interfaces/KeeperApp'; +import { useQuery } from '@realm/react'; +import { RealmSchema } from 'src/storage/realm/enum'; +import { getJSONFromRealmObject } from 'src/storage/realm/utils'; type IProps = { navigation: any; route: { params: { - signer: VaultSigner; - parentNavigation: any; + vault: Vault; }; }; }; -function AssignSignerType({ navigation, route }: IProps) { - const dispatch = useDispatch(); - const { signer, parentNavigation } = route.params; +function AssignSignerType({ route }: IProps) { + const { vault } = route.params; + const { signers: appSigners } = useSigners(); + const [visible, setVisible] = useState(false); + const [signerType, setSignerType] = useState(); const assignSignerType = (type: SignerType) => { - parentNavigation.setParams({ - signer: { ...signer, type, signerName: getSignerNameFromType(type) }, - }); - dispatch(updateSignerDetails(signer, 'type', type)); - dispatch(updateSignerDetails(signer, 'signerName', getSignerNameFromType(type))); - navigation.goBack(); + setSignerType(type); + setVisible(true); }; const { plan } = usePlan(); @@ -42,6 +42,7 @@ function AssignSignerType({ navigation, route }: IProps) { SignerType.TAPSIGNER, SignerType.COLDCARD, SignerType.SEEDSIGNER, + SignerType.SPECTER, SignerType.PASSPORT, SignerType.JADE, SignerType.KEYSTONE, @@ -50,16 +51,17 @@ function AssignSignerType({ navigation, route }: IProps) { SignerType.BITBOX02, SignerType.KEEPER, SignerType.SEED_WORDS, - SignerType.MOBILE_KEY, + // SignerType.MOBILE_KEY, SignerType.POLICY_SERVER, ]; const [isNfcSupported, setNfcSupport] = useState(true); const [signersLoaded, setSignersLoaded] = useState(false); - const { - activeVault: { signers, scheme }, - } = useVault(); const isOnL1 = plan === SubscriptionTier.L1.toUpperCase(); + const isOnL2 = plan === SubscriptionTier.L2.toUpperCase(); + const { primaryMnemonic }: KeeperApp = useQuery(RealmSchema.KeeperApp).map( + getJSONFromRealmObject + )[0]; const getNfcSupport = async () => { const isSupported = await NFC.isNFCSupported(); @@ -74,14 +76,9 @@ function AssignSignerType({ navigation, route }: IProps) { return ( - - Master fingerprint: {signer.masterFingerprint} - Fingerprint: {signer.signerId} - xPub: {signer.xpub} - assignSignerType(type)} + onPress={() => { + assignSignerType(type); + }} key={type} > )} + setVisible(false)} + vaultSigners={vault?.signers} + skipHealthCheckCallBack={() => { + setVisible(false); + }} + mode={InteracationMode.IDENTIFICATION} + vaultShellId={vault?.shellId} + isMultisig={vault?.isMultiSig} + primaryMnemonic={primaryMnemonic} + addSignerFlow={false} + vaultId={vault?.id} + /> ); diff --git a/src/screens/Vault/ChoosePolicyNew.tsx b/src/screens/Vault/ChoosePolicyNew.tsx index 548312694..c95d3c010 100644 --- a/src/screens/Vault/ChoosePolicyNew.tsx +++ b/src/screens/Vault/ChoosePolicyNew.tsx @@ -28,6 +28,7 @@ import CustomGreenButton from 'src/components/CustomButton/CustomGreenButton'; import CVVInputsView from 'src/components/HealthCheck/CVVInputsView'; import useToastMessage from 'src/hooks/useToastMessage'; import DeleteIcon from 'src/assets/images/deleteBlack.svg'; +import useVault from 'src/hooks/useVault'; function ChoosePolicyNew({ navigation, route }) { const { colorMode } = useColorMode(); @@ -39,7 +40,7 @@ function ChoosePolicyNew({ navigation, route }) { const [validationModal, showValidationModal] = useState(false); const [otp, setOtp] = useState(''); - const isUpdate = route.params.update; + const { isUpdate, addSignerFlow, vaultId } = route.params; const existingRestrictions: SignerRestriction = route.params.restrictions; const existingMaxTransactionRestriction = idx( existingRestrictions, @@ -54,6 +55,7 @@ function ChoosePolicyNew({ navigation, route }) { const [minTransaction, setMinTransaction] = useState( existingMaxTransactionException ? `${existingMaxTransactionException}` : '1000000' ); + const { activeVault } = useVault({ vaultId }); const dispatch = useDispatch(); @@ -82,7 +84,7 @@ function ChoosePolicyNew({ navigation, route }) { }; navigation.dispatch( - CommonActions.navigate({ name: 'SetupSigningServer', params: { policy } }) + CommonActions.navigate({ name: 'SetupSigningServer', params: { policy, addSignerFlow } }) ); } }; @@ -104,9 +106,14 @@ function ChoosePolicyNew({ navigation, route }) { exceptions, }; const verificationToken = Number(otp); - dispatch(updateSignerPolicy(route.params.signer, updates, verificationToken)); + dispatch( + updateSignerPolicy(route.params.signer, route.params.vaultKey, updates, verificationToken) + ); navigation.dispatch( - CommonActions.navigate({ name: 'VaultDetails', params: { vaultTransferSuccessful: null } }) + CommonActions.navigate({ + name: 'VaultDetails', + params: { vaultId: activeVault.id, vaultTransferSuccessful: null }, + }) ); }; @@ -232,7 +239,7 @@ function ChoosePolicyNew({ navigation, route }) { showValidationModal(false); }} title="Confirm OTP to change policy" - subTitle="To complete setting up the signing server" + subTitle="To complete setting up the signer" textColor="light.primaryText" Content={otpContent} /> diff --git a/src/screens/Vault/HardwareModalMap.tsx b/src/screens/Vault/HardwareModalMap.tsx index 15fc7011c..b52987de3 100644 --- a/src/screens/Vault/HardwareModalMap.tsx +++ b/src/screens/Vault/HardwareModalMap.tsx @@ -1,6 +1,9 @@ +// /* eslint-disable no-case-declarations */ + /* eslint-disable no-case-declarations */ import React, { useCallback, useContext, useEffect, useState } from 'react'; import * as bip39 from 'bip39'; +import moment from 'moment'; import { ActivityIndicator, Alert, Clipboard, StyleSheet, TouchableOpacity } from 'react-native'; import { Box, useColorMode, View } from 'native-base'; import { CommonActions, useNavigation } from '@react-navigation/native'; @@ -18,7 +21,10 @@ import CVVInputsView from 'src/components/HealthCheck/CVVInputsView'; import ColdCardSetupImage from 'src/assets/images/ColdCardSetup.svg'; import DeleteIcon from 'src/assets/images/deleteBlack.svg'; import JadeSVG from 'src/assets/images/illustration_jade.svg'; +import RecoverImage from 'src/assets/images/recover_white.svg'; + import KeeperModal from 'src/components/KeeperModal'; + import KeyPadView from 'src/components/AppNumPad/KeyPadView'; import KeystoneSetupImage from 'src/assets/images/keystone_illustration.svg'; import LedgerImage from 'src/assets/images/ledger_image.svg'; @@ -26,6 +32,7 @@ import { LocalizationContext } from 'src/context/Localization/LocContext'; import MobileKeyIllustration from 'src/assets/images/mobileKey_illustration.svg'; import PassportSVG from 'src/assets/images/illustration_passport.svg'; import SeedSignerSetupImage from 'src/assets/images/seedsigner_setup.svg'; +import SpecterSetupImage from 'src/assets/images/illustration_spectre.svg'; import KeeperSetupImage from 'src/assets/images/illustration_ksd.svg'; import SeedWordsIllustration from 'src/assets/images/illustration_seed_words.svg'; import SigningServerIllustration from 'src/assets/images/signingServer_illustration.svg'; @@ -33,11 +40,15 @@ import TapsignerSetupImage from 'src/assets/images/TapsignerSetup.svg'; import OtherSDSetup from 'src/assets/images/illustration_othersd.svg'; import BitboxImage from 'src/assets/images/bitboxSetup.svg'; import TrezorSetup from 'src/assets/images/trezor_setup.svg'; -import { VaultSigner, XpubDetailsType } from 'src/core/wallets/interfaces/vault'; +import { Signer, VaultSigner, XpubDetailsType } from 'src/core/wallets/interfaces/vault'; import { addSigningDevice } from 'src/store/sagaActions/vaults'; import { captureError } from 'src/services/sentry'; import config from 'src/core/config'; -import { generateSignerFromMetaData, getSignerNameFromType } from 'src/hardware'; +import { + extractKeyFromDescriptor, + generateSignerFromMetaData, + getSignerNameFromType, +} from 'src/hardware'; import { getJadeDetails } from 'src/hardware/jade'; import { getKeystoneDetails } from 'src/hardware/keystone'; import { getPassportDetails } from 'src/hardware/passport'; @@ -56,22 +67,31 @@ import Buttons from 'src/components/Buttons'; import ActivityIndicatorView from 'src/components/AppActivityIndicator/ActivityIndicatorView'; import { healthCheckSigner } from 'src/store/sagaActions/bhr'; import SigningServer from 'src/services/operations/SigningServer'; -import { checkSigningDevice } from './AddSigningDevice'; import * as SecureStore from 'src/storage/secure-store'; import { setSigningDevices } from 'src/store/reducers/bhr'; import CustomGreenButton from 'src/components/CustomButton/CustomGreenButton'; import InheritanceKeyServer from 'src/services/operations/InheritanceKey'; -import { formatDuration } from '../VaultRecovery/VaultRecovery'; import { setInheritanceRequestId } from 'src/store/reducers/storage'; -import { getnavigationState } from '../Recovery/SigninDeviceListRecovery'; import Instruction from 'src/components/Instruction'; +import useUnkownSigners from 'src/hooks/useUnkownSigners'; +import WalletUtilities from 'src/core/wallets/operations/utils'; +import { getSpecterDetails } from 'src/hardware/specter'; +import useSignerMap from 'src/hooks/useSignerMap'; +import InhertanceKeyIcon from 'src/assets/images/inheritanceTitleKey.svg'; +import SignerCard from '../AddSigner/SignerCard'; +import useSigners from 'src/hooks/useSigners'; + +import useConfigRecovery from 'src/hooks/useConfigReocvery'; const RNBiometrics = new ReactNativeBiometrics(); export const enum InteracationMode { - SIGNING = 'SIGNING', + VAULT_ADDITION = 'VAULT_ADDITION', HEALTH_CHECK = 'HEALTH_CHECK', RECOVERY = 'RECOVERY', + CONFIG_RECOVERY = 'CONFIG_RECOVERY', + IDENTIFICATION = 'IDENTIFICATION', + APP_ADDITION = 'APP_ADDITION', } const getSignerContent = ( @@ -83,17 +103,17 @@ const getSignerContent = ( const { tapsigner, coldcard, ledger, bitbox, trezor } = translations; switch (type) { case SignerType.COLDCARD: - const ccInstructions = `Export the xPub by going to Advanced/Tools > Export wallet > Generic JSON. From here choose the account number and transfer over NFC. Make sure you remember the account you had chosen (This is important for recovering your Vault).\n`; return { Illustration: , - Instructions: isTestnet() - ? [ - ccInstructions, - `Make sure you enable Testnet mode on the coldcard if you are running the app in the Testnet mode from Advance option > Danger Zone > Testnet and enable it.`, - ] - : [ccInstructions], + Instructions: [ + 'Export the xPub by going to Advanced/Tools > Export wallet > Generic JSON.', + 'From here choose the account number and transfer over NFC', + ], title: coldcard.SetupTitle, subTitle: `${coldcard.SetupDescription}`, + sepInstruction: + 'Make sure you remember the account you had chosen (This is important for vault recovery)', + options: [], }; case SignerType.JADE: const jadeInstructions = `Make sure the Jade is setup with a companion app and Unlocked. Then export the xPub by going to Settings > Xpub Export. Also to be sure that the wallet type and script type is set to ${ @@ -104,45 +124,49 @@ const getSignerContent = ( Instructions: isTestnet() ? [ jadeInstructions, - `Make sure you enable Testnet mode on the Jade while creating the wallet with the companion app if you are running Keeper in the Testnet mode.`, + 'Make sure you enable Testnet mode on the Jade while creating the wallet with the companion app if you are running Keeper in the Testnet mode.', ] : [jadeInstructions], title: 'Setting up Blockstream Jade', subTitle: 'Keep your Jade ready and unlocked before proceeding', + options: [], }; case SignerType.KEEPER: return { Illustration: , Instructions: [ - `Choose a wallet or create a new one from your Linked Wallets`, - `Within settings choose Show co-signer Details to scan the QR`, + 'Choose a wallet or create a new one from your Linked Wallets', + 'Within settings choose Show co-signer Details to scan the QR', ], title: 'Keep your Device Ready', - subTitle: 'Keep your Keeper Signing Device ready before proceeding', + subTitle: 'Keep your Collaborative Key ready before proceeding', + options: [], }; case SignerType.MOBILE_KEY: return { Illustration: , Instructions: [ - `Make sure that this wallet's Recovery Phrase is backed-up properly to secure this key.`, + 'Make sure that this wallet’s Recovery Key is backed-up properly to secure this key.', ], title: isHealthcheck ? 'Verify Mobile Key' : 'Set up a Mobile Key', subTitle: 'Your passcode or biometrics act as your key for signing transactions', + options: [], }; case SignerType.KEYSTONE: const keystoneInstructions = isMultisig - ? `Make sure the BTC-only firmware is installed and export the xPub by going to the Side Menu > Multisig Wallet > Extended menu (three dots) from the top right corner > Show/Export XPUB > Nested SegWit.\n` - : `Make sure the BTC-only firmware is installed and export the xPub by going to the extended menu (three dots) in the Generic Wallet section > Export Wallet`; + ? 'Make sure the BTC-only firmware is installed and export the xPub by going to the Side Menu > Multisig Wallet > Extended menu (three dots) from the top right corner > Show/Export XPUB > Nested SegWit.\n' + : 'Make sure the BTC-only firmware is installed and export the xPub by going to the extended menu (three dots) in the Generic Wallet section > Export Wallet'; return { Illustration: , Instructions: isTestnet() ? [ keystoneInstructions, - `Make sure you enable Testnet mode on the Keystone if you are running the app in the Testnet mode from Side Menu > Settings > Blockchain > Testnet and confirm`, + 'Make sure you enable Testnet mode on the Keystone if you are running the app in the Testnet mode from Side Menu > Settings > Blockchain > Testnet and confirm', ] : [keystoneInstructions], title: isHealthcheck ? 'Verify Keystone' : 'Setting up Keystone', subTitle: 'Keep your Keystone ready before proceeding', + options: [], }; case SignerType.PASSPORT: const passportInstructions = `Export the xPub from the Account section > Manage Account > Connect Wallet > Keeper > ${ @@ -153,23 +177,25 @@ const getSignerContent = ( Instructions: isTestnet() ? [ passportInstructions, - `Make sure you enable Testnet mode on the Passport if you are running the app in the Testnet mode from Settings > Bitcoin > Network > Testnet and enable it.`, + 'Make sure you enable Testnet mode on the Passport if you are running the app in the Testnet mode from Settings > Bitcoin > Network > Testnet and enable it.', ] : [passportInstructions], title: isHealthcheck ? 'Verify Passport (Batch 2)' : 'Setting up Passport (Batch 2)', subTitle: 'Keep your Foundation Passport (Batch 2) ready before proceeding', + options: [], }; case SignerType.POLICY_SERVER: return { Illustration: , Instructions: isHealthcheck - ? ['A request to the signing server will be made to checks it health'] + ? ['A request to the signer will be made to checks it health'] : [ `A 2FA authenticator will have to be set up to use this option.`, - `On providing the correct code from the auth app, the Signing Server will sign the transaction.`, + `On providing the correct code from the auth app, the signer will sign the transaction.`, ], - title: isHealthcheck ? 'Verify Signing Server' : 'Setting up a Signing Server', - subTitle: 'A Signing Server will hold one of the keys of the Vault', + title: isHealthcheck ? 'Verify signer' : 'Setting up a signer', + subTitle: 'A signer will hold one of the keys of the vault', + options: [], }; case SignerType.SEEDSIGNER: const seedSignerInstructions = `Make sure the seed is loaded and export the xPub by going to Seeds > Select your master fingerprint > Export Xpub > ${ @@ -180,81 +206,118 @@ const getSignerContent = ( Instructions: isTestnet() ? [ seedSignerInstructions, - `Make sure you enable Testnet mode on the SeedSigner if you are running the app in the Testnet mode from Settings > Advanced > Bitcoin network > Testnet and enable it.`, + 'Make sure you enable Testnet mode on the SeedSigner if you are running the app in the Testnet mode from Settings > Advanced > Bitcoin network > Testnet and enable it.', ] : [seedSignerInstructions], title: isHealthcheck ? 'Verify SeedSigner' : 'Setting up SeedSigner', subTitle: 'Keep your SeedSigner ready and powered before proceeding', + options: [], + }; + case SignerType.SPECTER: + const specterInstructions = `Make sure the seed is loaded and export the xPub by going to Master Keys > ${ + isMultisig ? 'Multisig' : 'Singlesig' + } > Native Segwit.\n`; + return { + Illustration: , + Instructions: isTestnet() + ? [ + specterInstructions, + `Make sure you enable Testnet mode on the Specter if you are running the app on Testnet by selecting Switch network (Testnet) on the home screen`, + ] + : [specterInstructions], + title: isHealthcheck ? 'Verify Specter' : 'Setting up Specter DIY', + subTitle: 'Keep your device ready and powered before proceeding', + options: [], }; case SignerType.BITBOX02: return { Illustration: , Instructions: [ `Please visit ${config.KEEPER_HWI} on your Chrome browser to use the Keeper Hardware Interface to connect with BitBox02. `, - `Make sure the device is setup with the Bitbox02 app before using it with the Keeper Hardware Interface.`, + 'Make sure the device is setup with the Bitbox02 app before using it with the Keeper Hardware Interface.', ], title: isHealthcheck ? 'Verify BitBox' : bitbox.SetupTitle, subTitle: bitbox.SetupDescription, + options: [], }; case SignerType.TREZOR: return { Illustration: , Instructions: [ `Please visit ${config.KEEPER_HWI} on your Chrome browser to use the Keeper Hardware Interface to connect with Trezor. `, - `Make sure the device is setup with the Trezor Connect app before using it with the Keeper Hardware Interface.`, + 'Make sure the device is setup with the Trezor Connect app before using it with the Keeper Hardware Interface.', ], title: isHealthcheck ? 'Verify Trezor' : trezor.SetupTitle, subTitle: trezor.SetupDescription, + options: [], }; case SignerType.LEDGER: return { Illustration: , Instructions: [ `Please visit ${config.KEEPER_HWI} on your Chrome browser to use the Keeper Hardware Interface to connect with Ledger. `, - `Please Make sure you have the BTC app downloaded on Ledger before this step.`, + 'Please Make sure you have the BTC app downloaded on Ledger before this step.', ], title: ledger.SetupTitle, subTitle: ledger.SetupDescription, + options: [], }; case SignerType.SEED_WORDS: return { Illustration: , Instructions: [ - `Once the transaction is signed the key is not stored on the app.`, - `Make sure that you're noting down the words in private as exposing them will compromise the Seed Key`, + 'This mnemonic (12 words) needs to be noted down and kept offline (the private keys are not stored on the app', + 'Make sure that you’re noting down the words in private as exposing them will compromise the Seed Key', ], title: isHealthcheck ? 'Verify Seed Key' : 'Setting up Seed Key', - subTitle: 'Seed Key is a 12 word Recovery Phrase. Please note them down and store safely', + subTitle: 'Seed Key is a 12 word Recovery Key.\nPlease note them down and store safely', + options: [], }; case SignerType.TAPSIGNER: return { Illustration: , Instructions: [ - 'You will need the Pin/CVC at the back of TAPSIGNER', - 'You should generally not use the same signing device on multiple wallets/apps', + 'You will need the Pin/CVC given at\n the back of the TAPSIGNER', + 'Make sure that the TAPSIGNER has not\n been used as a signer in other apps', ], title: isHealthcheck ? 'Verify TAPSIGNER' : tapsigner.SetupTitle, subTitle: tapsigner.SetupDescription, + options: [], }; case SignerType.OTHER_SD: return { Illustration: , Instructions: [ - 'Manually provide the signing device details', - `The hardened part of the derivation path of the xpub has to be denoted with a " h " or " ' ". Please do not use any other charecter`, + 'Provide the Signer details either by entering them or scanning', + 'The hardened part of the derivation path of the xpub has to be denoted with a “h” or “”. Please do not use any other character', ], - title: 'Keep your signing device ready', - subTitle: 'Keep your signing device ready before proceeding', + title: 'Setting up Signer', + subTitle: 'Keep your Signer ready before proceeding', + options: [], }; + case SignerType.INHERITANCEKEY: return { - Illustration: , + Illustration: , + title: 'Setting up an Inheritance Key', + subTitle: 'This step will add an additional, mandatory key to your m-of-n vault', Instructions: [ - 'Manually provide the signing device details', - `The hardened part of the derivation path of the xpub has to be denoted with a " h " or " ' ". Please do not use any other charecter`, + 'This Key would only get activated after the other two Keys have signed', + `On activation the Key would send emails to your email id for 30 days for you to decline using it`, + ], + options: [ + { + title: 'Configure a New Key', + icon: , + callback: () => {}, + name: 'newKey', + }, + { + title: 'Recover Existing Key', + icon: , + name: 'recoverKey', + }, ], - title: 'Keep your signing device ready', - subTitle: 'Keep your signing device ready before proceeding', }; default: return { @@ -267,129 +330,205 @@ const getSignerContent = ( } }; +const getnavigationState = (type) => ({ + index: 5, + routes: [ + { name: 'NewKeeperApp' }, + { name: 'EnterSeedScreen', params: { isSoftKeyRecovery: false, type } }, + { name: 'OtherRecoveryMethods' }, + { name: 'VaultRecoveryAddSigner' }, + { name: 'SigningDeviceListRecovery' }, + { name: 'EnterSeedScreen', params: { isSoftKeyRecovery: true, type } }, + ], +}); + +function formatDuration(ms) { + const duration = moment.duration(ms); + return Math.floor(duration.asHours()) + moment.utc(duration.asMilliseconds()).format(':mm:ss'); +} function SignerContent({ Illustration, Instructions, mode, + options, + setSelectInheritanceType, + selectInheritanceType, + sepInstruction = '', }: { Illustration: Element; Instructions: Array; mode: InteracationMode; + options?: any; + setSelectInheritanceType: (index) => any; + selectInheritanceType: any; + sepInstruction?: String; }) { + const { colorMode } = useColorMode(); return ( {Illustration} {mode === InteracationMode.HEALTH_CHECK && ( - + )} - {Instructions.map((instruction) => ( + {Instructions?.map((instruction) => ( ))} + {sepInstruction && ( + + {sepInstruction} + + )} + + {options && + options.map((option, index) => ( + { + setSelectInheritanceType(index); + }} + /> + ))} + ); } const setupPassport = (qrData, isMultisig) => { - const { xpub, derivationPath, xfp, forMultiSig, forSingleSig } = getPassportDetails(qrData); + const { xpub, derivationPath, masterFingerprint, forMultiSig, forSingleSig } = + getPassportDetails(qrData); if ((isMultisig && forMultiSig) || (!isMultisig && forSingleSig)) { - const passport: VaultSigner = generateSignerFromMetaData({ + const { signer: passport, key } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, signerType: SignerType.PASSPORT, storageType: SignerStorage.COLD, isMultisig, }); - return passport; + return { signer: passport, key }; } throw new HWError(HWErrorType.INVALID_SIG); }; const verifyPassport = (qrData, signer) => { - const { xpub } = getPassportDetails(qrData); - return xpub === signer.xpub; + const { masterFingerprint } = getPassportDetails(qrData); + return masterFingerprint === signer.masterFingerprint; }; const setupSeedSigner = (qrData, isMultisig) => { - const { xpub, derivationPath, xfp, forMultiSig, forSingleSig } = getSeedSignerDetails(qrData); + const { xpub, derivationPath, masterFingerprint, forMultiSig, forSingleSig } = + getSeedSignerDetails(qrData); if ((isMultisig && forMultiSig) || (!isMultisig && forSingleSig)) { - const seedSigner: VaultSigner = generateSignerFromMetaData({ + const { signer: seedSigner, key } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, signerType: SignerType.SEEDSIGNER, storageType: SignerStorage.COLD, isMultisig, }); - return seedSigner; + return { signer: seedSigner, key }; + } + throw new HWError(HWErrorType.INVALID_SIG); +}; + +const verifySeedSigner = (qrData: any, signer: VaultSigner) => { + const { masterFingerprint } = getSeedSignerDetails(qrData); + return masterFingerprint === signer.masterFingerprint; +}; + +const setupSpecter = (qrData, isMultisig) => { + const { xpub, derivationPath, masterFingerprint, forMultiSig, forSingleSig } = + getSpecterDetails(qrData); + if ((isMultisig && forMultiSig) || (!isMultisig && forSingleSig)) { + const { signer, key } = generateSignerFromMetaData({ + xpub, + derivationPath, + masterFingerprint, + signerType: SignerType.SPECTER, + storageType: SignerStorage.COLD, + isMultisig, + }); + return { signer, key }; } throw new HWError(HWErrorType.INVALID_SIG); }; -const verifySeedSigner = (qrData, signer) => { - const { xpub } = getSeedSignerDetails(qrData); - return xpub === signer.xpub; +const verifySpecter = (qrData, signer) => { + const { masterFingerprint } = getSpecterDetails(qrData); + return masterFingerprint === signer.masterFingerprint; }; const setupKeystone = (qrData, isMultisig) => { - const { xpub, derivationPath, xfp, forMultiSig, forSingleSig } = getKeystoneDetails(qrData); + const { xpub, derivationPath, masterFingerprint, forMultiSig, forSingleSig } = + getKeystoneDetails(qrData); if ((isMultisig && forMultiSig) || (!isMultisig && forSingleSig)) { - const keystone: VaultSigner = generateSignerFromMetaData({ + const { signer: keystone, key } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, signerType: SignerType.KEYSTONE, storageType: SignerStorage.COLD, isMultisig, }); - return keystone; + return { signer: keystone, key }; } throw new HWError(HWErrorType.INVALID_SIG); }; const verifyKeystone = (qrData, signer) => { - const { xpub } = getKeystoneDetails(qrData); - return xpub === signer.xpub; + const { masterFingerprint } = getKeystoneDetails(qrData); + return masterFingerprint === signer.masterFingerprint; }; const setupJade = (qrData, isMultisig) => { - const { xpub, derivationPath, xfp, forMultiSig, forSingleSig } = getJadeDetails(qrData); + const { xpub, derivationPath, masterFingerprint, forMultiSig, forSingleSig } = + getJadeDetails(qrData); if ((isMultisig && forMultiSig) || (!isMultisig && forSingleSig)) { - const jade: VaultSigner = generateSignerFromMetaData({ + const { signer: jade, key } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, signerType: SignerType.JADE, storageType: SignerStorage.COLD, isMultisig, }); - return jade; + return { signer: jade, key }; } throw new HWError(HWErrorType.INVALID_SIG); }; const verifyJade = (qrData, signer) => { - const { xpub } = getJadeDetails(qrData); - return xpub === signer.xpub; + const { masterFingerprint } = getJadeDetails(qrData); + return masterFingerprint === signer.masterFingerprint; }; const setupKeeperSigner = (qrData, isMultisig) => { try { const { mfp, xpubDetails } = JSON.parse(qrData); - const ksd = generateSignerFromMetaData({ + const { signer: ksd, key } = generateSignerFromMetaData({ xpub: isMultisig ? xpubDetails[XpubTypes.P2WSH].xpub : xpubDetails[XpubTypes.P2WPKH].xpub, derivationPath: isMultisig ? xpubDetails[XpubTypes.P2WSH].derivationPath : xpubDetails[XpubTypes.P2WPKH].derivationPath, - xfp: mfp, + masterFingerprint: mfp, signerType: SignerType.KEEPER, storageType: SignerStorage.WARM, isMultisig: true, xpubDetails, }); - return ksd; + return { signer: ksd, key }; } catch (err) { const message = crossInteractionHandler(err); throw new Error(message); @@ -398,8 +537,8 @@ const setupKeeperSigner = (qrData, isMultisig) => { const verifyKeeperSigner = (qrData, signer) => { try { - const { xpub } = JSON.parse(qrData); - return xpub === signer.xpub; + const { masterFingerprint } = extractKeyFromDescriptor(qrData); + return masterFingerprint === signer.masterFingerprint; } catch (err) { const message = crossInteractionHandler(err); throw new Error(message); @@ -435,17 +574,17 @@ const setupMobileKey = async ({ primaryMnemonic, isMultisig }) => { xpriv: multiSigXpriv, }; - const mobileKey = generateSignerFromMetaData({ + const { signer: mobileKey, key } = generateSignerFromMetaData({ xpub: isMultisig ? multiSigXpub : singleSigXpub, derivationPath: isMultisig ? multiSigPath : singleSigPath, - xfp: masterFingerprint, + masterFingerprint, signerType: SignerType.MOBILE_KEY, storageType: SignerStorage.WARM, isMultisig: true, xpriv: isMultisig ? multiSigXpriv : singleSigXpriv, xpubDetails, }); - return mobileKey; + return { signer: mobileKey, key }; }; export const setupSeedWordsBasedKey = (mnemonic: string, isMultisig: boolean) => { @@ -467,17 +606,17 @@ export const setupSeedWordsBasedKey = (mnemonic: string, isMultisig: boolean) => xpubDetails[XpubTypes.P2WPKH] = { xpub: singleSigXpub, derivationPath: singleSigPath }; xpubDetails[XpubTypes.P2WSH] = { xpub: multiSigXpub, derivationPath: multiSigPath }; - const softSigner = generateSignerFromMetaData({ + const { signer: softSigner, key } = generateSignerFromMetaData({ xpub: isMultisig ? multiSigXpub : singleSigXpub, derivationPath: isMultisig ? multiSigPath : singleSigPath, - xfp: masterFingerprint, + masterFingerprint, signerType: SignerType.SEED_WORDS, storageType: SignerStorage.WARM, isMultisig, xpubDetails, }); - return softSigner; + return { signer: softSigner, key }; }; function PasswordEnter({ @@ -488,6 +627,8 @@ function PasswordEnter({ isHealthcheck, signer, close, + isMultisig, + addSignerFlow, }: { primaryMnemonic; navigation; @@ -496,6 +637,8 @@ function PasswordEnter({ isHealthcheck?; signer?; close?; + isMultisig; + addSignerFlow; }) { const [password, setPassword] = useState(''); const { showToast } = useToastMessage(); @@ -513,12 +656,13 @@ function PasswordEnter({ try { const currentPinHash = hash512(password); if (currentPinHash === pinHash) { - const mobileKey = await setupMobileKey({ primaryMnemonic }); - dispatch(addSigningDevice(mobileKey)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); - showToast(`${mobileKey.signerName} added successfully`, ); + const { signer } = await setupMobileKey({ primaryMnemonic, isMultisig }); + dispatch(addSigningDevice([signer])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); + showToast(`${signer.signerName} added successfully`, ); setInProgress(false); close(); } else { @@ -541,7 +685,7 @@ function PasswordEnter({ const currentPinHash = hash512(password); if (currentPinHash === pinHash) { dispatch(healthCheckSigner([signer])); - showToast(`Mobile Key verified successfully`, ); + showToast('Mobile Key verified successfully', ); setInProgress(false); close(); } else { @@ -618,19 +762,25 @@ function HardwareModalMap({ isMultisig, signer, skipHealthCheckCallBack, - mode, + mode = InteracationMode.VAULT_ADDITION, primaryMnemonic, vaultShellId, + addSignerFlow = false, + vaultSigners, + vaultId, }: { type: SignerType; visible: boolean; close: any; - signer?: VaultSigner; + signer?: Signer; skipHealthCheckCallBack?: any; - mode: InteracationMode; + mode?: InteracationMode; isMultisig: boolean; primaryMnemonic?: string; vaultShellId?: string; + addSignerFlow: boolean; + vaultSigners?: VaultSigner[]; + vaultId: string; }) { const { colorMode } = useColorMode(); const dispatch = useDispatch(); @@ -641,8 +791,12 @@ function HardwareModalMap({ const [passwordModal, setPasswordModal] = useState(false); const [inProgress, setInProgress] = useState(false); + const { mapUnknownSigner } = useUnkownSigners(); const loginMethod = useAppSelector((state) => state.settings.loginMethod); - const { signingDevices } = useAppSelector((state) => state.bhr); + const { signers } = useSigners(); + const signingDevices = signers; + const { signerMap } = useSignerMap() as { signerMap: { [key: string]: Signer } }; + const appId = useAppSelector((state) => state.storage.appId); const { pinHash } = useAppSelector((state) => state.storage); const isHealthcheck = mode === InteracationMode.HEALTH_CHECK; @@ -657,7 +811,10 @@ function HardwareModalMap({ ); } navigation.dispatch( - CommonActions.navigate({ name: 'AddTapsigner', params: { mode, signer, isMultisig } }) + CommonActions.navigate({ + name: 'AddTapsigner', + params: { mode, signer, isMultisig, addSignerFlow }, + }) ); }; @@ -671,7 +828,10 @@ function HardwareModalMap({ ); } navigation.dispatch( - CommonActions.navigate({ name: 'AddColdCard', params: { mode, signer, isMultisig } }) + CommonActions.navigate({ + name: 'AddColdCard', + params: { mode, signer, isMultisig, addSignerFlow }, + }) ); }; @@ -680,12 +840,12 @@ function HardwareModalMap({ CommonActions.navigate({ name: 'ScanQR', params: { - title: `${isHealthcheck ? `Verify` : `Setting up`} ${getSignerNameFromType(type)}`, + title: `${isHealthcheck ? 'Verify' : 'Setting up'} ${getSignerNameFromType(type)}`, subtitle: 'Please scan until all the QR data has been retrieved', onQrScan: isHealthcheck ? onQRScanHealthCheck : onQRScan, setup: true, type, - isHealthcheck: true, + mode, signer, }, }) @@ -696,23 +856,33 @@ function HardwareModalMap({ if (mode === InteracationMode.HEALTH_CHECK) { try { setInProgress(true); - const { isSignerAvailable } = await SigningServer.checkSignerHealth(signer.signerId); + const signerXfp = WalletUtilities.getFingerprintFromExtendedKey( + signer.signerXpubs[XpubTypes.P2WSH][0].xpub, + WalletUtilities.getNetworkByType(config.NETWORK_TYPE) + ); + const { isSignerAvailable } = await SigningServer.checkSignerHealth(signerXfp); if (isSignerAvailable) { dispatch(healthCheckSigner([signer])); close(); - showToast(`Health check done successfully`, ); + showToast('Health check done successfully', ); } else { close(); showToast('Error in Health check', , 3000); } setInProgress(false); } catch (err) { + console.log(err); setInProgress(false); close(); showToast('Error in Health check', , 3000); } } else { - navigation.dispatch(CommonActions.navigate({ name: 'ChoosePolicyNew', params: { signer } })); + navigation.dispatch( + CommonActions.navigate({ + name: 'ChoosePolicyNew', + params: { signer, addSignerFlow, vaultId }, + }) + ); } }; @@ -721,12 +891,13 @@ function HardwareModalMap({ CommonActions.navigate({ name: 'ConnectChannel', params: { - title: `${isHealthcheck ? `Verify` : `Setting up`} ${getSignerNameFromType(type)}`, + title: `${isHealthcheck ? 'Verify' : 'Setting up'} ${getSignerNameFromType(type)}`, subtitle: `Please visit ${config.KEEPER_HWI} on your Chrome browser to use the Keeper Hardware Interface to setup`, type, signer, mode, isMultisig, + addSignerFlow, }, }) ); @@ -739,6 +910,7 @@ function HardwareModalMap({ params: { mode, isMultisig, + addSignerFlow, }, }) ); @@ -749,7 +921,7 @@ function HardwareModalMap({ const navigationState = getnavigationState(SignerType.SEED_WORDS); navigation.dispatch(CommonActions.reset(navigationState)); close(); - } else if (mode === InteracationMode.SIGNING) { + } else if (mode === InteracationMode.VAULT_ADDITION) { close(); const mnemonic = bip39.generateMnemonic(); navigation.dispatch( @@ -760,25 +932,29 @@ function HardwareModalMap({ next: true, isHealthcheck, onSuccess: (mnemonic) => { - const softSigner = setupSeedWordsBasedKey(mnemonic, isMultisig); - dispatch(addSigningDevice(softSigner)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); - showToast(`${softSigner.signerName} added successfully`, ); + const { signer, key } = setupSeedWordsBasedKey(mnemonic, isMultisig); + dispatch(addSigningDevice([signer])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); + showToast(`${signer.signerName} added successfully`, ); }, + addSignerFlow, }, }) ); - } else if (mode === InteracationMode.HEALTH_CHECK) { + } else if (mode === InteracationMode.HEALTH_CHECK || mode === InteracationMode.IDENTIFICATION) { navigation.dispatch( CommonActions.navigate({ name: 'EnterSeedScreen', params: { + mode, isHealthCheck: true, signer, isMultisig, setupSeedWordsBasedSigner: setupSeedWordsBasedKey, + addSignerFlow, }, }) ); @@ -786,7 +962,7 @@ function HardwareModalMap({ }; const onQRScan = async (qrData, resetQR) => { - let hw: VaultSigner; + let hw: { signer: Signer; key: VaultSigner }; try { switch (type) { case SignerType.PASSPORT: @@ -795,6 +971,9 @@ function HardwareModalMap({ case SignerType.SEEDSIGNER: hw = setupSeedSigner(qrData, isMultisig); break; + case SignerType.SPECTER: + hw = setupSpecter(qrData, isMultisig); + break; case SignerType.KEEPER: hw = setupKeeperSigner(qrData, isMultisig); break; @@ -808,19 +987,31 @@ function HardwareModalMap({ break; } + const handleSuccess = () => { + dispatch(healthCheckSigner([signer])); + navigation.dispatch(CommonActions.goBack()); + showToast(`${signer.signerName} verified successfully`, ); + }; + + const handleFailure = () => { + navigation.dispatch(CommonActions.goBack()); + showToast(`${signer.signerName} verification failed`, ); + }; + if (mode === InteracationMode.RECOVERY) { - dispatch(setSigningDevices(hw)); + dispatch(setSigningDevices(hw.signer)); navigation.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }); + } else if (mode === InteracationMode.IDENTIFICATION) { + const mapped = mapUnknownSigner({ masterFingerprint: hw.signer.masterFingerprint, type }); + mapped ? handleSuccess() : handleFailure(); } else { - dispatch(addSigningDevice(hw)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); + dispatch(addSigningDevice([hw.signer])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); } - showToast(`${hw.signerName} added successfully`, ); - const exsists = await checkSigningDevice(hw.signerId); - if (exsists) - showToast('Warning: Vault with this signer already exisits', , 3000); + showToast(`${hw.signer.signerName} added successfully`, ); } catch (error) { if (error instanceof HWError) { showToast(error.message, , 3000); @@ -848,6 +1039,9 @@ function HardwareModalMap({ case SignerType.SEEDSIGNER: healthcheckStatus = verifySeedSigner(qrData, signer); break; + case SignerType.SPECTER: + healthcheckStatus = verifySpecter(qrData, signer); + break; case SignerType.KEEPER: healthcheckStatus = verifyKeeperSigner(qrData, signer); break; @@ -863,10 +1057,10 @@ function HardwareModalMap({ if (healthcheckStatus) { dispatch(healthCheckSigner([signer])); navigation.dispatch(CommonActions.goBack()); - showToast(`Health check done successfully`, ); + showToast('Health check done successfully', ); } else { navigation.dispatch(CommonActions.goBack()); - showToast('Error in Health check', , 3000); + showToast('Health check Failed', , 3000); } } catch (error) { console.log('err'); @@ -891,18 +1085,22 @@ function HardwareModalMap({ try { setInProgress(true); - if (signingDevices.length <= 1) throw new Error('Add two other devices first to recover'); - const cosignersMapIds = generateCosignerMapIds(signingDevices, SignerType.POLICY_SERVER); + if (vaultSigners.length <= 1) throw new Error('Add two other devices first to recover'); + const cosignersMapIds = generateCosignerMapIds( + signerMap, + vaultSigners, + SignerType.POLICY_SERVER + ); const response = await SigningServer.fetchSignerSetupViaCosigners(cosignersMapIds[0], otp); if (response.xpub) { - const signingServerKey = generateSignerFromMetaData({ + const { signer: signingServerKey } = generateSignerFromMetaData({ xpub: response.xpub, derivationPath: response.derivationPath, - xfp: response.masterFingerprint, + masterFingerprint: response.masterFingerprint, signerType: SignerType.POLICY_SERVER, storageType: SignerStorage.WARM, isMultisig: true, - signerId: response.id, + xfp: response.id, signerPolicy: response.policy, }); setInProgress(false); @@ -915,6 +1113,34 @@ function HardwareModalMap({ Alert.alert(`${err}`); } }; + + const findSigningServer = async (otp) => { + try { + setInProgress(true); + if (vaultSigners.length <= 1) + throw new Error('Add two other devices first to do a health check'); + const network = WalletUtilities.getNetworkByType(config.NETWORK_TYPE); + const ids = vaultSigners.map((signer) => + WalletUtilities.getFingerprintFromExtendedKey(signer.xpub, network) + ); + const response = await SigningServer.findSignerSetup(ids, otp); + if (response.valid) { + const mapped = mapUnknownSigner({ + masterFingerprint: response.masterFingerprint, + type: SignerType.POLICY_SERVER, + signerPolicy: response.policy, + }); + if (mapped) { + showToast(`Signing Server verified successfully`, ); + } else { + showToast(`Something Went Wrong!`, ); + } + } + } catch (err) { + setInProgress(false); + Alert.alert(`${err}`); + } + }; const [otp, setOtp] = useState(''); const onPressNumber = (text) => { let tmpPasscode = otp; @@ -955,6 +1181,7 @@ function HardwareModalMap({ { + if (mode === InteracationMode.IDENTIFICATION) findSigningServer(otp); verifySigningServer(otp); }} value={common.confirm} @@ -972,13 +1199,13 @@ function HardwareModalMap({ ); }; - const navigateToMobileKey = async () => { + const navigateToMobileKey = async (isMultiSig) => { if (mode === InteracationMode.RECOVERY) { const navigationState = getnavigationState(SignerType.MOBILE_KEY); navigation.dispatch(CommonActions.reset(navigationState)); close(); - } else if (mode === InteracationMode.SIGNING) { - await biometricAuth(); + } else if (mode === InteracationMode.VAULT_ADDITION) { + await biometricAuth(isMultiSig); } else if (mode === InteracationMode.HEALTH_CHECK) { navigation.dispatch( CommonActions.navigate({ @@ -991,10 +1218,23 @@ function HardwareModalMap({ }, }) ); + } else if (mode === InteracationMode.IDENTIFICATION) { + navigation.dispatch( + CommonActions.navigate({ + name: 'ExportSeed', + params: { + seed: primaryMnemonic, + signer, + isHealthCheck: true, + mode, + next: true, + }, + }) + ); } }; - const biometricAuth = async () => { + const biometricAuth = async (isMultisig) => { if (loginMethod === LoginMethod.BIOMETRIC) { try { setInProgress(true); @@ -1008,12 +1248,13 @@ function HardwareModalMap({ setInProgress(false); const res = await SecureStore.verifyBiometricAuth(signature, appId); if (res.success) { - const mobileKey = await setupMobileKey({ primaryMnemonic }); - dispatch(addSigningDevice(mobileKey)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); - showToast(`${mobileKey.signerName} added successfully`, ); + const { signer, key } = await setupMobileKey({ primaryMnemonic, isMultisig }); + dispatch(addSigningDevice([signer])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); + showToast(`${signer.signerName} added successfully`, ); } else { showToast('Incorrect password. Try again!', ); } @@ -1029,42 +1270,190 @@ function HardwareModalMap({ } }; - const requestInheritanceKeyRecovery = async (signers: VaultSigner[]) => { - try { - if (signingDevices.length <= 1) throw new Error('Add two others devices first to recover'); - const cosignersMapIds = generateCosignerMapIds(signingDevices, SignerType.INHERITANCEKEY); + const handleInheritanceKey = () => { + if (selectInheritanceType === 1) { + requestInheritanceKeyRecovery(); + } else { + setupInheritanceKey(); + } + }; + + // const requestInheritanceKeyRecovery = async () => { + // if (mode === InteracationMode.IDENTIFICATION) { + // try { + // setInProgress(true); + // if (vaultSigners.length <= 1) + // throw new Error('Add two other devices first to do a health check'); + // const network = WalletUtilities.getNetworkByType(config.NETWORK_TYPE); + // const thresholdDescriptors = vaultSigners.map((signer) => signer.xfp); + // const ids = vaultSigners.map((signer) => signer.xfp); + // const response = await InheritanceKeyServer.findIKSSetup(ids, thresholdDescriptors); + // if (response.setupInfo.id) { + // const mapped = mapUnknownSigner({ + // masterFingerprint: response.setupInfo.masterFingerprint, + // type: SignerType.POLICY_SERVER, + // inheritanceKeyInfo: { + // configuration: response.setupInfo.configuration, + // policy: response.setupInfo?.policy, + // }, + // }); + // if (mapped) { + // showToast(`IKS verified successfully`, ); + // } else { + // showToast(`Something Went Wrong!`, ); + // } + // setInProgress(false); + // } + // } catch (err) { + // setInProgress(false); + // Alert.alert(`${err}`); + // } + // } else { + // try { + // if (vaultSigners.length <= 1) throw new Error('Add two others devices first to recover'); + // const cosignersMapIds = generateCosignerMapIds( + // signerMap, + // vaultSigners, + // SignerType.INHERITANCEKEY + // ); + + // const requestId = `request-${generateKey(10)}`; + // const thresholdDescriptors = vaultSigners.map((signer) => signer.xfp); + + // const { requestStatus } = await InheritanceKeyServer.requestInheritanceKey( + // requestId, + // cosignersMapIds[0], + // thresholdDescriptors + // ); + + // showToast( + // `Request would approve in ${formatDuration(requestStatus.approvesIn)} if not rejected`, + // + // ); + // dispatch(setInheritanceRequestId(requestId)); + // navigation.dispatch(CommonActions.navigate('VaultRecoveryAddSigner')); + // } catch (err) { + // showToast(`${err}`, ); + // } + // } - const requestId = `request-${generateKey(10)}`; - const thresholdDescriptors = signers.map((signer) => signer.signerId); + // close(); + // }; + const { initateRecovery, recoveryLoading: configRecoveryLoading } = useConfigRecovery(); + const { inheritanceRequestId } = useAppSelector((state) => state.storage); - const { requestStatus } = await InheritanceKeyServer.requestInheritanceKey( + const requestInheritanceKeyRecovery = async () => { + try { + if (vaultSigners.length <= 1) throw new Error('Add two other devices first to recover'); + const cosignersMapIds = generateCosignerMapIds( + signerMap, + vaultSigners, + SignerType.INHERITANCEKEY + ); + const thresholdDescriptors = vaultSigners.map((signer) => signer.xfp); + // let requestId = `request-${generateKey(10)}`; + let requestId = inheritanceRequestId; + let isNewRequest = false; + if (!requestId) { + requestId = `request-${generateKey(10)}`; + isNewRequest = true; + } + const { requestStatus, setupInfo } = await InheritanceKeyServer.requestInheritanceKey( requestId, cosignersMapIds[0], thresholdDescriptors ); + if (requestStatus && isNewRequest) dispatch(setInheritanceRequestId(requestId)); + if (requestStatus.isDeclined) { + showToast('Inheritance request has been declined', ); + // dispatch(setInheritanceRequestId('')); // clear existing request + return; + } - showToast( - `Request would approve in ${formatDuration(requestStatus.approvesIn)} if not rejected`, - - ); - dispatch(setInheritanceRequestId(requestId)); - navigation.dispatch(CommonActions.navigate('VaultRecoveryAddSigner')); + if (!requestStatus.isApproved) { + showToast( + `Request would approve in ${formatDuration(requestStatus.approvesIn)} if not rejected`, + + ); + } + + if (requestStatus.isApproved && setupInfo) { + const { signer: inheritanceKey } = generateSignerFromMetaData({ + xpub: setupInfo.inheritanceXpub, + derivationPath: setupInfo.derivationPath, + masterFingerprint: setupInfo.masterFingerprint, + signerType: SignerType.INHERITANCEKEY, + storageType: SignerStorage.WARM, + isMultisig: true, + inheritanceKeyInfo: { + configuration: setupInfo.configuration, + // policy: setupInfo.policy, // policy doesn't really apply to the heir + }, + xfp: setupInfo.id, + }); + if (setupInfo.configuration.bsms) { + initateRecovery(setupInfo.configuration.bsms); + } else { + // showToast('Cannot recreate vault as BSMS was not present', ); + } + dispatch(addSigningDevice([inheritanceKey])); + dispatch(setInheritanceRequestId('')); // clear approved request + showToast(`${inheritanceKey.signerName} added successfully`, ); + navigation.goBack(); + } } catch (err) { showToast(`${err}`, ); } - close(); }; - const { Illustration, Instructions, title, subTitle, unsupported } = getSignerContent( - type, - isMultisig, - translations, - isHealthcheck - ); + const setupInheritanceKey = async () => { + try { + close(); + setInProgress(true); + const { setupData } = await InheritanceKeyServer.initializeIKSetup(); + const { id, inheritanceXpub: xpub, derivationPath, masterFingerprint } = setupData; + const { signer: inheritanceKey } = generateSignerFromMetaData({ + xpub, + derivationPath, + masterFingerprint, + signerType: SignerType.INHERITANCEKEY, + storageType: SignerStorage.WARM, + xfp: id, + isMultisig: true, + }); + setInProgress(false); + dispatch(addSigningDevice([inheritanceKey])); + showToast(`${inheritanceKey.signerName} added successfully`, ); + } catch (err) { + console.log({ err }); + showToast(`Failed to add inheritance key`, ); + } + }; + + const { + Illustration, + Instructions, + title, + subTitle, + unsupported, + options, + sepInstruction = '', + } = getSignerContent(type, isMultisig, translations, isHealthcheck); + const [selectInheritanceType, setSelectInheritanceType] = useState(1); const Content = useCallback( - () => , - [] + () => ( + + ), + [selectInheritanceType] ); const buttonCallback = () => { @@ -1077,7 +1466,7 @@ function HardwareModalMap({ case SignerType.POLICY_SERVER: return navigateToSigningServerSetup(); case SignerType.MOBILE_KEY: - return navigateToMobileKey(); + return navigateToMobileKey(isMultisig); case SignerType.SEED_WORDS: return navigateToSeedWordSetup(); case SignerType.BITBOX02: @@ -1086,6 +1475,7 @@ function HardwareModalMap({ return navigateToSetupWithChannel(); case SignerType.PASSPORT: case SignerType.SEEDSIGNER: + case SignerType.SPECTER: case SignerType.KEYSTONE: case SignerType.JADE: case SignerType.KEEPER: @@ -1093,7 +1483,7 @@ function HardwareModalMap({ case SignerType.OTHER_SD: return navigateToSetupWithOtherSD(); case SignerType.INHERITANCEKEY: - return requestInheritanceKeyRecovery(signingDevices); + return handleInheritanceKey(); default: return null; } @@ -1114,11 +1504,19 @@ function HardwareModalMap({ textColor={`${colorMode}.primaryText`} buttonBackground={`${colorMode}.greenButtonBackground`} Content={Content} - secondaryButtonText={isHealthcheck ? 'Skip' : null} - secondaryCallback={isHealthcheck ? skipHealthCheckCallBack : null} + secondaryButtonText={ + isHealthcheck ? 'Skip' : type === SignerType.INHERITANCEKEY ? 'cancel' : null + } + secondaryCallback={ + isHealthcheck + ? skipHealthCheckCallBack + : type === SignerType.INHERITANCEKEY + ? close + : null + } /> { setPasswordModal(false); }} @@ -1136,14 +1534,20 @@ function HardwareModalMap({ close: () => { setPasswordModal(false); }, + isMultisig, + addSignerFlow, }) } /> ; + addSignerFlow?: boolean; + mode: InteracationMode; + signerXfp?: string; //needed in Identification and HC flow }) { const dispatch = useDispatch(); const nav = navigation ?? useNavigation(); const { showToast } = useToastMessage(); const addMockSigner = () => { try { - const signer = getMockSigner(signerType); - if (signer) { - if (!isRecovery) { - dispatch(addSigningDevice(signer)); - nav.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); - } - if (isRecovery) { - dispatch(setSigningDevices(signer)); - nav.dispatch(CommonActions.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' })); - } + const data = getMockSigner(signerType); + if (data.signer && data.key) { + const { signer } = data; + dispatch(addSigningDevice([signer])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + nav.dispatch(CommonActions.navigate(navigationState)); showToast(`${signer.signerName} added successfully`, ); } + } catch (error) { + if (error.toString().includes(`We don't support`)) { + showToast(error.toString()); + return; + } + captureError(error); + } + }; + const { mapUnknownSigner } = useUnkownSigners(); + const verifyMockSigner = () => { + try { + const data = getMockSigner(signerType); + console.log(data.signer.masterFingerprint, mode); + const handleSuccess = () => { + dispatch(healthCheckSigner([data.signer])); + nav.dispatch(CommonActions.goBack()); + showToast(`${data.signer.type} verified successfully`, ); + }; + + const handleFailure = () => { + showToast('Something went wrong, please try again!', null, 2000, true); + }; + + if (mode === InteracationMode.IDENTIFICATION) { + const mapped = mapUnknownSigner({ + masterFingerprint: data.signer.masterFingerprint, + type: data.signer.type, + }); + if (mapped) { + handleSuccess(); + } else { + handleFailure(); + } + } else { + if (signerXfp === data.signer.masterFingerprint) { + console.log('here'); + handleSuccess(); + } else { + handleFailure(); + } + } } catch (error) { captureError(error); + console.error('Vrification Failed', error); + } + }; + + const handleMockTap = () => { + if (mode === InteracationMode.VAULT_ADDITION || mode === InteracationMode.APP_ADDITION) { + addMockSigner(); + } else if (mode === InteracationMode.HEALTH_CHECK) { + verifyMockSigner(); + } else if (mode === InteracationMode.IDENTIFICATION) { + verifyMockSigner(); + } else { + console.log('unhandled case'); } }; if (!enable) { return children; } return ( - + {children} ); diff --git a/src/screens/Vault/NFCScanner.tsx b/src/screens/Vault/NFCScanner.tsx index e81208b1f..b346097cb 100644 --- a/src/screens/Vault/NFCScanner.tsx +++ b/src/screens/Vault/NFCScanner.tsx @@ -17,7 +17,6 @@ import { SignerStorage, SignerType } from 'src/core/wallets/enums'; import { useDispatch } from 'react-redux'; import { addSigningDevice } from 'src/store/sagaActions/vaults'; import { CommonActions, useNavigation } from '@react-navigation/native'; -import { checkSigningDevice } from './AddSigningDevice'; import HWError from 'src/hardware/HWErrorState'; import { captureError } from 'src/services/sentry'; import TickIcon from 'src/assets/images/icon_tick.svg'; @@ -28,29 +27,29 @@ const NFCScanner = ({ route }) => { const dispatch = useDispatch(); const navigation = useNavigation(); - const { isMultisig }: { isMultisig: boolean } = route.params; + const { isMultisig, addSignerFlow = false }: { isMultisig: boolean; addSignerFlow?: boolean } = + route.params; const addColdCard = async () => { try { const ccDetails = await withNfcModal(async () => getColdcardDetails(isMultisig)); - const { xpub, derivationPath, xfp, xpubDetails } = ccDetails; - const coldcard = generateSignerFromMetaData({ + const { xpub, derivationPath, masterFingerprint, xpubDetails } = ccDetails; + const { signer } = generateSignerFromMetaData({ xpub, derivationPath, - xfp, + masterFingerprint, isMultisig, signerType: SignerType.COLDCARD, storageType: SignerStorage.COLD, xpubDetails, }); - dispatch(addSigningDevice(coldcard)); - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); - showToast(`${coldcard.signerName} added successfully`, ); - const exists = await checkSigningDevice(coldcard.signerId); - if (exists) showToast('Warning: Vault with this signer already exists', ); + dispatch(addSigningDevice([signer])); + const navigationState = addSignerFlow + ? { name: 'ManageSigners' } + : { name: 'AddSigningDevice', merge: true, params: {} }; + navigation.dispatch(CommonActions.navigate(navigationState)); + showToast(`${signer.signerName} added successfully`, ); } catch (error) { if (error instanceof HWError) { showToast(error.message, , 3000); diff --git a/src/screens/Vault/SignerAdvanceSettings.tsx b/src/screens/Vault/SignerAdvanceSettings.tsx index 1ac1a016a..bcebcf534 100644 --- a/src/screens/Vault/SignerAdvanceSettings.tsx +++ b/src/screens/Vault/SignerAdvanceSettings.tsx @@ -1,21 +1,20 @@ import Text from 'src/components/KeeperText'; -import { Box, HStack, useColorMode, VStack } from 'native-base'; +import { Box, VStack, useColorMode } from 'native-base'; + import { CommonActions, useNavigation } from '@react-navigation/native'; -import React, { useState } from 'react'; -import { Dimensions, StyleSheet } from 'react-native'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; +import React, { useContext, useState } from 'react'; +import { Dimensions, StyleSheet, TouchableOpacity } from 'react-native'; +import { Signer, Vault, VaultSigner } from 'src/core/wallets/interfaces/vault'; import KeeperHeader from 'src/components/KeeperHeader'; import NfcPrompt from 'src/components/NfcPromptAndroid'; import ScreenWrapper from 'src/components/ScreenWrapper'; import { SignerType } from 'src/core/wallets/enums'; -import { getSignerNameFromType, isSignerAMF } from 'src/hardware'; -import moment from 'moment'; +import TickIcon from 'src/assets/images/icon_tick.svg'; import { registerToColcard } from 'src/hardware/coldcard'; import idx from 'idx'; import { useDispatch } from 'react-redux'; -import { updateSignerDetails } from 'src/store/sagaActions/wallets'; +import { updateKeyDetails, updateSignerDetails } from 'src/store/sagaActions/wallets'; import useToastMessage from 'src/hooks/useToastMessage'; -import { globalStyles } from 'src/constants/globalStyles'; import useVault from 'src/hooks/useVault'; import useNfcModal from 'src/hooks/useNfcModal'; import { SDIcons } from './SigningDeviceIcons'; @@ -23,22 +22,56 @@ import DescriptionModal from './components/EditDescriptionModal'; import WarningIllustration from 'src/assets/images/warning.svg'; import KeeperModal from 'src/components/KeeperModal'; import OptionCard from 'src/components/OptionCard'; +import WalletVault from 'src/assets/images/wallet_vault.svg'; +import DeleteIcon from 'src/assets/images/delete_phone.svg'; + +import { hp, wp } from 'src/constants/responsive'; +import ActionCard from 'src/components/ActionCard'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import { ScrollView, TextInput } from 'react-native-gesture-handler'; +import { InheritanceAlert, InheritancePolicy } from 'src/services/interfaces'; +import InheritanceKeyServer from 'src/services/operations/InheritanceKey'; +import { captureError } from 'src/services/sentry'; +import { emailCheck } from 'src/utils/utilities'; +import CircleIconWrapper from 'src/components/CircleIconWrapper'; +import WalletFingerprint from 'src/components/WalletFingerPrint'; +import useSignerMap from 'src/hooks/useSignerMap'; const { width } = Dimensions.get('screen'); function SignerAdvanceSettings({ route }: any) { const { colorMode } = useColorMode(); - const { signer }: { signer: VaultSigner } = route.params; + const { + vaultKey, + vaultId, + signer: signerFromParam, + }: { signer: Signer; vaultKey: VaultSigner; vaultId: string } = route.params; + const { signerMap } = useSignerMap(); + const signer = signerFromParam ? signerFromParam : signerMap[vaultKey.masterFingerprint]; const { showToast } = useToastMessage(); - const signerName = getSignerNameFromType(signer.type, signer.isMock, isSignerAMF(signer)); - const [visible, setVisible] = useState(false); + const [editEmailModal, setEditEmailModal] = useState(false); + const [deleteEmailModal, setDeleteEmailModal] = useState(false); + + const currentEmail = idx(signer, (_) => _.inheritanceKeyInfo.policy.alert.emails[0]) || ''; + const [waningModal, setWarning] = useState(false); const { withNfcModal, nfcVisible, closeNfc } = useNfcModal(); const openDescriptionModal = () => setVisible(true); const closeDescriptionModal = () => setVisible(false); - const { activeVault } = useVault(); + const { activeVault, allVaults } = useVault({ vaultId, includeArchived: false }); + const signerVaults: Vault[] = []; + + allVaults.forEach((vault) => { + const keys = vault.signers; + for (const key of keys) { + if (signer.masterFingerprint === key.masterFingerprint) { + signerVaults.push(vault); + break; + } + } + }); const registerColdCard = async () => { await withNfcModal(() => registerToColcard({ vault: activeVault })); @@ -47,22 +80,84 @@ function SignerAdvanceSettings({ route }: any) { const navigation: any = useNavigation(); const dispatch = useDispatch(); + const updateIKSPolicy = async (removeEmail: string, newEmail?: string) => { + try { + if (!removeEmail && !newEmail) { + showToast('Nothing to update'); + navigation.goBack(); + return; + } + + const thresholdDescriptors = activeVault.signers.map((signer) => signer.xfp).slice(0, 2); + + if (signer.inheritanceKeyInfo === undefined) + showToast('Something went wrong, IKS configuration missing', ); + + const existingPolicy: InheritancePolicy = signer.inheritanceKeyInfo.policy; + const existingAlert: InheritanceAlert | any = + idx(signer, (_) => _.inheritanceKeyInfo.policy.alert) || {}; + const existingEmails = existingAlert.emails || []; + + // remove the previous email + const index = existingEmails.indexOf(removeEmail); + if (index !== -1) existingEmails.splice(index, 1); + + // add the new email(if provided) + const updatedEmails = [...existingEmails]; + if (newEmail) updatedEmails.push(newEmail); + + const updatedPolicy: InheritancePolicy = { + ...existingPolicy, + alert: { + ...existingAlert, + emails: updatedEmails, + }, + }; + + const { updated } = await InheritanceKeyServer.updateInheritancePolicy( + vaultKey.xfp, + updatedPolicy, + thresholdDescriptors + ); + + if (updated) { + const updateInheritanceKeyInfo = { + ...signer.inheritanceKeyInfo, + policy: updatedPolicy, + }; + + dispatch(updateSignerDetails(signer, 'inheritanceKeyInfo', updateInheritanceKeyInfo)); + showToast(`Email ${newEmail ? 'updated' : 'deleted'}`, ); + navigation.goBack(); + } else showToast(`Failed to ${newEmail ? 'update' : 'delete'} email`); + } catch (err) { + captureError(err); + showToast(`Failed to ${newEmail ? 'update' : 'delete'} email`); + } + }; + const registerSigner = async () => { switch (signer.type) { case SignerType.COLDCARD: await registerColdCard(); - dispatch(updateSignerDetails(signer, 'registered', true)); + dispatch( + updateKeyDetails(vaultKey, 'registered', { + registered: true, + vaultId: activeVault.id, + }) + ); return; case SignerType.LEDGER: case SignerType.BITBOX02: - navigation.dispatch(CommonActions.navigate('RegisterWithChannel', { signer })); + navigation.dispatch(CommonActions.navigate('RegisterWithChannel', { vaultKey, vaultId })); break; case SignerType.KEYSTONE: case SignerType.JADE: case SignerType.PASSPORT: case SignerType.SEEDSIGNER: + case SignerType.SPECTER: case SignerType.OTHER_SD: - navigation.dispatch(CommonActions.navigate('RegisterWithQR', { signer })); + navigation.dispatch(CommonActions.navigate('RegisterWithQR', { vaultKey, vaultId })); break; default: showToast('Comming soon', null, 1000); @@ -70,7 +165,7 @@ function SignerAdvanceSettings({ route }: any) { } }; - const navigateToPolicyChange = (signer: VaultSigner) => { + const navigateToPolicyChange = () => { const restrictions = idx(signer, (_) => _.signerPolicy.restrictions); const exceptions = idx(signer, (_) => _.signerPolicy.exceptions); navigation.dispatch( @@ -79,8 +174,10 @@ function SignerAdvanceSettings({ route }: any) { params: { restrictions, exceptions, - update: true, + isUpdate: true, signer, + vaultId, + vaultKey, }, }) ); @@ -91,7 +188,7 @@ function SignerAdvanceSettings({ route }: any) { - + If the signer is identified incorrectly there may be repurcusssions with general signer interactions like signing etc. @@ -100,6 +197,88 @@ function SignerAdvanceSettings({ route }: any) { ); } + const EditModalContent = () => { + const [email, setEmail] = useState(currentEmail); + const [emailStatusFail, setEmailStatusFail] = useState(false); + return ( + + + { + setEmail(value); + emailStatusFail && setEmailStatusFail(false); + }} + /> + {emailStatusFail && ( + + Email is not correct + + )} + { + setEditEmailModal(false); + setDeleteEmailModal(true); + }} + > + + + + + + + Delete Email + + This is a irreversible action + + + + + + + + Note: + + + If notification is not declined continuously for 30 days, the Key would be activated + + + {currentEmail !== email && ( + { + if (!emailCheck(email)) { + setEmailStatusFail(true); + } else { + updateIKSPolicy(currentEmail, email); + } + }} + > + + + Update + + + + )} + + ); + }; + + function DeleteEmailModalContent() { + return ( + + + + You would not receive daily reminders about your Inheritance Key if it is used + + + + ); + } + const navigateToAssignSigner = () => { setWarning(false); navigation.dispatch( @@ -107,7 +286,7 @@ function SignerAdvanceSettings({ route }: any) { name: 'AssignSignerType', params: { parentNavigation: navigation, - signer, + vault: activeVault, }, }) ); @@ -121,64 +300,91 @@ function SignerAdvanceSettings({ route }: any) { }; const isPolicyServer = signer.type === SignerType.POLICY_SERVER; - const isOtherSD = signer.type === SignerType.OTHER_SD; + const isInheritanceKey = signer.type === SignerType.INHERITANCEKEY; + const isAssistedKey = isPolicyServer || isInheritanceKey; + + const isOtherSD = signer.type === SignerType.UNKOWN_SIGNER; const isTapsigner = signer.type === SignerType.TAPSIGNER; - const changePolicy = () => { - if (isPolicyServer) navigateToPolicyChange(signer); - }; + const { translations } = useContext(LocalizationContext); - const { font12, font10, font14 } = globalStyles; + const { wallet: walletTranslation } = translations; return ( - - - - {SDIcons(signer.type, true).Icon} - - - {signerName} - - - {moment(signer.addedOn).format('DD MMM YYYY, HH:mmA')} - - {signer.signerDescription ? ( - - {signer.signerDescription} - - ) : null} - - - - - - setWarning(true)} + + } /> - {isPolicyServer && ( + - )} - {isTapsigner && ( - - )} + {isInheritanceKey && vaultId && ( + { + setEditEmailModal(true); + }} + /> + )} + {isAssistedKey || !vaultId ? null : ( + + )} + {/* disabling this temporarily */} + {/* setWarning(true)} + /> */} + {isPolicyServer && vaultId && ( + + )} + {isTapsigner && ( + + )} + {/* ---------TODO Pratyaksh--------- */} + {/* {}} /> */} + + + + {`Wallet used in ${signerVaults.length} wallet${signerVaults.length > 1 ? 's' : ''}`} + + + {signerVaults.map((vault) => ( + } + callback={() => {}} + /> + ))} + + + + + + setEditEmailModal(false)} + title="Registered Email" + subTitle="Delete or edit registered email" + subTitleColor="light.secondaryText" + buttonTextColor="light.white" + textColor="light.primaryText" + Content={EditModalContent} + /> + setDeleteEmailModal(false)} + title="Deleting Registered Email" + subTitle="Are you sure you want to delete email id?" + subTitleColor="light.secondaryText" + buttonTextColor="light.white" + textColor="light.primaryText" + buttonText="Delete" + buttonCallback={() => { + updateIKSPolicy(currentEmail); + }} + secondaryButtonText="Cancel" + secondaryCallback={() => setDeleteEmailModal(false)} + Content={DeleteEmailModalContent} + /> ); } @@ -246,4 +478,118 @@ const styles = StyleSheet.create({ descriptionContainer: { width: width * 0.8, }, + textInput: { + height: 55, + padding: 20, + backgroundColor: 'rgba(253, 247, 240, 1)', + borderRadius: 10, + }, + walletHeaderWrapper: { + margin: wp(15), + flexDirection: 'row', + width: '100%', + }, + walletIconWrapper: { + width: '15%', + }, + walletIconView: { + height: 40, + width: 40, + borderRadius: 40, + alignItems: 'center', + justifyContent: 'center', + }, + walletDescText: { + fontSize: 14, + }, + walletNameWrapper: { + width: '85%', + }, + walletNameText: { + fontSize: 20, + }, + inputContainer: { + alignItems: 'center', + borderBottomLeftRadius: 10, + borderTopLeftRadius: 10, + marginTop: '10%', + }, + inputWrapper: { + flexDirection: 'row', + width: '100%', + alignItems: 'center', + justifyContent: 'space-between', + borderRadius: 10, + height: 60, + }, + copyIconWrapper: { + padding: 10, + borderRadius: 10, + marginRight: 5, + }, + deleteContentWrapper: { + borderWidth: 1, + borderStyle: 'dashed', + borderRadius: 10, + marginVertical: hp(10), + gap: 10, + padding: 10, + height: hp(70), + alignItems: 'center', + flexDirection: 'row', + }, + warningIconWrapper: { + alignItems: 'center', + marginVertical: hp(20), + }, + noteText: { + fontWeight: '900', + fontSize: 14, + }, + noteDescription: { + fontSize: 13, + padding: 1, + letterSpacing: 0.65, + }, + editModalContainer: {}, + fw800: { + fontWeight: '800', + }, + fingerprintContainer: { + justifyContent: 'center', + paddingLeft: 2, + }, + w80: { + width: '80%', + }, + warningText: { + fontSize: 13, + padding: 1, + letterSpacing: 0.65, + }, + walletUsedText: { + marginLeft: 2, + marginVertical: 20, + }, + actionCardContainer: { + gap: 5, + }, + cta: { + borderRadius: 10, + width: wp(120), + height: hp(45), + justifyContent: 'center', + alignItems: 'center', + }, + ctaText: { + fontSize: 13, + letterSpacing: 1, + }, + updateBtnCtaStyle: { alignItems: 'flex-end', marginTop: 10 }, + errorStyle: { + marginTop: 10, + }, + fingerprint: { + alignItems: 'center', + }, }); diff --git a/src/screens/Vault/SigningDeviceChecklist.tsx b/src/screens/Vault/SigningDeviceChecklist.tsx index 5916d3046..6a275e6e2 100644 --- a/src/screens/Vault/SigningDeviceChecklist.tsx +++ b/src/screens/Vault/SigningDeviceChecklist.tsx @@ -3,42 +3,37 @@ import { Box, useColorMode } from 'native-base'; import React from 'react'; import DotView from 'src/components/DotView'; import moment from 'moment'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Signer } from 'src/core/wallets/interfaces/vault'; -function SigningDeviceChecklist({ signer }: { signer: VaultSigner }) { +function SigningDeviceChecklist({ item }: { item: Signer }) { const { colorMode } = useColorMode(); return ( - {signer && ( + {item && ( - + - - {moment(signer?.lastHealthCheck).calendar()} + + Health Check Successful + + + {moment(item?.lastHealthCheck).calendar()} - - Health Check Successful - )} diff --git a/src/screens/Vault/SigningDeviceDetails.tsx b/src/screens/Vault/SigningDeviceDetails.tsx index e1d66a1ed..b9ce7e369 100644 --- a/src/screens/Vault/SigningDeviceDetails.tsx +++ b/src/screens/Vault/SigningDeviceDetails.tsx @@ -1,24 +1,22 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { StyleSheet } from 'react-native'; import { Box, Center, useColorMode } from 'native-base'; import { CommonActions, useNavigation } from '@react-navigation/native'; import { useDispatch } from 'react-redux'; -import moment from 'moment'; import Text from 'src/components/KeeperText'; import ScreenWrapper from 'src/components/ScreenWrapper'; import { ScrollView } from 'react-native-gesture-handler'; -import { hp, windowWidth, wp } from 'src/constants/responsive'; +import { hp, wp } from 'src/constants/responsive'; import KeeperHeader from 'src/components/KeeperHeader'; -import { getSignerNameFromType, isSignerAMF } from 'src/hardware'; import useToastMessage from 'src/hooks/useToastMessage'; import KeeperModal from 'src/components/KeeperModal'; import SeedSigner from 'src/assets/images/seedsigner_setup.svg'; import Ledger from 'src/assets/images/ledger_image.svg'; import Keystone from 'src/assets/images/keystone_illustration.svg'; import PassportSVG from 'src/assets/images/illustration_passport.svg'; -import AdvnaceOptions from 'src/assets/images/Advancedoptions.svg'; +import AdvnaceOptions from 'src/assets/images/settings.svg'; import Change from 'src/assets/images/change.svg'; -import HealthCheck from 'src/assets/images/heathcheck.svg'; +import HealthCheck from 'src/assets/images/healthcheck_light.svg'; import SkipHealthCheck from 'src/assets/images/skipHealthCheck.svg'; import TapsignerSetupImage from 'src/assets/images/TapsignerSetup.svg'; import ColdCardSetupImage from 'src/assets/images/ColdCardSetup.svg'; @@ -30,21 +28,24 @@ import BitboxImage from 'src/assets/images/bitboxSetup.svg'; import TrezorSetup from 'src/assets/images/trezor_setup.svg'; import JadeSVG from 'src/assets/images/illustration_jade.svg'; import InhertanceKeyIcon from 'src/assets/images/illustration_inheritanceKey.svg'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; import { SignerType } from 'src/core/wallets/enums'; import { healthCheckSigner } from 'src/store/sagaActions/bhr'; import useVault from 'src/hooks/useVault'; -import SigningDeviceChecklist from './SigningDeviceChecklist'; -import { SDIcons } from './SigningDeviceIcons'; -import HardwareModalMap, { InteracationMode } from './HardwareModalMap'; import { useQuery } from '@realm/react'; import { RealmSchema } from 'src/storage/realm/enum'; import { KeeperApp } from 'src/models/interfaces/KeeperApp'; import { getJSONFromRealmObject } from 'src/storage/realm/utils'; -import IdentifySignerModal from './components/IdentifySignerModal'; import KeeperFooter from 'src/components/KeeperFooter'; import openLink from 'src/utils/OpenLink'; import { KEEPER_KNOWLEDGEBASE } from 'src/core/config'; +import SigningDeviceChecklist from './SigningDeviceChecklist'; +import HardwareModalMap, { InteracationMode } from './HardwareModalMap'; +import IdentifySignerModal from './components/IdentifySignerModal'; +import { SDIcons } from './SigningDeviceIcons'; +import moment from 'moment'; +import CircleIconWrapper from 'src/components/CircleIconWrapper'; +import useSignerMap from 'src/hooks/useSignerMap'; +import CurrencyTypeSwitch from 'src/components/Switch/CurrencyTypeSwitch'; const getSignerContent = (type: SignerType) => { switch (type) { @@ -55,7 +56,7 @@ const getSignerContent = (type: SignerType) => { 'Coldcard is an easy-to-use, ultra-secure, open-source, and affordable hardware wallet that is easy to back up via an encrypted microSD card. Your private key is stored in a dedicated security chip.', assert: , description: - '\u2022 Coldcard provides the best Physical Security.\n\u2022 All of the Coldcard is viewable, editable, and verifiable. You can compile it yourself.\n\u2022 Only signing device (hardware wallet) with the option to avoid ever being connected to a computer.', + '\u2022 Coldcard provides the best Physical Security.\n\u2022 All of the Coldcard is viewable, editable, and verifiable. You can compile it yourself.\n\u2022 Only signer (hardware wallet) with the option to avoid ever being connected to a computer.', FAQ: 'https://coldcard.com/docs/faq', }; case SignerType.TAPSIGNER: @@ -81,7 +82,7 @@ const getSignerContent = (type: SignerType) => { return { title: 'SeedSigner', subTitle: - 'The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50).', + 'The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signer using inexpensive, publicly available hardware components (usually < $50).', assert: , description: '\u2022 SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multi-signature wallet setup. \n\u2022 It also help users transact with Bitcoin via a secure, air-gapped QR-exchange signing model.', @@ -127,8 +128,8 @@ const getSignerContent = (type: SignerType) => { }; case SignerType.KEEPER: return { - title: 'Keeper as signing device', - subTitle: 'You can use a specific BIP-85 wallet on another Keeper app as a signer', + title: 'Keeper as signer', + subTitle: 'You can use a specific BIP-85 wallet on Collaborative Key as a signer', assert: , description: '\u2022Make sure that the other Keeper app is backed up using the 12-word Recovery Phrase.\n\u2022 When you want to sign a transaction using this option, you will have to navigate to the specific wallet used', @@ -138,10 +139,10 @@ const getSignerContent = (type: SignerType) => { return { title: 'Signing Server', subTitle: - 'The key on the Signing Server will sign a transaction depending on the policy and authentication', + 'The key on the signer will sign a transaction depending on the policy and authentication', assert: , description: - '\u2022An auth app provides the 6-digit authentication code.\n\u2022 When restoring the app using signing devices, you will need to provide this code. \n\u2022 Considered a hot key as it is on a connected online server', + '\u2022An auth app provides the 6-digit authentication code.\n\u2022 When restoring the app using signers, you will need to provide this code. \n\u2022 Considered a hot key as it is on a connected online server', FAQ: '', }; case SignerType.BITBOX02: @@ -197,27 +198,34 @@ function SigningDeviceDetails({ route }) { const { colorMode } = useColorMode(); const navigation = useNavigation(); const dispatch = useDispatch(); - const { signerId = null } = route.params; + const { vaultKey, vaultId, signer: currentSigner, vaultSigners } = route.params; + const { signerMap } = useSignerMap(); + const signer = currentSigner ? currentSigner : signerMap[vaultKey.masterFingerprint]; const [detailModal, setDetailModal] = useState(false); const [skipHealthCheckModalVisible, setSkipHealthCheckModalVisible] = useState(false); const [visible, setVisible] = useState(false); const [identifySignerModal, setIdentifySignerModal] = useState(false); const { showToast } = useToastMessage(); - const { activeVault } = useVault(); - const signer: VaultSigner = activeVault.signers.filter( - (signer) => signer?.signerId === signerId - )[0]; + const { activeVault } = useVault({ vaultId }); const { primaryMnemonic }: KeeperApp = useQuery(RealmSchema.KeeperApp).map( getJSONFromRealmObject )[0]; + const [healthCheckArray, setHealthCheckArray] = useState([]); + + useEffect(() => { + if (signer) { + setHealthCheckArray([ + { name: 'Health Check Successful', lastHealthCheck: signer.lastHealthCheck }, + ]); + } + }, []); + if (!signer) { return null; } const { title, subTitle, assert, description, FAQ } = getSignerContent(signer?.type); - const { Icon } = SDIcons(signer?.type, true); - function SignerContent() { return ( @@ -243,165 +251,160 @@ function SigningDeviceDetails({ route }) { - You can choose to manually confirm the health of the Signing Device if you are sure that - they are secure and accessible. Or you can choose to do the Health Check when you can + You can choose to manually confirm the health of the signer if you are sure that they are + secure and accessible. Or you can choose to do the Health Check when you can ); } - const FooterIcon = ({ Icon }) => { + function FooterIcon({ Icon }) { return ( ); - }; + } const identifySigner = signer.type === SignerType.OTHER_SD; const footerItems = [ - { - text: 'Change signing device', - Icon: () => , - onPress: () => - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ), - }, { text: 'Health Check', Icon: () => , onPress: () => { - if (signer.type === SignerType.OTHER_SD) { - setIdentifySignerModal(true); + if (signer.type === SignerType.UNKOWN_SIGNER) { + navigation.dispatch( + CommonActions.navigate({ + name: 'AssignSignerType', + params: { + parentNavigation: navigation, + vault: activeVault, + }, + }) + ); } else { setVisible(true); } }, }, { - text: 'Advance Options', + text: 'Settings', Icon: () => , onPress: () => { - navigation.dispatch(CommonActions.navigate('SignerAdvanceSettings', { signer })); + navigation.dispatch( + CommonActions.navigate('SignerAdvanceSettings', { signer, vaultKey, vaultId }) + ); }, }, ]; + if (vaultKey) { + footerItems.push({ + text: 'Change Signer', + Icon: () => , + onPress: () => + navigation.dispatch( + CommonActions.navigate({ + name: 'AddSigningDevice', + merge: true, + params: { vaultId, scheme: activeVault.scheme }, + }) + ), + }); + } + return ( - setDetailModal(true)} /> - - - {Icon} - - - - - {getSignerNameFromType(signer?.type, signer?.isMock, isSignerAMF(signer))} - - {`Added on ${moment( - signer?.addedOn - ).format('DD MMM YY, HH:mm A')}`} - - + setDetailModal(true)} + learnTextColor={`${colorMode}.white`} + title={signer.signerName} + subtitle={ + signer.signerDescription || `Added on ${moment(signer.addedOn).calendar().toLowerCase()}` + } + icon={ + + } + rightComponent={} + /> + + Recent History - - - + + + {healthCheckArray.map((_, index) => ( + + ))} - - - You will be reminded in 90 days for the health check - - - setVisible(false)} - signer={signer} - skipHealthCheckCallBack={() => { - setVisible(false); - setSkipHealthCheckModalVisible(true); - }} - mode={InteracationMode.HEALTH_CHECK} - vaultShellId={activeVault.shellId} - isMultisig={activeVault.isMultiSig} - primaryMnemonic={primaryMnemonic} - /> - setSkipHealthCheckModalVisible(false)} - title="Skipping Health Check" - subTitle="It is very important that you keep your Signing Devices secure and fairly accessible at all times." - buttonText="Do Later" - secondaryButtonText="Confirm Access" - buttonTextColor="light.white" - buttonCallback={() => setSkipHealthCheckModalVisible(false)} - secondaryCallback={() => { - dispatch(healthCheckSigner([signer])); - showToast('Device verified manually!'); - setSkipHealthCheckModalVisible(false); - }} - textColor="light.primaryText" - Content={HealthCheckSkipContent} - /> - setDetailModal(false)} - title={title} - subTitle={subTitle} - modalBackground={`${colorMode}.modalGreenBackground`} - textColor="light.white" - learnMoreCallback={() => openLink(FAQ)} - Content={SignerContent} - DarkCloseIcon - learnMore - /> - setIdentifySignerModal(false)} - signer={signer} - secondaryCallback={() => { - setVisible(true); - }} - /> - + + setVisible(false)} + signer={signer} + skipHealthCheckCallBack={() => { + setVisible(false); + setSkipHealthCheckModalVisible(true); + }} + mode={InteracationMode.HEALTH_CHECK} + isMultisig={activeVault?.isMultiSig || true} + primaryMnemonic={primaryMnemonic} + vaultId={vaultId} + addSignerFlow={false} + vaultSigners={vaultSigners} + /> + setSkipHealthCheckModalVisible(false)} + title="Skipping Health Check" + subTitle="It is very important that you keep your signers secure and fairly accessible at all times." + buttonText="Do Later" + secondaryButtonText="Confirm Access" + buttonTextColor="light.white" + buttonCallback={() => setSkipHealthCheckModalVisible(false)} + secondaryCallback={() => { + dispatch(healthCheckSigner([signer])); + showToast('Device verified manually!'); + setSkipHealthCheckModalVisible(false); + }} + textColor="light.primaryText" + Content={HealthCheckSkipContent} + /> + setDetailModal(false)} + title={title} + subTitle={subTitle} + modalBackground={`${colorMode}.modalGreenBackground`} + textColor="light.white" + learnMoreCallback={() => openLink(FAQ)} + Content={SignerContent} + DarkCloseIcon + learnMore + /> + setIdentifySignerModal(false)} + signer={signer} + secondaryCallback={() => { + setVisible(true); + }} + vaultId={vaultId} + /> ); } @@ -409,6 +412,32 @@ const styles = StyleSheet.create({ skipHealthIllustration: { marginLeft: wp(25), }, + walletHeaderWrapper: { + margin: wp(15), + flexDirection: 'row', + width: '100%', + }, + walletIconWrapper: { + width: '15%', + }, + walletIconView: { + height: 40, + width: 40, + borderRadius: 40, + alignItems: 'center', + justifyContent: 'center', + }, + walletDescText: { + fontSize: 14, + }, + walletNameWrapper: { + width: '85%', + marginLeft: 10, + }, + walletNameText: { + fontSize: 20, + }, + recentHistoryText: { fontSize: 16, padding: '7%' }, }); export default SigningDeviceDetails; diff --git a/src/screens/Vault/SigningDeviceIcons.tsx b/src/screens/Vault/SigningDeviceIcons.tsx index f74d1cdfa..7311281df 100644 --- a/src/screens/Vault/SigningDeviceIcons.tsx +++ b/src/screens/Vault/SigningDeviceIcons.tsx @@ -24,6 +24,9 @@ import PASSPORTLOGO from 'src/assets/images/passport_logo.svg'; import SEEDSIGNERICON from 'src/assets/images/seedsigner_icon.svg'; import SEEDSIGNERICONLIGHT from 'src/assets/images/seedsigner_light.svg'; import SEEDSIGNERLOGO from 'src/assets/images/seedsignerlogo.svg'; +import SPECTERICON from 'src/assets/images/specter_icon.svg'; +import SPECTERICONLIGHT from 'src/assets/images/specter_icon_light.svg'; +import SPECTERLOGO from 'src/assets/images/specterlogo.svg'; import SEEDWORDS from 'src/assets/images/seedwords.svg'; import SEEDWORDSLIGHT from 'src/assets/images/seedwordsLight.svg'; import SERVER from 'src/assets/images/server.svg'; @@ -69,7 +72,7 @@ export const SDIcons = (type: SignerType, light = false) => { Icon: getColouredIcon(, , light), Logo: ( - Another Keeper App + Collaborative Key ), }; @@ -129,6 +132,17 @@ export const SDIcons = (type: SignerType, light = false) => { Logo: , type: SignerStorage.COLD, }; + case SignerType.SPECTER: + return { + Icon: getColouredIcon(, , light), + // Logo: , + Logo: ( + + Specter DIY + + ), + type: SignerStorage.COLD, + }; case SignerType.BITBOX02: return { Icon: getColouredIcon(, , light), @@ -140,7 +154,18 @@ export const SDIcons = (type: SignerType, light = false) => { Icon: getColouredIcon(, , light), Logo: ( - Other Signing Device + Other signer + + ), + type: SignerStorage.COLD, + }; + + case SignerType.UNKOWN_SIGNER: + return { + Icon: getColouredIcon(, , light), + Logo: ( + + Unknonw Signer ), type: SignerStorage.COLD, diff --git a/src/screens/Vault/SigningDeviceList.tsx b/src/screens/Vault/SigningDeviceList.tsx index d774bf58f..7ad85aa1b 100644 --- a/src/screens/Vault/SigningDeviceList.tsx +++ b/src/screens/Vault/SigningDeviceList.tsx @@ -6,13 +6,10 @@ import React, { useContext, useEffect, useState } from 'react'; import { KEEPER_KNOWLEDGEBASE } from 'src/core/config'; import { hp, windowHeight, windowWidth, wp } from 'src/constants/responsive'; import { useAppDispatch, useAppSelector } from 'src/store/hooks'; - import KeeperHeader from 'src/components/KeeperHeader'; - import KeeperModal from 'src/components/KeeperModal'; import { LocalizationContext } from 'src/context/Localization/LocContext'; import NFC from 'src/services/nfc'; - import ScreenWrapper from 'src/components/ScreenWrapper'; import { ScrollView } from 'react-native-gesture-handler'; import { SignerType } from 'src/core/wallets/enums'; @@ -22,15 +19,16 @@ import openLink from 'src/utils/OpenLink'; import { setSdIntroModal } from 'src/store/reducers/vaults'; import usePlan from 'src/hooks/usePlan'; import Note from 'src/components/Note/Note'; -import { SDIcons } from './SigningDeviceIcons'; -import HardwareModalMap, { InteracationMode } from './HardwareModalMap'; import { useQuery } from '@realm/react'; import { RealmSchema } from 'src/storage/realm/enum'; import { KeeperApp } from 'src/models/interfaces/KeeperApp'; import { getJSONFromRealmObject } from 'src/storage/realm/utils'; import { getDeviceStatus, getSDMessage } from 'src/hardware'; import { useRoute } from '@react-navigation/native'; -import { VaultScheme } from 'src/core/wallets/interfaces/vault'; +import { VaultScheme, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import useSigners from 'src/hooks/useSigners'; +import HardwareModalMap, { InteracationMode } from './HardwareModalMap'; +import { SDIcons } from './SigningDeviceIcons'; type HWProps = { type: SignerType; @@ -42,19 +40,30 @@ type HWProps = { function SigningDeviceList() { const route = useRoute(); - const { scheme }: { scheme: VaultScheme } = route.params as any; + const { + scheme, + addSignerFlow = false, + vaultId, + vaultSigners, + }: { + scheme: VaultScheme; + addSignerFlow: boolean; + vaultId: string; + vaultSigners?: VaultSigner[]; + } = route.params as any; const { colorMode } = useColorMode(); const { translations } = useContext(LocalizationContext); const { plan } = usePlan(); const dispatch = useAppDispatch(); const isOnL1 = plan === SubscriptionTier.L1.toUpperCase(); - const vaultSigners = useAppSelector((state) => state.vault.signers); + const isOnL2 = plan === SubscriptionTier.L2.toUpperCase(); + const sdModal = useAppSelector((state) => state.vault.sdIntroModal); const { primaryMnemonic }: KeeperApp = useQuery(RealmSchema.KeeperApp).map( getJSONFromRealmObject )[0]; - const isMultisig = scheme.n !== 1; - + const isMultisig = addSignerFlow ? true : scheme.n !== 1; + const { signers } = useSigners(); const [isNfcSupported, setNfcSupport] = useState(true); const [signersLoaded, setSignersLoaded] = useState(false); @@ -73,7 +82,7 @@ function SigningDeviceList() { - {`In the ${SubscriptionTier.L1} tier, you can add one signing device to activate your vault. This can be upgraded to three signing devices and five signing devices on ${SubscriptionTier.L2} and ${SubscriptionTier.L3} tiers\n\nIf a particular signing device is not supported, it will be indicated.`} + {`In the ${SubscriptionTier.L1} tier, you can add one signer to activate your vault. This can be upgraded to three signers and five signers on ${SubscriptionTier.L2} and ${SubscriptionTier.L3} tiers\n\nIf a particular signer is not supported, it will be indicated.`}
); @@ -93,12 +102,15 @@ function SigningDeviceList() { SignerType.PASSPORT, SignerType.JADE, SignerType.KEYSTONE, + SignerType.SPECTER, SignerType.OTHER_SD, SignerType.SEED_WORDS, - SignerType.MOBILE_KEY, - SignerType.POLICY_SERVER, + // SignerType.MOBILE_KEY, SignerType.KEEPER, + SignerType.POLICY_SERVER, + SignerType.INHERITANCEKEY, ]; + function HardWareWallet({ type, disabled, message, first = false, last = false }: HWProps) { const [visible, setVisible] = useState(false); @@ -129,7 +141,7 @@ function SigningDeviceList() { {SDIcons(type).Logo} - + {message} @@ -141,9 +153,12 @@ function SigningDeviceList() { visible={visible} close={close} type={type} - mode={InteracationMode.SIGNING} + mode={InteracationMode.VAULT_ADDITION} isMultisig={isMultisig} primaryMnemonic={primaryMnemonic} + addSignerFlow={addSignerFlow} + vaultId={vaultId} + vaultSigners={vaultSigners} /> ); @@ -153,8 +168,10 @@ function SigningDeviceList() { { dispatch(setSdIntroModal(true)); }} @@ -169,9 +186,11 @@ function SigningDeviceList() { const { disabled, message: connectivityStatus } = getDeviceStatus( type, isNfcSupported, - vaultSigners, isOnL1, - scheme + isOnL2, + scheme, + signers, + addSignerFlow ); let message = connectivityStatus; if (!connectivityStatus) { @@ -191,14 +210,13 @@ function SigningDeviceList() {
)} - { dispatch(setSdIntroModal(false)); }} - title="Signing Devices" - subTitle="A signing device is a hardware or software that stores one of the private keys needed for your Vault" + title="Signers" + subTitle="A signer is a hardware or software that stores one of the private keys needed for your vaults" modalBackground={`${colorMode}.modalGreenBackground`} buttonTextColor={colorMode === 'light' ? `${colorMode}.greenText2` : `${colorMode}.white`} buttonBackground={`${colorMode}.modalWhiteButton`} @@ -215,12 +233,13 @@ function SigningDeviceList() { } />
- + + + ); } @@ -249,7 +268,7 @@ const styles = StyleSheet.create({ }, walletMapContainer: { alignItems: 'center', - height: windowHeight * 0.08, + minHeight: windowHeight * 0.08, flexDirection: 'row', paddingLeft: wp(40), }, @@ -268,6 +287,7 @@ const styles = StyleSheet.create({ fontWeight: '400', letterSpacing: 1.3, marginTop: hp(5), + width: windowWidth * 0.6, }, dividerStyle: { opacity: 0.1, @@ -282,5 +302,8 @@ const styles = StyleSheet.create({ italics: { fontStyle: 'italic', }, + noteContainer: { + paddingHorizontal: 20, + }, }); export default SigningDeviceList; diff --git a/src/screens/Vault/SigningServer.tsx b/src/screens/Vault/SigningServer.tsx index bb73f258e..6f37416aa 100644 --- a/src/screens/Vault/SigningServer.tsx +++ b/src/screens/Vault/SigningServer.tsx @@ -27,7 +27,8 @@ function SigningServer({ navigation }) { justifyContent: 'center', alignItems: 'center', }} - backgroundColor={`${colorMode}.coffeeBackground`}> + backgroundColor={`${colorMode}.coffeeBackground`} + >
); diff --git a/src/screens/Vault/TimelockScreen.tsx b/src/screens/Vault/TimelockScreen.tsx index 6e517c033..6a302a0f2 100644 --- a/src/screens/Vault/TimelockScreen.tsx +++ b/src/screens/Vault/TimelockScreen.tsx @@ -22,7 +22,7 @@ function TimelockScreen() { { - navigate('LoginStack', { - screen: 'ScanQRFileRecovery', - }); + navigate('ScanQRFileRecovery'); }} /> initateRecovery(inputText)} - primaryText="Recover" + primaryText="Create Vault" primaryLoading={recoveryLoading} /> @@ -63,7 +61,7 @@ function VaultConfigurationRecovery() { ); } -export default VaultConfigurationRecovery; +export default VaultConfigurationCreation; const styles = StyleSheet.create({ wrapper: { diff --git a/src/screens/Recovery/OtherRecoveryMethods.tsx b/src/screens/Vault/VaultCreationOptions.tsx similarity index 68% rename from src/screens/Recovery/OtherRecoveryMethods.tsx rename to src/screens/Vault/VaultCreationOptions.tsx index ffdd6a116..80ce4dd75 100644 --- a/src/screens/Recovery/OtherRecoveryMethods.tsx +++ b/src/screens/Vault/VaultCreationOptions.tsx @@ -1,4 +1,3 @@ -import { StyleSheet } from 'react-native'; import React from 'react'; import ScreenWrapper from 'src/components/ScreenWrapper'; import KeeperHeader from 'src/components/KeeperHeader'; @@ -7,27 +6,27 @@ import { useNavigation } from '@react-navigation/native'; import { hp } from 'src/constants/responsive'; import { Tile } from '../NewKeeperAppScreen/NewKeeperAppScreen'; -function OtherRecoveryMethods() { +function VaultCreationOptions() { const { navigate } = useNavigation(); return ( { - navigate('LoginStack', { screen: 'VaultSetup', params: { isRecreation: true } }); + navigate('VaultSetup'); }} /> { - navigate('LoginStack', { screen: 'VaultConfigurationRecovery' }); + navigate('VaultConfigurationCreation'); }} /> @@ -35,7 +34,7 @@ function OtherRecoveryMethods() { title="Signing Device with Vault details" subTitle="These are the signing devices where you may have registered the Vault" onPress={() => { - navigate('LoginStack', { screen: 'SigningDeviceConfigRecovery' }); + navigate('SigningDeviceConfigRecovery'); }} /> @@ -43,6 +42,4 @@ function OtherRecoveryMethods() { ); } -export default OtherRecoveryMethods; - -const styles = StyleSheet.create({}); +export default VaultCreationOptions; diff --git a/src/screens/Vault/VaultDetails.tsx b/src/screens/Vault/VaultDetails.tsx index 09a4c6f69..be656a1b0 100644 --- a/src/screens/Vault/VaultDetails.tsx +++ b/src/screens/Vault/VaultDetails.tsx @@ -1,67 +1,61 @@ /* eslint-disable react/no-unstable-nested-components */ import Text from 'src/components/KeeperText'; import { Box, HStack, VStack, View, useColorMode, Pressable, StatusBar } from 'native-base'; -import { CommonActions, useNavigation, useRoute } from '@react-navigation/native'; -import { FlatList, Linking, RefreshControl, StyleSheet, TouchableOpacity } from 'react-native'; +import { CommonActions, useNavigation } from '@react-navigation/native'; +import { FlatList, RefreshControl, StyleSheet } from 'react-native'; import React, { useCallback, useContext, useEffect, useState } from 'react'; import { hp, windowHeight, windowWidth, wp } from 'src/constants/responsive'; -import Buy from 'src/assets/images/icon_buy.svg'; -import IconArrowBlack from 'src/assets/images/icon_arrow_black.svg'; -import IconSettings from 'src/assets/images/icon_settings.svg'; +import CoinIcon from 'src/assets/images/coins.svg'; +import SignerIcon from 'src/assets/images/signer_white.svg'; import KeeperModal from 'src/components/KeeperModal'; -import Recieve from 'src/assets/images/receive.svg'; -import { ScrollView } from 'react-native-gesture-handler'; -import Send from 'src/assets/images/send.svg'; -import SignerIcon from 'src/assets/images/icon_vault_coldcard.svg'; +import SendIcon from 'src/assets/images/icon_sent_footer.svg'; +import RecieveIcon from 'src/assets/images/icon_received_footer.svg'; +import SettingIcon from 'src/assets/images/settings_footer.svg'; import Success from 'src/assets/images/Success.svg'; import TransactionElement from 'src/components/TransactionElement'; -import { Vault, VaultSigner } from 'src/core/wallets/interfaces/vault'; -import VaultIcon from 'src/assets/images/icon_vault_new.svg'; +import { Signer, Vault } from 'src/core/wallets/interfaces/vault'; +import VaultIcon from 'src/assets/images/vault_icon.svg'; import CollaborativeIcon from 'src/assets/images/icon_collaborative.svg'; -import { EntityKind, SignerType } from 'src/core/wallets/enums'; +import { SignerType, VaultType } from 'src/core/wallets/enums'; import VaultSetupIcon from 'src/assets/images/vault_setup.svg'; -import moment from 'moment'; import { refreshWallets } from 'src/store/sagaActions/wallets'; import { setIntroModal } from 'src/store/reducers/vaults'; import { useAppSelector } from 'src/store/hooks'; import { useDispatch } from 'react-redux'; -import { getSignerNameFromType, isSignerAMF, UNVERIFYING_SIGNERS } from 'src/hardware'; import { SubscriptionTier } from 'src/models/enums/SubscriptionTier'; -import NoVaultTransactionIcon from 'src/assets/images/emptystate.svg'; -import AddPhoneEmailIcon from 'src/assets/images/AddPhoneEmail.svg'; -import RightArrowIcon from 'src/assets/images/icon_arrow.svg'; +import AddPhoneEmailIcon from 'src/assets/images/phoneemail.svg'; import EmptyStateView from 'src/components/EmptyView/EmptyStateView'; import useVault from 'src/hooks/useVault'; -import Buttons from 'src/components/Buttons'; -import { fetchRampReservation } from 'src/services/ramp'; -import WalletOperations from 'src/core/wallets/operations'; import openLink from 'src/utils/OpenLink'; -import { SDIcons } from './SigningDeviceIcons'; -import CurrencyInfo from '../HomeScreen/components/CurrencyInfo'; import NoTransactionIcon from 'src/assets/images/noTransaction.svg'; -import IdentifySignerModal from './components/IdentifySignerModal'; import KeeperFooter from 'src/components/KeeperFooter'; import { KEEPER_KNOWLEDGEBASE } from 'src/core/config'; import KeeperHeader from 'src/components/KeeperHeader'; import { LocalizationContext } from 'src/context/Localization/LocContext'; +import useSigners from 'src/hooks/useSigners'; +import CardPill from 'src/components/CardPill'; +import ActionCard from 'src/components/ActionCard'; +import CurrencyInfo from '../Home/components/CurrencyInfo'; +import HexagonIcon from 'src/components/HexagonIcon'; +import Colors from 'src/theme/Colors'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { AppStackParams } from 'src/navigation/types'; function Footer({ vault, - onPressBuy, isCollaborativeWallet, identifySigner, setIdentifySignerModal, }: { vault: Vault; - onPressBuy: Function; isCollaborativeWallet: boolean; - identifySigner: VaultSigner; + identifySigner: Signer; setIdentifySignerModal: any; }) { const navigation = useNavigation(); const footerItems = [ { - Icon: Send, + Icon: SendIcon, text: 'Send', onPress: () => { if (identifySigner) { @@ -72,25 +66,20 @@ function Footer({ }, }, { - Icon: Recieve, + Icon: RecieveIcon, text: 'Receive', onPress: () => { navigation.dispatch(CommonActions.navigate('Receive', { wallet: vault })); }, }, { - Icon: Buy, - text: 'Buy', - onPress: onPressBuy, - }, - { - Icon: IconSettings, + Icon: SettingIcon, text: 'Settings', onPress: () => { navigation.dispatch( CommonActions.navigate( isCollaborativeWallet ? 'CollaborativeWalletSettings' : 'VaultSettings', - { wallet: isCollaborativeWallet ? vault : vault } + { vaultId: vault.id } ) ); }, @@ -99,63 +88,33 @@ function Footer({ return ; } -function VaultInfo({ - vault, - isCollaborativeWallet, -}: { - vault: Vault; - isCollaborativeWallet: boolean; -}) { - const { translations } = useContext(LocalizationContext); - const { common } = translations; +function VaultInfo({ vault }: { vault: Vault }) { const { colorMode } = useColorMode(); const { - presentationData: { name, description } = { name: '', description: '' }, - specs: { balances: { confirmed, unconfirmed } } = { + specs: { balances: { confirmed } } = { balances: { confirmed: 0, unconfirmed: 0 }, }, } = vault; return ( - - - {isCollaborativeWallet ? : } - - - {name} - - - {description} - - - - - - - {common.unconfirmed} - - - - - - {common.availableBalance} - - - + + + + - + + ); } @@ -164,7 +123,7 @@ function TransactionList({ pullDownRefresh, pullRefresh, vault, - collaborativeWalletId, + isCollaborativeWallet, }) { const { translations } = useContext(LocalizationContext); const { common } = translations; @@ -185,238 +144,49 @@ function TransactionList({ ); return ( <> - - - - {common.transactions} - - {transactions ? ( - { - navigation.dispatch( - CommonActions.navigate('AllTransactions', { - title: collaborativeWalletId ? 'Wallet Transactions' : 'Vault Transactions', - subtitle: 'All incoming and outgoing transactions', - collaborativeWalletId, - entityKind: EntityKind.VAULT, - }) - ); - }} - > - - - {common.viewAll} - - - - - ) : null} - + + + {common.transactions} + } data={transactions} renderItem={renderTransactionElement} keyExtractor={(item) => item.txid} showsVerticalScrollIndicator={false} ListEmptyComponent={ - collaborativeWalletId ? ( - - ) : ( - - ) + } /> ); } -function SignerList({ vault }: { vault: Vault }) { - const { colorMode } = useColorMode(); - const { signers: Signers, isMultiSig } = vault; - const navigation = useNavigation(); - - return ( - - {Signers.map((signer) => { - const indicate = - !signer.registered && isMultiSig && !UNVERIFYING_SIGNERS.includes(signer.type); - - return ( - - { - navigation.dispatch( - CommonActions.navigate('SigningDeviceDetails', { - SignerIcon: , - signerId: signer.signerId, - vaultId: vault.id, - }) - ); - }} - > - {indicate ? : null} - - {SDIcons(signer.type, true).Icon} - - - {indicate ? 'Not registered' : ' '} - - - - {getSignerNameFromType(signer.type, signer.isMock, isSignerAMF(signer))} - - - {signer.signerDescription - ? signer.signerDescription - : `Added ${moment(signer.addedOn).fromNow().toLowerCase()}`} - - - - - ); - })} - - ); -} +type ScreenProps = NativeStackScreenProps; -function RampBuyContent({ - buyWithRamp, - vault, - setShowBuyRampModal, -}: { - buyWithRamp: boolean; - vault: Vault; - setShowBuyRampModal: any; -}) { - const { translations } = useContext(LocalizationContext); - const { ramp } = translations; - const [buyAddress, setBuyAddress] = useState(''); - - useEffect(() => { - const receivingAddress = WalletOperations.getNextFreeAddress(vault); - setBuyAddress(receivingAddress); - }, []); - - return ( - - {ramp.byProceedRampParagraph} - - - - - {ramp.bitcoinTransfer} - - - {vault.presentationData.name} - - {`Balance: ${vault.specs.balances.confirmed} sats`} - - - - - - @ - - - - {ramp.addressForRamp} - - - {buyAddress} - - - - { - setShowBuyRampModal(false); - }} - primaryText="Buy Bitcoin" - primaryCallback={() => buyWithRamp(buyAddress)} - /> - - ); -} - -function VaultDetails({ navigation }) { +const VaultDetails = ({ navigation, route }: ScreenProps) => { const { colorMode } = useColorMode(); const { translations } = useContext(LocalizationContext); - const { vault: vaultTranslation, ramp, common } = translations; - const route = useRoute() as { - params: { - vaultTransferSuccessful: boolean; - autoRefresh: boolean; - collaborativeWalletId: string; - }; - }; + const { vault: vaultTranslation, common } = translations; - const { - vaultTransferSuccessful = false, - autoRefresh = false, - collaborativeWalletId = '', - } = route.params || {}; + const { vaultTransferSuccessful = false, autoRefresh = false, vaultId = '' } = route.params || {}; const dispatch = useDispatch(); const introModal = useAppSelector((state) => state.vault.introModal); - const { activeVault: vault } = useVault(collaborativeWalletId); + const { activeVault: vault } = useVault({ vaultId }); const [pullRefresh, setPullRefresh] = useState(false); - const [identifySignerModal, setIdentifySignerModal] = useState(false); + const [identifySignerModal, setIdentifySignerModal] = useState(true); const [vaultCreated, setVaultCreated] = useState(introModal ? false : vaultTransferSuccessful); - const inheritanceSigner = vault.signers.filter( - (signer) => signer.type === SignerType.INHERITANCEKEY - )[0]; - const [showBuyRampModal, setShowBuyRampModal] = useState(false); + const { vaultSigners: keys } = useSigners(vault.id); + const inheritanceSigner = keys.filter((signer) => signer?.type === SignerType.INHERITANCEKEY)[0]; const transactions = vault?.specs?.transactions || []; + const isCollaborativeWallet = vault.type === VaultType.COLLABORATIVE; useEffect(() => { if (autoRefresh) syncVault(); @@ -430,40 +200,50 @@ function VaultDetails({ navigation }) { const VaultContent = useCallback( () => ( - - + + - - {collaborativeWalletId + + {isCollaborativeWallet ? vaultTranslation.walletSetupDetails : vaultTranslation.keeperSupportSigningDevice} - {!collaborativeWalletId ? ( - + {!isCollaborativeWallet ? ( + {vaultTranslation.additionalOptionForSignDevice} ) : null} ), - [collaborativeWalletId] + [isCollaborativeWallet] ); const NewVaultContent = useCallback( () => ( - - {vaultTranslation.sendVaultSignDevices} + + Your 3-of-6 vault has been setup successfully. You can start receiving/transfering bitcoin + + + For sending bitcoin out of the vault you will need the signers{' '} - + + This means no one can steal your sats from the vault unless they also have access to your + signers{' '} + + + {' '} {inheritanceSigner && ( { - navigation.navigate('IKSAddEmailPhone'); + navigation.navigate('IKSAddEmailPhone', { vaultId }); setVaultCreated(false); }} > @@ -471,16 +251,13 @@ function VaultDetails({ navigation }) { - - {vaultTranslation.addEmail} + + {vaultTranslation.addEmailPhone} - - {vaultTranslation.addEmailDetails} + + {vaultTranslation.addEmailVaultDetail} - - - )} @@ -488,59 +265,85 @@ function VaultDetails({ navigation }) { [] ); - const buyWithRamp = (address: string) => { - try { - setShowBuyRampModal(false); - Linking.openURL(fetchRampReservation({ receiveAddress: address })); - } catch (error) { - console.log(error); - } - }; - const subtitle = `Vault with a ${vault.scheme.m} of ${vault.scheme.n} setup is created`; - const identifySigner = vault.signers.find((signer) => signer.type === SignerType.OTHER_SD); + const identifySigner = keys.find((signer) => signer.type === SignerType.OTHER_SD); return ( - + + ) : ( + } + /> + ) + } + subtitle={vault.presentationData?.description} learnMore learnTextColor="light.white" learnBackgroundColor="rgba(0,0,0,.2)" learnMorePressed={() => dispatch(setIntroModal(true))} contrastScreen={true} /> - + - {collaborativeWalletId ? null : } - 800 ? 5 : 0} - > + + + navigation.navigate('UTXOManagement', { + data: vault, + routeName: 'Vault', + vaultId, + }) + } + icon={} + /> + + navigation.dispatch( + CommonActions.navigate({ + name: 'ManageSigners', + params: { vaultId, vaultKeys: vault.signers }, + }) + ) + } + icon={} + /> + +
setShowBuyRampModal(true)} vault={vault} - isCollaborativeWallet={!!collaborativeWalletId} + isCollaborativeWallet={isCollaborativeWallet} identifySigner={identifySigner} setIdentifySignerModal={setIdentifySignerModal} /> @@ -549,7 +352,7 @@ function VaultDetails({ navigation }) { visible={vaultCreated} title={vaultTranslation.newVaultCreated} subTitle={subtitle} - buttonText={vaultTranslation.ViewVault} + buttonText={'Confirm'} DarkCloseIcon={colorMode === 'dark'} modalBackground={`${colorMode}.modalWhiteBackground`} textColor={`${colorMode}.primaryText`} @@ -557,6 +360,8 @@ function VaultDetails({ navigation }) { buttonCallback={() => { setVaultCreated(false); }} + secondaryButtonText={'Cancel'} + secondaryCallback={() => setVaultCreated(false)} close={() => setVaultCreated(false)} Content={NewVaultContent} /> @@ -566,14 +371,14 @@ function VaultDetails({ navigation }) { dispatch(setIntroModal(false)); }} title={ - collaborativeWalletId + isCollaborativeWallet ? vaultTranslation.collaborativeWallet : vaultTranslation.keeperVault } subTitle={ - collaborativeWalletId + isCollaborativeWallet ? vaultTranslation.collaborativeWalletMultipleUsers - : `Depending on your tier - ${SubscriptionTier.L1}, ${SubscriptionTier.L2} or ${SubscriptionTier.L3}, you need to add signing devices to the Vault` + : `Depending on your tier - ${SubscriptionTier.L1}, ${SubscriptionTier.L2} or ${SubscriptionTier.L3}, you need to add signers to the vault` } modalBackground={`${colorMode}.modalGreenBackground`} textColor={`${colorMode}.modalGreenContent`} @@ -588,40 +393,15 @@ function VaultDetails({ navigation }) { learnMore learnMoreCallback={() => openLink( - collaborativeWalletId + isCollaborativeWallet ? `${KEEPER_KNOWLEDGEBASE}knowledge-base/what-is-wallet/` : `${KEEPER_KNOWLEDGEBASE}knowledge-base/what-is-vault/` ) } /> - { - setShowBuyRampModal(false); - }} - title={ramp.buyBitcoinRamp} - subTitle={ramp.buyBitcoinRampSubTitle} - subTitleColor="#5F6965" - textColor="light.primaryText" - Content={() => ( - - )} - /> - setIdentifySignerModal(false)} - signer={identifySigner} - secondaryCallback={() => { - navigation.dispatch(CommonActions.navigate('Send', { sender: vault })); - }} - />
); -} +}; const styles = StyleSheet.create({ container: { @@ -629,6 +409,36 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', flex: 1, }, + vaultInfoContainer: { + paddingLeft: '10%', + marginVertical: 20, + justifyContent: 'space-between', + }, + pillsContainer: { + gap: 2, + }, + actionCardContainer: { + marginTop: 20, + marginBottom: -50, + zIndex: 10, + gap: 4, + alignItems: 'center', + justifyContent: 'center', + }, + topSection: { + paddingHorizontal: 20, + paddingTop: 15, + }, + bottomSection: { + paddingHorizontal: wp(30), + flex: 1, + justifyContent: 'space-between', + paddingBottom: 20, + }, + transactionHeading: { + fontSize: 16, + letterSpacing: 1.28, + }, IconText: { justifyContent: 'center', alignItems: 'center', @@ -723,6 +533,8 @@ const styles = StyleSheet.create({ marginVertical: hp(20), paddingVertical: hp(10), borderRadius: 10, + borderWidth: 1, + borderStyle: 'dashed', }, iconWrapper: { width: '15%', @@ -732,12 +544,37 @@ const styles = StyleSheet.create({ }, addPhoneEmailTitle: { fontSize: 14, + fontWeight: '800', }, addPhoneEmailSubTitle: { fontSize: 12, }, rightIconWrapper: { width: '10%', + marginLeft: 5, + }, + vaultModalContainer: { + marginVertical: 5, + gap: 4, + }, + alignSelf: { + alignSelf: 'center', + }, + modalContent: { + marginTop: hp(20), + fontSize: 13, + letterSpacing: 0.65, + padding: 1, + }, + descText: { + fontSize: 13, + letterSpacing: 0.65, + }, + mt3: { + marginTop: 3, + }, + alignItems: { + alignItems: 'center', }, }); export default VaultDetails; diff --git a/src/screens/Vault/VaultMigrationController.tsx b/src/screens/Vault/VaultMigrationController.tsx index cdae68ba8..80b84eef3 100644 --- a/src/screens/Vault/VaultMigrationController.tsx +++ b/src/screens/Vault/VaultMigrationController.tsx @@ -4,7 +4,6 @@ import { TxPriority, VaultType } from 'src/core/wallets/enums'; import { VaultScheme, VaultSigner } from 'src/core/wallets/interfaces/vault'; import { addNewVault, finaliseVaultMigration, migrateVault } from 'src/store/sagaActions/vaults'; import { useAppSelector } from 'src/store/hooks'; -import { clearSigningDevice } from 'src/store/reducers/vaults'; import { TransferType } from 'src/models/enums/TransferType'; import ToastErrorIcon from 'src/assets/images/toast_error.svg'; import { NewVaultInfo } from 'src/store/sagas/wallets'; @@ -12,26 +11,27 @@ import { useDispatch } from 'react-redux'; import { captureError } from 'src/services/sentry'; import useVault from 'src/hooks/useVault'; import WalletOperations from 'src/core/wallets/operations'; -import { UNVERIFYING_SIGNERS } from 'src/hardware'; import { resetRealyVaultState } from 'src/store/reducers/bhr'; import useToastMessage from 'src/hooks/useToastMessage'; import { AverageTxFeesByNetwork } from 'src/core/wallets/interfaces'; import WalletUtilities from 'src/core/wallets/operations/utils'; import { sendPhasesReset } from 'src/store/reducers/send_and_receive'; import { sendPhaseOne } from 'src/store/sagaActions/send_and_receive'; +import { generateVaultId } from 'src/core/wallets/factories/VaultFactory'; function VaultMigrationController({ vaultCreating, - signersState, + vaultKeys, scheme, setCreating, name, description, -}: any) { + vaultId, +}) { const navigation = useNavigation(); const dispatch = useDispatch(); const { showToast } = useToastMessage(); - const { activeVault } = useVault(); + const { activeVault, allVaults } = useVault({ vaultId }); const temporaryVault = useAppSelector((state) => state.vault.intrimVault); const averageTxFees: AverageTxFeesByNetwork = useAppSelector( (state) => state.network.averageTxFees @@ -46,6 +46,13 @@ function VaultMigrationController({ ); const [recipients, setRecepients] = useState(); + const [generatedVaultId, setGeneratedVaultId] = useState(''); + + useEffect(() => { + if (temporaryVault && temporaryVault.id) { + setGeneratedVaultId(temporaryVault.id); + } + }, [temporaryVault]); useEffect(() => { if (vaultCreating) { @@ -54,17 +61,24 @@ function VaultMigrationController({ }, [vaultCreating]); useEffect(() => { - if (relayVaultUpdate && activeVault) { + const newVault = allVaults.filter((v) => v.id === generatedVaultId)[0]; + if (relayVaultUpdate && newVault) { const navigationState = { index: 1, routes: [ { name: 'Home' }, - { name: 'VaultDetails', params: { vaultTransferSuccessful: true } }, + { + name: 'VaultDetails', + params: { vaultId: generatedVaultId, vaultTransferSuccessful: true }, + }, ], }; navigation.dispatch(CommonActions.reset(navigationState)); dispatch(resetRealyVaultState()); - dispatch(clearSigningDevice()); + setCreating(false); + } else if (relayVaultUpdate) { + navigation.dispatch(CommonActions.reset({ index: 1, routes: [{ name: 'Home' }] })); + dispatch(resetRealyVaultState()); setCreating(false); } @@ -148,6 +162,8 @@ function VaultMigrationController({ description, }, }; + const generatedVaultId = generateVaultId(signers, scheme); + setGeneratedVaultId(generatedVaultId); dispatch(addNewVault({ newVaultInfo: vaultInfo })); return vaultInfo; } catch (err) { @@ -156,19 +172,6 @@ function VaultMigrationController({ } }, []); - const sanitizeSigners = () => - signersState.map((signer: VaultSigner) => { - if ( - !signer.isMock && - scheme.n !== 1 && - !UNVERIFYING_SIGNERS.includes(signer.type) && - signer.registered - ) { - return { ...signer, registered: false }; - } - return signer; - }); - const initiateNewVault = () => { if (activeVault) { if (unconfirmed) { @@ -184,26 +187,26 @@ function VaultMigrationController({ { name: 'Home' }, { name: 'VaultDetails', - params: { autoRefresh: true }, + params: { autoRefresh: true, vaultId: activeVault.id }, }, ], }) ); return; } - const freshSignersState = sanitizeSigners(); + const vaultInfo: NewVaultInfo = { vaultType: VaultType.DEFAULT, vaultScheme: scheme, - vaultSigners: freshSignersState, + vaultSigners: vaultKeys, vaultDetails: { - name: 'Vault', - description: 'Secure your sats', + name, + description, }, }; dispatch(migrateVault(vaultInfo, activeVault.shellId)); } else { - createVault(signersState, scheme); + createVault(vaultKeys, scheme); } }; return null; diff --git a/src/screens/Vault/VaultSettings.tsx b/src/screens/Vault/VaultSettings.tsx index ff01f1cdb..293325213 100644 --- a/src/screens/Vault/VaultSettings.tsx +++ b/src/screens/Vault/VaultSettings.tsx @@ -1,82 +1,47 @@ import React from 'react'; import { StyleSheet } from 'react-native'; -import { Box, ScrollView, Text, useColorMode } from 'native-base'; +import { Box, ScrollView, useColorMode } from 'native-base'; import { CommonActions, useNavigation } from '@react-navigation/native'; import KeeperHeader from 'src/components/KeeperHeader'; -import { wp, hp, windowWidth } from 'src/constants/responsive'; -import useBalance from 'src/hooks/useBalance'; -import Note from 'src/components/Note/Note'; +import { hp } from 'src/constants/responsive'; import { genrateOutputDescriptors } from 'src/core/utils'; import Colors from 'src/theme/Colors'; import useVault from 'src/hooks/useVault'; import ScreenWrapper from 'src/components/ScreenWrapper'; import OptionCard from 'src/components/OptionCard'; +import VaultIcon from 'src/assets/images/vault_icon.svg'; +import HexagonIcon from 'src/components/HexagonIcon'; +import WalletFingerprint from 'src/components/WalletFingerPrint'; -function VaultCard({ vaultName, vaultBalance, vaultDescription, getSatUnit }) { - const { colorMode } = useColorMode(); - return ( - - - - - {vaultName} - - - {vaultDescription} - - - - {vaultBalance} - {getSatUnit()} - - - - ); -} - -function VaultSettings() { +function VaultSettings({ route }) { const { colorMode } = useColorMode(); const navigation = useNavigation(); - const { getSatUnit, getBalance } = useBalance(); - const { activeVault: vault } = useVault(); + const { vaultId } = route.params; + const { activeVault: vault } = useVault({ vaultId }); const descriptorString = genrateOutputDescriptors(vault); - const { - presentationData: { name, description } = { name: '', description: '' }, - specs: { balances: { confirmed, unconfirmed } } = { - balances: { confirmed: 0, unconfirmed: 0 }, - }, - } = vault; - return ( - - - - + } + /> + } + /> + { + navigation.dispatch(CommonActions.navigate('EditWalletDetails', { wallet: vault })); + }} + /> { navigation.dispatch(CommonActions.navigate('ArchivedVault')); @@ -97,67 +62,26 @@ function VaultSettings() { title="Update scheme" description="Update your vault configuration and transfer funds" callback={() => { - navigation.dispatch(CommonActions.navigate('VaultSetup')); + navigation.dispatch( + CommonActions.navigate({ name: 'VaultSetup', params: { vaultId } }) + ); }} /> - - + + ); } const styles = StyleSheet.create({ - Container: { - flex: 1, - padding: 20, - position: 'relative', - alignItems: 'center', - }, - moadalContainer: { - width: wp(280), - }, - inputWrapper: { - borderRadius: 10, - flexDirection: 'row', - height: 150, - width: windowWidth * 0.8, - alignItems: 'center', - justifyContent: 'center', - padding: 15, - }, - IconText: { - justifyContent: 'center', - alignItems: 'center', - }, - buttonContainer: { - borderColor: Colors.Seashell, - marginTop: 10, - paddingTop: 20, - borderTopWidth: 0.5, - alignItems: 'center', - }, - shareText: { - fontSize: 12, - letterSpacing: 0.84, - marginVertical: 2.5, - paddingLeft: 3, - }, - vaultCardWrapper: { - marginTop: hp(30), - }, optionViewWrapper: { + marginTop: hp(30), alignItems: 'center', }, - bottomNoteWrapper: { - marginHorizontal: '5%', - }, - modalNoteWrapper: { - width: '90%', + fingerprint: { + alignItems: 'center', }, }); export default VaultSettings; diff --git a/src/screens/Vault/VaultSetup.tsx b/src/screens/Vault/VaultSetup.tsx index 02c992fdd..2662b9170 100644 --- a/src/screens/Vault/VaultSetup.tsx +++ b/src/screens/Vault/VaultSetup.tsx @@ -1,9 +1,8 @@ import { StyleSheet, TouchableOpacity } from 'react-native'; -import React, { useState } from 'react'; -import { CommonActions, useNavigation, useRoute } from '@react-navigation/native'; -import { Box, HStack, VStack, useColorMode } from 'native-base'; +import React, { useContext, useState } from 'react'; +import { CommonActions, useNavigation } from '@react-navigation/native'; +import { Box, HStack, ScrollView, VStack, useColorMode } from 'native-base'; import { useDispatch } from 'react-redux'; - import ScreenWrapper from 'src/components/ScreenWrapper'; import KeeperHeader from 'src/components/KeeperHeader'; import KeeperTextInput from 'src/components/KeeperTextInput'; @@ -13,8 +12,14 @@ import Buttons from 'src/components/Buttons'; import { setVaultRecoveryDetails } from 'src/store/reducers/bhr'; import useToastMessage from 'src/hooks/useToastMessage'; import ToastErrorIcon from 'src/assets/images/toast_error.svg'; +import useVault from 'src/hooks/useVault'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import config, { APP_STAGE } from 'src/core/config'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { AppStackParams } from 'src/navigation/types'; +import Note from 'src/components/Note/Note'; -const NumberInput = ({ value, onDecrease, onIncrease }) => { +function NumberInput({ value, onDecrease, onIncrease }) { const { colorMode } = useColorMode(); return ( @@ -36,18 +41,28 @@ const NumberInput = ({ value, onDecrease, onIncrease }) => { ); -}; +} -const VaultSetup = () => { +type ScreenProps = NativeStackScreenProps; +const VaultSetup = ({ route }: ScreenProps) => { const { colorMode } = useColorMode(); const navigation = useNavigation(); const { showToast } = useToastMessage(); - const { params } = useRoute(); - const { isRecreation } = (params as { isRecreation: Boolean }) || {}; + const { isRecreation, scheme: preDefinedScheme, vaultId } = route.params || {}; const dispatch = useDispatch(); - const [vaultName, setVaultName] = useState(''); - const [vaultDescription, setVaultDescription] = useState(''); - const [scheme, setScheme] = useState({ m: 2, n: 3 }); + const { activeVault } = useVault({ vaultId }); + const [vaultName, setVaultName] = useState( + activeVault?.presentationData?.name || config.ENVIRONMENT === APP_STAGE.DEVELOPMENT + ? 'Vault' + : '' + ); + const [vaultDescription, setVaultDescription] = useState( + activeVault?.presentationData?.description || '' + ); + const [scheme, setScheme] = useState(activeVault?.scheme || preDefinedScheme || { m: 3, n: 4 }); + const { translations } = useContext(LocalizationContext); + const { vault } = translations; + const onDecreaseM = () => { if (scheme.m > 1) { setScheme({ ...scheme, m: scheme.m - 1 }); @@ -69,7 +84,7 @@ const VaultSetup = () => { } }; const OnProceed = () => { - if (vaultName !== '' && vaultDescription !== '') { + if (vaultName !== '') { if (isRecreation) { dispatch( setVaultRecoveryDetails({ @@ -83,47 +98,88 @@ const VaultSetup = () => { navigation.dispatch( CommonActions.navigate({ name: 'AddSigningDevice', - params: { scheme, name: vaultName, description: vaultDescription }, + params: { + scheme, + name: vaultName, + description: vaultDescription, + vaultId, + }, }) ); } } else { - showToast('Please Enter Vault name and description', ) + showToast('Please Enter vault name', ); } - } + }; + //TODO: add learn more modal return ( - - - - - - - Total Keys for Vault Configuration - Select the total number of keys - - Required Keys - Select the number of keys required - - - + + + { + if (vaultName === 'Vault') { + setVaultName(''); + } else { + setVaultName(value); + } + }} + testID="vault_name" + maxLength={20} + /> + + + + + Total Keys for vault configuration + + + Select the total number of keys + + + + Required Keys + + + Minimum number of keys to broadcast a transaction + + + + + {!preDefinedScheme && ( + + + + )} + ); }; @@ -151,4 +207,7 @@ const styles = StyleSheet.create({ width: windowWidth * 0.4, marginVertical: 20, }, + mt20: { + margin: 20, + }, }); diff --git a/src/screens/Vault/components/EditDescriptionModal.tsx b/src/screens/Vault/components/EditDescriptionModal.tsx index 2a4e003ef..ddfa3f183 100644 --- a/src/screens/Vault/components/EditDescriptionModal.tsx +++ b/src/screens/Vault/components/EditDescriptionModal.tsx @@ -1,10 +1,9 @@ - import { StyleSheet, TextInput } from 'react-native'; import { Box, HStack, useColorMode, VStack } from 'native-base'; import React, { useCallback, useRef, useState } from 'react'; import moment from 'moment'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Signer } from 'src/core/wallets/interfaces/vault'; import { windowWidth } from 'src/constants/responsive'; import Text from 'src/components/KeeperText'; import KeeperModal from 'src/components/KeeperModal'; @@ -12,7 +11,7 @@ import Colors from 'src/theme/Colors'; import Fonts from 'src/constants/Fonts'; import { SDIcons } from '../SigningDeviceIcons'; -function SignerData({ signer }: { signer: VaultSigner }) { +function SignerData({ signer }: { signer: Signer }) { const { colorMode } = useColorMode(); return ( @@ -29,7 +28,7 @@ function SignerData({ signer }: { signer: VaultSigner }) { ); } -function Content({ signer, descRef }: { signer: VaultSigner; descRef }) { +function Content({ signer, descRef }: { signer: Signer; descRef }) { const { colorMode } = useColorMode(); const updateDescription = useCallback((text) => { descRef.current = text; @@ -69,7 +68,7 @@ function DescriptionModal({ }: { visible: boolean; close: () => void; - signer: VaultSigner; + signer: Signer; callback: any; }) { const { colorMode } = useColorMode(); @@ -91,7 +90,7 @@ function DescriptionModal({ DarkCloseIcon={colorMode === 'dark'} close={close} title="Add Description" - subTitle="Optionally you can add a short description to the signing device" + subTitle="Optionally you can add a short description to the signer" buttonText="Save" justifyContent="center" Content={MemoisedContent} diff --git a/src/screens/Vault/components/IdentifySignerModal.tsx b/src/screens/Vault/components/IdentifySignerModal.tsx index d8029cc37..97dd42cf6 100644 --- a/src/screens/Vault/components/IdentifySignerModal.tsx +++ b/src/screens/Vault/components/IdentifySignerModal.tsx @@ -4,10 +4,13 @@ import { Box, useColorMode } from 'native-base'; import { useNavigation, CommonActions } from '@react-navigation/native'; import WarningIllustration from 'src/assets/images/warning.svg'; import Text from 'src/components/KeeperText'; +import useVault from 'src/hooks/useVault'; -const IdentifySignerModal = ({ visible, close, signer, secondaryCallback }) => { +const IdentifySignerModal = ({ visible, close, signer, secondaryCallback, vaultId }) => { const { colorMode } = useColorMode(); const navigation = useNavigation(); + + const { activeVault } = useVault({ vaultId }); const Content = useCallback(() => { return ( @@ -29,8 +32,7 @@ const IdentifySignerModal = ({ visible, close, signer, secondaryCallback }) => { CommonActions.navigate({ name: 'AssignSignerType', params: { - parentNavigation: navigation, - signer, + vault: activeVault, }, }) ); diff --git a/src/screens/Vault/components/RampModal.tsx b/src/screens/Vault/components/RampModal.tsx index 8469e80ad..ed6fea6b5 100644 --- a/src/screens/Vault/components/RampModal.tsx +++ b/src/screens/Vault/components/RampModal.tsx @@ -129,7 +129,7 @@ const styles = StyleSheet.create({ color: '#00836A', }, addressWrapper: { - marginVertical: 4, + marginVertical: 10, alignItems: 'center', borderRadius: 10, paddingHorizontal: 4, diff --git a/src/screens/Vault/components/SignerList.tsx b/src/screens/Vault/components/SignerList.tsx deleted file mode 100644 index c9f9484ac..000000000 --- a/src/screens/Vault/components/SignerList.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { Platform, ScrollView, StyleSheet, TouchableOpacity } from 'react-native'; -import React, { useCallback } from 'react'; -import { Box, useColorMode, VStack } from 'native-base'; -import moment from 'moment'; -import { getSignerNameFromType, isSignerAMF, UNVERIFYING_SIGNERS } from 'src/hardware'; -import { CommonActions, useNavigation } from '@react-navigation/native'; -import { VaultMigrationType } from 'src/core/wallets/enums'; -import Text from 'src/components/KeeperText'; -import { Vault } from 'src/core/wallets/interfaces/vault'; -import AddIcon from 'src/assets/images/icon_add_plus.svg'; -import SignerIcon from 'src/assets/images/icon_vault_coldcard.svg'; -import { windowHeight } from 'src/constants/responsive'; -import { WalletMap } from '../WalletMap'; - -function SignerList({ vault, upgradeStatus }: { vault: Vault; upgradeStatus: VaultMigrationType }) { - const { colorMode } = useColorMode(); - const { signers: Signers, isMultiSig } = vault; - const navigation = useNavigation(); - - const AddSigner = useCallback(() => { - if (upgradeStatus === VaultMigrationType.UPGRADE) { - return ( - - { - navigation.dispatch( - CommonActions.navigate({ name: 'AddSigningDevice', merge: true, params: {} }) - ); - }} - > - - - - - - Add signing device to upgrade - - - - - ); - } - return null; - }, [upgradeStatus]); - return ( - - {Signers.map((signer) => { - const indicate = - !signer.registered && isMultiSig && !UNVERIFYING_SIGNERS.includes(signer.type); - return ( - - { - navigation.dispatch( - CommonActions.navigate('SigningDeviceDetails', { - SignerIcon: , - signerId: signer.signerId, - vaultId: vault.id, - }) - ); - }} - > - {indicate ? : null} - {WalletMap(signer.type, true).Icon} - - {indicate ? 'Not registered' : ' '} - - - - {getSignerNameFromType(signer.type, signer.isMock, isSignerAMF(signer))} - - - {signer.signerDescription - ? signer.signerDescription - : `Added ${moment(signer.addedOn).fromNow().toLowerCase()}`} - - - - - ); - })} - - - ); -} - -export default SignerList; - -const styles = StyleSheet.create({ - signerCard: { - elevation: 4, - shadowRadius: 4, - shadowOpacity: 0.3, - shadowOffset: { height: 2, width: 0 }, - height: windowHeight > 670 ? 130 : 121, - width: 70, - borderTopLeftRadius: 100, - borderTopRightRadius: 100, - borderBottomLeftRadius: Platform.OS === 'ios' ? 10 : 30, - borderBottomRightRadius: Platform.OS === 'ios' ? 10 : 30, - alignItems: 'center', - justifyContent: 'space-between', - padding: windowHeight > 670 ? 5 : 2, - backgroundColor: '#FDF7F0', - }, - scrollContainer: { - paddingVertical: '3%', - paddingHorizontal: '2%', - width: Platform.select({ android: null, ios: '100%' }), - marginRight: '10%', - }, - unregistered: { - color: '#6E563B', - fontSize: 8, - letterSpacing: 0.6, - textAlign: 'center', - lineHeight: 16, - }, - indicator: { - height: 10, - width: 10, - borderRadius: 10, - position: 'absolute', - zIndex: 2, - right: '10%', - top: '5%', - borderWidth: 1, - borderColor: 'white', - backgroundColor: '#F86B50', - }, - signerNameFromTypeText: { - fontSize: 9, - letterSpacing: 0.6, - textAlign: 'center', - }, - signerDescDateText: { - fontSize: 7, - letterSpacing: windowHeight > 670 ? 0.6 : 0, - textAlign: 'center', - }, - signerNameFromTypeWrapper: { - paddingBottom: 2, - }, - signerTypeIconWrapper: { - margin: 2, - width: windowHeight > 670 ? 45 : 40, - height: windowHeight > 670 ? 45 : 40, - borderRadius: 45, - backgroundColor: '#725436', - justifyContent: 'center', - alignItems: 'center', - alignSelf: 'center', - }, -}); diff --git a/src/screens/Vault/components/VaultCreatedModal.tsx b/src/screens/Vault/components/VaultCreatedModal.tsx index d65ffbcc7..a7842efc1 100644 --- a/src/screens/Vault/components/VaultCreatedModal.tsx +++ b/src/screens/Vault/components/VaultCreatedModal.tsx @@ -15,14 +15,17 @@ function VaultCreatedModal({ close: () => void; }) { const { colorMode } = useColorMode(); - const subtitle = vault.scheme.n > 1 ? `Vault with a ${vault.scheme.m} of ${vault.scheme.n} setup will be created` : `Vault with ${vault.scheme.m} of ${vault.scheme.n} setup will be created`; + const subtitle = + vault.scheme.n > 1 + ? `Vault with a ${vault.scheme.m} of ${vault.scheme.n} setup will be created` + : `Vault with ${vault.scheme.m} of ${vault.scheme.n} setup will be created`; const NewVaultContent = useCallback( () => ( - For sending out of the vault you will need the signing devices. This means no one can - steal your bitcoin in the vault unless they also have the signing devices + For sending out of the vault you will need the signers. This means no one can steal your + bitcoin in the vault unless they also have the signers ), @@ -32,9 +35,9 @@ function VaultCreatedModal({ return ( state.vault.introModal); - const { activeVault: vault } = useVault(); - const [vaultCreated, setVaultCreated] = useState(vaultTransferSuccessful); - - const VaultContent = useCallback( - () => ( - - - - - - Keeper supports all the popular bitcoin signing devices (Hardware Wallets) that a user can - select - - - There are also some additional options if you do not have hardware signing devices - - - ), - [] - ); - const closeVaultCreatedDialog = () => { - setVaultCreated(false); - }; - - return ( - <> - - { - dispatch(setIntroModal(false)); - }} - title="Keeper Vault" - subTitle={`Depending on your tier - ${SubscriptionTier.L1}, ${SubscriptionTier.L2} or ${SubscriptionTier.L3}, you need to add signing devices to the vault`} - modalBackground={`${colorMode}.modalGreenBackground`} - textColor={`${colorMode}.modalGreenContent`} - Content={VaultContent} - buttonTextColor={colorMode === 'light' ? `${colorMode}.greenText2` : `${colorMode}.white`} - buttonBackground={`${colorMode}.modalWhiteButton`} - buttonText="Continue" - buttonCallback={() => { - dispatch(setIntroModal(false)); - }} - DarkCloseIcon - learnMore - learnMoreCallback={() => openLink('https://www.bitcoinkeeper.app/')} - /> - - - ); -} - -export default VaultModals; diff --git a/src/screens/VaultRecovery/SignersList.tsx b/src/screens/VaultRecovery/SignersList.tsx deleted file mode 100644 index e50612bd2..000000000 --- a/src/screens/VaultRecovery/SignersList.tsx +++ /dev/null @@ -1,961 +0,0 @@ -import { Box, ScrollView, View } from 'native-base'; -import { CommonActions, useNavigation } from '@react-navigation/native'; -import React, { useContext, useEffect, useState } from 'react'; -import { hp, windowHeight, windowWidth, wp } from 'src/constants/responsive'; -import Text from 'src/components/KeeperText'; -import SeedWordsIllustration from 'src/assets/images/illustration_seed_words.svg'; -import ColdCardSetupImage from 'src/assets/images/ColdCardSetup.svg'; -import KeeperHeader from 'src/components/KeeperHeader'; -import KeeperModal from 'src/components/KeeperModal'; -import ScreenWrapper from 'src/components/ScreenWrapper'; -import SeedSignerSetupImage from 'src/assets/images/seedsigner_setup.svg'; -import { SignerStorage, SignerType } from 'src/core/wallets/enums'; -import TapsignerSetupImage from 'src/assets/images/TapsignerSetup.svg'; -import { Alert, StyleSheet, TouchableOpacity } from 'react-native'; -import { captureError } from 'src/services/sentry'; -import config, { APP_STAGE } from 'src/core/config'; -import { getPassportDetails } from 'src/hardware/passport'; -import { getSeedSignerDetails } from 'src/hardware/seedsigner'; -import { setSigningDevices } from 'src/store/reducers/bhr'; -import { useDispatch } from 'react-redux'; -import KeystoneSetupImage from 'src/assets/images/keystone_illustration.svg'; -import JadeSVG from 'src/assets/images/illustration_jade.svg'; -import { getKeystoneDetails } from 'src/hardware/keystone'; -import { getJadeDetails } from 'src/hardware/jade'; -import useToastMessage from 'src/hooks/useToastMessage'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; -import { generateSignerFromMetaData, getSignerNameFromType } from 'src/hardware'; -import { crossInteractionHandler } from 'src/utils/utilities'; -import SigningServer from 'src/services/operations/SigningServer'; -import NFC from 'src/services/nfc'; -import { useAppSelector } from 'src/store/hooks'; -import Clipboard from '@react-native-community/clipboard'; -import CVVInputsView from 'src/components/HealthCheck/CVVInputsView'; -import CustomGreenButton from 'src/components/CustomButton/CustomGreenButton'; -import KeyPadView from 'src/components/AppNumPad/KeyPadView'; -import DeleteIcon from 'src/assets/images/deleteBlack.svg'; -import LedgerImage from 'src/assets/images/ledger_image.svg'; -import TickIcon from 'src/assets/images/icon_tick.svg'; -import BitoxImage from 'src/assets/images/bitboxSetup.svg'; -import TrezorSetup from 'src/assets/images/trezor_setup.svg'; -import InheritanceKeyServer from 'src/services/operations/InheritanceKey'; -import { generateKey } from 'src/services/operations/encryption'; -import { setInheritanceRequestId } from 'src/store/reducers/storage'; -import { close } from '@sentry/react-native'; -import ToastErrorIcon from 'src/assets/images/toast_error.svg'; -import { SDIcons } from '../Vault/SigningDeviceIcons'; -import { KeeperContent } from '../SignTransaction/SignerModals'; -import { formatDuration } from './VaultRecovery'; -import { LocalizationContext } from 'src/context/Localization/LocContext'; -import { generateCosignerMapIds } from 'src/core/wallets/factories/VaultFactory'; - -const getnavigationState = (type) => ({ - index: 5, - routes: [ - { name: 'NewKeeperApp' }, - { name: 'EnterSeedScreen', params: { isSoftKeyRecovery: false, type } }, - { name: 'OtherRecoveryMethods' }, - { name: 'VaultRecoveryAddSigner' }, - { name: 'SignersList' }, - { name: 'EnterSeedScreen', params: { isSoftKeyRecovery: true, type } }, - ], -}); - -export const getDeviceStatus = ( - type: SignerType, - isNfcSupported, - signingDevices, - inheritanceRequestId -) => { - switch (type) { - case SignerType.COLDCARD: - case SignerType.TAPSIGNER: - return { - message: !isNfcSupported ? 'NFC is not supported in your device' : '', - disabled: config.ENVIRONMENT !== APP_STAGE.DEVELOPMENT && !isNfcSupported, - }; - case SignerType.POLICY_SERVER: - if (signingDevices.length < 2) { - return { - message: 'Add two other devices first to recover', - disabled: true, - }; - } - return { - message: '', - disabled: false, - }; - case SignerType.INHERITANCEKEY: - if (signingDevices.length < 2 || inheritanceRequestId) { - return { - message: 'Add two other devices first to recover', - disabled: true, - }; - } - return { - message: '', - disabled: false, - }; - case SignerType.SEED_WORDS: - case SignerType.MOBILE_KEY: - case SignerType.KEEPER: - case SignerType.JADE: - case SignerType.PASSPORT: - case SignerType.SEEDSIGNER: - case SignerType.KEYSTONE: - case SignerType.LEDGER: - default: - return { - message: '', - disabled: false, - }; - } -}; - -function TapsignerSetupContent() { - return ( - - - - - {`\u2022 You will need the Pin/CVC at the back of TAPSIGNER`} - - - {'\u2022 Make sure that TAPSIGNER is not used as a Signer on other apps'} - - - - ); -} - -function LedgerSetupContent() { - return ( - - - - - {`\u2022 Please visit ${config.KEEPER_HWI} using Chrome browser on your desktop to use the Keeper Hardware Interface to connect with Ledger.`} - - - {`\u2022 The Keeper Harware Interface will exchange the device details from/to the Keeper app and the signing device.`} - - - - ); -} -function ColdCardSetupContent() { - return ( - - - - - - - {`Export the xPub by going to Advanced/Tools > Export wallet > Generic JSON. From here choose the account number and transfer over NFC. Make sure you remember the account you had chosen (This is important for recovering your Vault)`} - - - - ); -} - -function PassportSetupContent() { - return ( - - - - - - - {`\u2022 Export the xPub from the Account section > Manage Account > Connect Wallet > Keeper > Multisig > QR Code.\n`} - - - {`\u2022 Make sure you enable Testnet mode on the Passport if you are running the app in the Testnet mode from Settings > Bitcoin > Network > Testnet and enable it`} - - - - ); -} - -function SeedSignerSetupContent() { - return ( - - - - - - - {`\u2022 Make sure the seed is loaded and export the xPub by going to Seeds > Select your master fingerprint > Export Xpub > Multisig > Nested Segwit > Keeper.\n`} - - - {`\u2022 Make sure you enable Testnet mode on the SeedSigner if you are running the app in the Testnet mode from Settings > Advanced > Bitcoin network > Testnet and enable it`} - - - - ); -} - -function KeystoneSetupContent() { - return ( - - - - - - - {`\u2022 Make sure the BTC-only firmware is installed and export the xPub by going to the Side Menu > Multisig Wallet > Extended menu (three dots) from the top right corner > Show/Export XPUB > Nested SegWit.\n`} - - - {`\u2022 Make sure you enable Testnet mode on the Keystone if you are running the app in the Testnet mode from Side Menu > Settings > Blockchain > Testnet and confirm`} - - - - ); -} - -function JadeSetupContent() { - return ( - - - - - - - {`\u2022 Make sure the Jade is setup with a companion app and Unlocked. Then export the xPub by going to Settings > Xpub Export. Also to be sure that the wallet type and script type is set to Multisig and Native Segwit in the options section.\n`} - - - - ); -} - -function BitBox02Content() { - return ( - - - - - {`\u2022 Please visit ${config.KEEPER_HWI} using Chrome browser on your desktop to use the Keeper Hardware Interface to connect with BitBox02.`} - - - {`\u2022 The Keeper Harware Interface will exchange the device details from/to the Keeper app and the signing device.`} - - - - ); -} - -function TrezorContent() { - return ( - - - - - {`\u2022 Please visit ${config.KEEPER_HWI} using Chrome browser on your desktop to use the Keeper Hardware Interface to connect with Trezor.`} - - - {`\u2022 The Keeper Harware Interface will exchange the device details from/to the Keeper app and the signing device.`} - - - - ); -} - -function SignersList({ navigation }) { - type HWProps = { - disabled: boolean; - message: string; - type: SignerType; - first?: boolean; - last?: boolean; - }; - const { signingDevices, relayVaultReoveryShellId } = useAppSelector((state) => state.bhr); - const [isNfcSupported, setNfcSupport] = useState(true); - - const getNfcSupport = async () => { - const isSupported = await NFC.isNFCSupported(); - setNfcSupport(isSupported); - }; - - useEffect(() => { - getNfcSupport(); - }, []); - - const { navigate } = useNavigation(); - const { translations } = useContext(LocalizationContext); - const { vault: vaultTranslation } = translations; - - const dispatch = useDispatch(); - const { showToast } = useToastMessage(); - const verifyPassport = async (qrData, resetQR) => { - try { - const { xpub, derivationPath, xfp } = getPassportDetails(qrData); - const passport: VaultSigner = generateSignerFromMetaData({ - xpub, - derivationPath, - xfp, - signerType: SignerType.PASSPORT, - storageType: SignerStorage.COLD, - isMultisig: signingDevices.length > 1, - }); - dispatch(setSigningDevices(passport)); - navigation.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }); - } catch (err) { - resetQR(); - showToast('Invalid QR, please scan the QR from Passport!'); - navigation.dispatch(CommonActions.navigate('SignersList')); - captureError(err); - } - }; - - const otpContent = () => { - const [otp, setOtp] = useState(''); - const onPressNumber = (text) => { - let tmpPasscode = otp; - if (otp.length < 6) { - if (text !== 'x') { - tmpPasscode += text; - setOtp(tmpPasscode); - } - } - if (otp && text === 'x') { - setOtp(otp.slice(0, -1)); - } - }; - - const onDeletePressed = () => { - setOtp(otp.slice(0, otp.length - 1)); - }; - - return ( - - - { - const clipBoardData = await Clipboard.getString(); - if (clipBoardData.match(/^\d{6}$/)) { - setOtp(clipBoardData); - } else { - showToast('Invalid OTP'); - } - }} - > - - - - {vaultTranslation.cvvSigningServerInfo} - - - - { - verifySigningServer(otp); - }} - value="Confirm" - /> - - - - } - /> - - ); - }; - - const verifySeedSigner = async (qrData, resetQR) => { - try { - const { xpub, derivationPath, xfp } = getSeedSignerDetails(qrData); - const seedSigner: VaultSigner = generateSignerFromMetaData({ - xpub, - derivationPath, - xfp, - signerType: SignerType.SEEDSIGNER, - storageType: SignerStorage.COLD, - isMultisig: signingDevices.length > 1, - }); - dispatch(setSigningDevices(seedSigner)); - navigation.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }); - } catch (err) { - resetQR(); - showToast('Invalid QR, please scan the QR from SeedSigner!'); - navigation.dispatch(CommonActions.navigate('SignersList')); - captureError(err); - } - }; - - const verifyKeystone = async (qrData, resetQR) => { - try { - const { xpub, derivationPath, xfp } = getKeystoneDetails(qrData); - const keystone: VaultSigner = generateSignerFromMetaData({ - xpub, - derivationPath, - xfp, - signerType: SignerType.KEYSTONE, - storageType: SignerStorage.COLD, - isMultisig: signingDevices.length > 1, - }); - dispatch(setSigningDevices(keystone)); - navigation.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }); - } catch (err) { - resetQR(); - showToast('Invalid QR, please scan the QR from Keystone!'); - navigation.dispatch(CommonActions.navigate('SignersList')); - captureError(err); - } - }; - - const verifyJade = async (qrData, resetQR) => { - try { - const { xpub, derivationPath, xfp } = getJadeDetails(qrData); - const jade: VaultSigner = generateSignerFromMetaData({ - xpub, - derivationPath, - xfp, - signerType: SignerType.JADE, - storageType: SignerStorage.COLD, - isMultisig: signingDevices.length > 1, - }); - dispatch(setSigningDevices(jade)); - navigation.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }); - } catch (err) { - resetQR(); - showToast('Invalid QR, please scan the QR from Jade!'); - navigation.dispatch(CommonActions.navigate('SignersList')); - captureError(err); - } - }; - - const verifyKeeperSigner = (qrData, resetQR) => { - try { - const { mfp, xpub, derivationPath } = JSON.parse(qrData); - const ksd = generateSignerFromMetaData({ - xpub, - derivationPath, - xfp: mfp, - signerType: SignerType.KEEPER, - storageType: SignerStorage.WARM, - isMultisig: true, - }); - dispatch(setSigningDevices(ksd)); - navigation.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }); - } catch (err) { - resetQR(); - const message = crossInteractionHandler(err); - throw new Error(message); - } - }; - - const verifySigningServer = async (otp) => { - try { - if (signingDevices.length <= 1) throw new Error('Add two other devices first to recover'); - const cosignersMapIds = generateCosignerMapIds(signingDevices, SignerType.POLICY_SERVER); - const response = await SigningServer.fetchSignerSetupViaCosigners(cosignersMapIds[0], otp); - if (response.xpub) { - const signingServerKey = generateSignerFromMetaData({ - xpub: response.xpub, - derivationPath: response.derivationPath, - xfp: response.masterFingerprint, - signerType: SignerType.POLICY_SERVER, - storageType: SignerStorage.WARM, - isMultisig: true, - signerId: response.id, - signerPolicy: response.policy, - }); - - dispatch(setSigningDevices(signingServerKey)); - navigation.dispatch(CommonActions.navigate('VaultRecoveryAddSigner')); - showToast(`${signingServerKey.signerName} added successfully`, ); - } - } catch (err) { - Alert.alert(`${err}`); - } - }; - - const requestInheritanceKey = async (signers: VaultSigner[]) => { - try { - if (signers.length <= 1) throw new Error('Add two other devices first to recover'); - const cosignersMapIds = generateCosignerMapIds(signingDevices, SignerType.INHERITANCEKEY); - - const requestId = `request-${generateKey(10)}`; - const thresholdDescriptors = signers.map((signer) => signer.signerId); - - const { requestStatus } = await InheritanceKeyServer.requestInheritanceKey( - requestId, - cosignersMapIds[0], - thresholdDescriptors - ); - - showToast( - `Request would approve in ${formatDuration(requestStatus.approvesIn)} if not rejected`, - - ); - dispatch(setInheritanceRequestId(requestId)); - navigation.dispatch(CommonActions.navigate('VaultRecoveryAddSigner')); - } catch (err) { - showToast(`${err}`, ); - } - close(); - }; - - function HardWareWallet({ disabled, message, type, first = false, last = false }: HWProps) { - const [visible, setVisible] = useState(false); - const { signingDevices } = useAppSelector((state) => state.bhr); - - const onPress = () => { - open(); - }; - - const open = () => setVisible(true); - const close = () => setVisible(false); - - const onQRScan = (qrData, resetQR) => { - switch (type as SignerType) { - case SignerType.PASSPORT: - return verifyPassport(qrData, resetQR); - case SignerType.SEEDSIGNER: - return verifySeedSigner(qrData, resetQR); - case SignerType.KEYSTONE: - return verifyKeystone(qrData, resetQR); - case SignerType.JADE: - return verifyJade(qrData, resetQR); - case SignerType.KEEPER: - return verifyKeeperSigner(qrData, resetQR); - default: - } - }; - - const navigateToAddQrBasedSigner = () => { - close(); - navigation.dispatch( - CommonActions.navigate({ - name: 'QrRecovery', - params: { - title: `Setting up ${type}`, - subtitle: 'Please scan until all the QR data has been retrieved', - onQrScan: onQRScan, - type, - }, - }) - ); - }; - - const navigateToVerifyWithChannel = () => { - setVisible(false); - navigation.dispatch( - CommonActions.navigate({ - name: 'ConnectChannelRecovery', - params: { - title: `Setting up ${getSignerNameFromType(type)}`, - subtitle: `Please visit ${config.KEEPER_HWI} on your Chrome browser to use the Keeper Hardware Interface to setup`, - type, - }, - }) - ); - }; - - return ( - <> - - - - {SDIcons(type).Icon} - - - {SDIcons(type).Logo} - - {message} - - - - - - - { - navigate('TapSignerRecovery'); - close(); - }} - textColor="light.primaryText" - Content={TapsignerSetupContent} - /> - { - navigate('ColdCardReocvery'); - close(); - }} - textColor="light.primaryText" - Content={ColdCardSetupContent} - /> - - - - - - - } - buttonText="Continue" - buttonTextColor="light.white" - buttonCallback={navigateToAddQrBasedSigner} - textColor="light.primaryText" - /> - } - buttonText="Continue" - buttonTextColor="light.white" - buttonCallback={() => { - const navigationState = getnavigationState(SignerType.SEED_WORDS); - navigation.dispatch(CommonActions.reset(navigationState)); - close(); - }} - textColor="light.primaryText" - /> - } - buttonText="Continue" - buttonTextColor="light.white" - buttonCallback={() => { - const navigationState = getnavigationState(SignerType.MOBILE_KEY); - navigation.dispatch(CommonActions.reset(navigationState)); - close(); - }} - textColor="light.primaryText" - /> - - } - buttonText="Continue" - buttonTextColor="light.white" - buttonCallback={() => { - requestInheritanceKey(signingDevices); - }} - textColor="light.primaryText" - /> - } - buttonText="Continue" - buttonCallback={navigateToVerifyWithChannel} - /> - } - buttonText="Continue" - buttonCallback={navigateToVerifyWithChannel} - /> - - ); - } - - const { inheritanceRequestId } = useAppSelector((state) => state.storage); - - return ( - - - navigation.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }) - } - /> - - - {[ - SignerType.TAPSIGNER, - SignerType.COLDCARD, - SignerType.SEEDSIGNER, - SignerType.PASSPORT, - SignerType.JADE, - SignerType.BITBOX02, - SignerType.TREZOR, - SignerType.KEYSTONE, - SignerType.LEDGER, - SignerType.KEEPER, - SignerType.SEED_WORDS, - SignerType.MOBILE_KEY, - SignerType.POLICY_SERVER, - SignerType.INHERITANCEKEY, - ].map((type: SignerType, index: number) => { - const { disabled, message } = getDeviceStatus( - type, - isNfcSupported, - signingDevices, - inheritanceRequestId - ); - return ( - - ); - })} - - - - ); -} - -const styles = StyleSheet.create({ - modalText: { - letterSpacing: 0.65, - fontSize: 13, - marginTop: 5, - padding: 1, - }, - scrollViewContainer: { - alignItems: 'center', - justifyContent: 'center', - }, - scrollViewWrapper: { - height: windowHeight > 800 ? '90%' : '85%', - }, - contactUsText: { - fontSize: 12, - letterSpacing: 0.6, - width: wp(300), - lineHeight: 20, - marginTop: hp(20), - }, - walletMapContainer: { - alignItems: 'center', - height: windowHeight * 0.08, - flexDirection: 'row', - paddingLeft: wp(40), - }, - walletMapWrapper: { - marginRight: wp(20), - width: wp(15), - }, - walletMapLogoWrapper: { - marginLeft: wp(23), - justifyContent: 'flex-end', - marginTop: hp(20), - }, - messageText: { - fontSize: 10, - fontWeight: '400', - letterSpacing: 1.3, - marginTop: hp(5), - }, - dividerStyle: { - opacity: 0.1, - width: windowWidth * 0.8, - height: 0.5, - }, - divider: { - opacity: 0.5, - height: hp(26), - width: 1.5, - }, - italics: { - fontStyle: 'italic', - }, -}); - -export default SignersList; diff --git a/src/screens/VaultRecovery/VaultRecovery.tsx b/src/screens/VaultRecovery/VaultRecovery.tsx deleted file mode 100644 index bd5a0bdde..000000000 --- a/src/screens/VaultRecovery/VaultRecovery.tsx +++ /dev/null @@ -1,441 +0,0 @@ -import Text from 'src/components/KeeperText'; -import { Box, HStack, Pressable, VStack } from 'native-base'; -import { FlatList, StyleSheet, TouchableOpacity, View } from 'react-native'; -import React, { useEffect, useState } from 'react'; -import messaging from '@react-native-firebase/messaging'; -import AddIcon from 'src/assets/images/green_add.svg'; -import AddSignerIcon from 'src/assets/images/addSigner.svg'; -import Buttons from 'src/components/Buttons'; -import KeeperHeader from 'src/components/KeeperHeader'; -import IconArrowBlack from 'src/assets/images/icon_arrow_black.svg'; -import Note from 'src/components/Note/Note'; -import ScreenWrapper from 'src/components/ScreenWrapper'; -import SuccessSvg from 'src/assets/images/successSvg.svg'; -import { hp, windowHeight, wp } from 'src/constants/responsive'; -import { - removeSigningDeviceBhr, - setRelayVaultRecoveryShellId, - setSigningDevices, -} from 'src/store/reducers/bhr'; -import { useAppSelector } from 'src/store/hooks'; -import { useDispatch } from 'react-redux'; -import { setupKeeperApp } from 'src/store/sagaActions/storage'; -import { NewVaultInfo } from 'src/store/sagas/wallets'; -import { captureError } from 'src/services/sentry'; -import { addNewVault } from 'src/store/sagaActions/vaults'; -import { SignerStorage, SignerType, VaultType } from 'src/core/wallets/enums'; -import Relay from 'src/services/operations/Relay'; -import { generateCosignerMapIds, generateVaultId } from 'src/core/wallets/factories/VaultFactory'; -import config from 'src/core/config'; -import { hash256 } from 'src/services/operations/encryption'; -import TickIcon from 'src/assets/images/icon_tick.svg'; -import { updateSignerForScheme } from 'src/hooks/useSignerIntel'; -import KeeperModal from 'src/components/KeeperModal'; -import { setTempShellId } from 'src/store/reducers/vaults'; -import useToastMessage from 'src/hooks/useToastMessage'; -import ToastErrorIcon from 'src/assets/images/toast_error.svg'; -import InheritanceIcon from 'src/assets/images/inheritanceBrown.svg'; -import TimeIcon from 'src/assets/images/time.svg'; -import InheritanceKeyServer from 'src/services/operations/InheritanceKey'; -import { VaultSigner } from 'src/core/wallets/interfaces/vault'; -import { generateSignerFromMetaData } from 'src/hardware'; -import moment from 'moment'; -import { setInheritanceRequestId, setRecoveryCreatedApp } from 'src/store/reducers/storage'; -import useConfigRecovery from 'src/hooks/useConfigReocvery'; -import ActivityIndicatorView from 'src/components/AppActivityIndicator/ActivityIndicatorView'; -import { SDIcons } from '../Vault/SigningDeviceIcons'; - -export function formatDuration(ms) { - const duration = moment.duration(ms); - return Math.floor(duration.asHours()) + moment.utc(duration.asMilliseconds()).format(':mm:ss'); -} - -function AddSigningDevice(props) { - return ( - - - - - {props.icon} - - - {props.title} - - - {props.subTitle} - - - - - {props.arrowIcon} - - - - - ); -} - -function SignerItem({ signer, index }: { signer: any | undefined; index: number }) { - const dispatch = useDispatch(); - const removeSigningDevice = () => { - dispatch(removeSigningDeviceBhr(signer)); - }; - return ( - - - - - {SDIcons(signer.type, true).Icon} - - - - {signer.type} - - - - - - Remove - - - - - ); -} - -function SuccessModalContent() { - return ( - - - - - - The BIP-85 wallets in the app are new as they can’t be recovered using this method - - - ); -} - -function VaultRecovery({ navigation }) { - const { showToast } = useToastMessage(); - const { initateRecovery, recoveryLoading: configRecoveryLoading } = useConfigRecovery(); - const dispatch = useDispatch(); - const { - signingDevices, - relayVaultError, - relayVaultUpdate, - relayVaultReoveryShellId, - vaultRecoveryDetails, - } = useAppSelector((state) => state.bhr); - - const [scheme, setScheme] = useState(); - const { appId } = useAppSelector((state) => state.storage); - const [signersList, setsignersList] = useState(signingDevices); - const [error, setError] = useState(false); - const [recoveryLoading, setRecoveryLoading] = useState(false); - const [successModalVisible, setSuccessModalVisible] = useState(false); - const { inheritanceRequestId } = useAppSelector((state) => state.storage); - const [isIKS, setIsIKS] = useState(false); - - async function createNewApp() { - try { - const fcmToken = await messaging().getToken(); - dispatch(setRecoveryCreatedApp(true)); - dispatch(setupKeeperApp(fcmToken)); - } catch (error) { - dispatch(setRecoveryCreatedApp(true)); - dispatch(setupKeeperApp()); - } - } - - const checkInheritanceKeyRequest = async (signers: VaultSigner[], requestId: string) => { - try { - if (signers.length <= 1) throw new Error('Add two other devices first to recover'); - const cosignersMapIds = generateCosignerMapIds(signers, SignerType.INHERITANCEKEY); - const thresholdDescriptors = signers.map((signer) => signer.signerId); - - const { requestStatus, setupInfo } = await InheritanceKeyServer.requestInheritanceKey( - requestId, - cosignersMapIds[0], - thresholdDescriptors - ); - - if (requestStatus.isDeclined) { - showToast('Inheritance request has been declined', ); - // dispatch(setInheritanceRequestId('')); // clear existing request - return; - } - - if (!requestStatus.isApproved) { - showToast( - `Request would approve in ${formatDuration(requestStatus.approvesIn)} if not rejected`, - - ); - } - - if (requestStatus.isApproved && setupInfo) { - const inheritanceKey = generateSignerFromMetaData({ - xpub: setupInfo.inheritanceXpub, - derivationPath: setupInfo.derivationPath, - xfp: setupInfo.masterFingerprint, - signerType: SignerType.INHERITANCEKEY, - storageType: SignerStorage.WARM, - isMultisig: true, - inheritanceKeyInfo: { - configuration: setupInfo.configuration, - policy: setupInfo.policy, - }, - }); - if (setupInfo.configuration.bsms) { - initateRecovery(setupInfo.configuration.bsms); - } else { - showToast(`Cannot recreate Vault as BSMS was not present`, ); - } - dispatch(setSigningDevices(inheritanceKey)); - dispatch(setInheritanceRequestId('')); // clear approved request - showToast(`${inheritanceKey.signerName} added successfully`, ); - } - } catch (err) { - showToast(`${err}`, ); - } - }; - - useEffect(() => { - setsignersList( - signingDevices.map((signer) => updateSignerForScheme(signer, signingDevices?.length)) - ); - }, [signingDevices]); - - useEffect(() => { - if (signersList.length === 1) { - getMetaData(); - } - const hasIKS = signersList.some((signer) => signer.type === SignerType.INHERITANCEKEY); - setIsIKS(hasIKS); - }, [signersList]); - - useEffect(() => { - if (scheme && !appId) { - createNewApp(); - } - }, [scheme]); - - useEffect(() => { - if (scheme && appId) { - try { - const vaultInfo: NewVaultInfo = { - vaultType: VaultType.DEFAULT, - vaultScheme: scheme, - vaultSigners: signersList, - vaultDetails: { - name: vaultRecoveryDetails.name, - description: vaultRecoveryDetails.description, - }, - }; - dispatch(addNewVault({ newVaultInfo: vaultInfo })); - } catch (err) { - captureError(err); - } - } - }, [appId]); - - useEffect(() => { - if (relayVaultUpdate) { - setRecoveryLoading(false); - setSuccessModalVisible(true); - } - if (relayVaultError) { - showToast('Something went wrong!', ); - } - }, [relayVaultUpdate, relayVaultError]); - - // try catch API error - const vaultCheck = async () => { - const vaultId = generateVaultId(signersList, config.NETWORK_TYPE, vaultRecoveryDetails.scheme); - const response = await Relay.vaultCheck(vaultId); - if (response.isVault) { - setScheme(response.scheme); - } else { - setRecoveryLoading(false); - showToast('Vault does not exist with this signer combination', ); - } - }; - - // try catch API error - const getMetaData = async () => { - try { - setError(false); - const xfpHash = hash256(signersList[0].masterFingerprint); - const multisigSignerId = updateSignerForScheme(signersList[0], 2).signerId; - const response = await Relay.getVaultMetaData(xfpHash, multisigSignerId); - if (response?.vaultShellId) { - dispatch(setRelayVaultRecoveryShellId(response.vaultShellId)); - dispatch(setTempShellId(response.vaultShellId)); - } else if (!response?.vaultShellId && response?.appId) { - dispatch(setRelayVaultRecoveryShellId(response.appId)); - dispatch(setTempShellId(response.appId)); - } else if (response.error) { - setError(true); - showToast( - 'No vault is assocaited with this signer, try with another signer', - - ); - } - } catch (err) { - console.log(err); - setError(true); - showToast('Something Went Wrong!', ); - } - }; - - const startRecovery = () => { - setRecoveryLoading(true); - vaultCheck(); - }; - - const renderSigner = ({ item, index }) => ; - return ( - - - - {signersList.length > 0 ? ( - - item?.signerId ?? index} - renderItem={renderSigner} - style={{ - marginTop: hp(32), - height: windowHeight > 680 ? '66%' : '51%', - }} - /> - {inheritanceRequestId && ( - } - arrowIcon={} - onPress={() => { - checkInheritanceKeyRequest(signingDevices, inheritanceRequestId); - }} - title="Inheritance Key Request Sent" - subTitle="3 weeks remaning" - /> - )} - } - arrowIcon={} - onPress={() => { - if (error) { - showToast( - 'Warning: No Vault is assocaited with this signer, please reomve and try with another signer' - ); - } else navigation.navigate('LoginStack', { screen: 'SigningDeviceListRecovery' }); - }} - title="Add Another" - subTitle="Select signing device" - /> - - ) : ( - - - navigation.navigate('LoginStack', { screen: 'SigningDeviceListRecovery' }) - } - > - - - - - - You can use any one of the signing devices to start with - - - )} - - - {signingDevices.length > 0 && ( - - checkInheritanceKeyRequest(signingDevices, inheritanceRequestId) - : startRecovery - } - secondaryText={isIKS && 'Recreate via BSMS'} - secondaryCallback={() => - navigation.navigate('LoginStack', { screen: 'VaultConfigurationRecovery' }) - } - primaryLoading={recoveryLoading} - /> - - )} - - - {}} - showCloseIcon={false} - buttonCallback={() => { - setSuccessModalVisible(false); - navigation.replace('App'); - }} - /> - - - ); -} - -const styles = StyleSheet.create({ - signerItem: { - alignItems: 'center', - justifyContent: 'space-between', - width: '100%', - }, - remove: { - height: 26, - paddingHorizontal: 12, - borderRadius: 5, - backgroundColor: '#FAC48B', - justifyContent: 'center', - }, - scrollViewWrapper: { - flex: 0.7, - justifyContent: 'space-between', - }, - bottomViewWrapper: { - position: 'absolute', - bottom: 5, - width: '100%', - marginHorizontal: 20, - }, -}); - -export default VaultRecovery; diff --git a/src/screens/WalletDetails/CollabrativeWalletSettings.tsx b/src/screens/WalletDetails/CollabrativeWalletSettings.tsx index c79688bab..14cf260e3 100644 --- a/src/screens/WalletDetails/CollabrativeWalletSettings.tsx +++ b/src/screens/WalletDetails/CollabrativeWalletSettings.tsx @@ -7,19 +7,20 @@ import Note from 'src/components/Note/Note'; import { SignerType } from 'src/core/wallets/enums'; import { signCosignerPSBT } from 'src/core/wallets/factories/WalletFactory'; import useWallets from 'src/hooks/useWallets'; -import { Vault } from 'src/core/wallets/interfaces/vault'; import { genrateOutputDescriptors } from 'src/core/utils'; import { StyleSheet } from 'react-native'; import useToastMessage from 'src/hooks/useToastMessage'; import OptionCard from 'src/components/OptionCard'; import ScreenWrapper from 'src/components/ScreenWrapper'; +import useVault from 'src/hooks/useVault'; function CollabrativeWalletSettings() { const route = useRoute(); - const { wallet: collaborativeWallet } = route.params as { wallet: Vault }; + const { vaultId } = route.params as { vaultId: string }; + const { activeVault } = useVault({ vaultId }); const navigation = useNavigation(); - const wallet = useWallets({ walletIds: [collaborativeWallet.collaborativeWalletId] }).wallets[0]; - const descriptorString = genrateOutputDescriptors(collaborativeWallet); + const wallet = useWallets({ walletIds: [activeVault.collaborativeWalletId] }).wallets[0]; + const descriptorString = genrateOutputDescriptors(activeVault); const { showToast } = useToastMessage(); const signPSBT = (serializedPSBT, resetQR) => { @@ -47,7 +48,7 @@ function CollabrativeWalletSettings() { { navigation.dispatch( @@ -91,7 +92,7 @@ function CollabrativeWalletSettings() { diff --git a/src/screens/WalletDetails/CosignerDetails.tsx b/src/screens/WalletDetails/CosignerDetails.tsx index 14e783578..e1ba3a1fb 100644 --- a/src/screens/WalletDetails/CosignerDetails.tsx +++ b/src/screens/WalletDetails/CosignerDetails.tsx @@ -16,6 +16,8 @@ import Note from 'src/components/Note/Note'; import { getCosignerDetails } from 'src/core/wallets/factories/WalletFactory'; import ShareWithNfc from '../NFCChannel/ShareWithNfc'; import { useQuery } from '@realm/react'; +import { getKeyExpression } from 'src/core/utils'; +import { XpubTypes } from 'src/core/wallets/enums'; function CosignerDetails() { const { colorMode } = useColorMode(); @@ -29,8 +31,14 @@ function CosignerDetails() { useEffect(() => { setTimeout(() => { - const details = getCosignerDetails(wallet, keeper.id); - setDetails(JSON.stringify(details)); + const details = getCosignerDetails(wallet); + const keyDescriptor = getKeyExpression( + details.mfp, + details.xpubDetails[XpubTypes.P2WSH].derivationPath, + details.xpubDetails[XpubTypes.P2WSH].xpub, + false + ); + setDetails(keyDescriptor); }, 200); }, []); @@ -54,11 +62,6 @@ function CosignerDetails() { - ); @@ -73,5 +76,6 @@ const styles = StyleSheet.create({ }, bottom: { marginHorizontal: '5%', + marginBottom: 25, }, }); diff --git a/src/screens/WalletDetails/EditWalletDetails.tsx b/src/screens/WalletDetails/EditWalletDetails.tsx index ce48efce8..d306e9e70 100644 --- a/src/screens/WalletDetails/EditWalletDetails.tsx +++ b/src/screens/WalletDetails/EditWalletDetails.tsx @@ -9,16 +9,22 @@ import KeeperHeader from 'src/components/KeeperHeader'; import StatusBarComponent from 'src/components/StatusBarComponent'; import { windowHeight, wp } from 'src/constants/responsive'; import Buttons from 'src/components/Buttons'; -import { updateWalletDetails } from 'src/store/sagaActions/wallets'; +import { updateVaultDetails, updateWalletDetails } from 'src/store/sagaActions/wallets'; import { LocalizationContext } from 'src/context/Localization/LocContext'; import useToastMessage from 'src/hooks/useToastMessage'; import TickIcon from 'src/assets/images/icon_tick.svg'; import ToastErrorIcon from 'src/assets/images/toast_error.svg'; import { useAppSelector } from 'src/store/hooks'; -import { resetRealyWalletState } from 'src/store/reducers/bhr'; +import { resetRealyVaultState, resetRealyWalletState } from 'src/store/reducers/bhr'; import KeeperTextInput from 'src/components/KeeperTextInput'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { AppStackParams } from 'src/navigation/types'; +import { EntityKind } from 'src/core/wallets/enums'; +import { Wallet } from 'src/core/wallets/interfaces/wallet'; +import { Vault } from 'src/core/wallets/interfaces/vault'; -function EditWalletSettings({ route }) { +type ScreenProps = NativeStackScreenProps; +function EditWalletSettings({ route }: ScreenProps) { const { colorMode } = useColorMode(); const navigtaion = useNavigation(); const dispatch = useDispatch(); @@ -26,10 +32,12 @@ function EditWalletSettings({ route }) { const walletText = translations.wallet; const { common } = translations; - const { wallet } = route.params; + const { wallet } = route.params || {}; const { showToast } = useToastMessage(); const { relayWalletUpdateLoading, relayWalletUpdate, relayWalletError, realyWalletErrorMessage } = useAppSelector((state) => state.bhr); + const { relayVaultUpdate, relayVaultError, realyVaultErrorMessage, relayVaultUpdateLoading } = + useAppSelector((state) => state.bhr); const [walletName, setWalletName] = useState(wallet.presentationData.name); const [walletDescription, setWalletDescription] = useState(wallet.presentationData.description); @@ -39,7 +47,11 @@ function EditWalletSettings({ route }) { name: walletName, description: walletDescription, }; - dispatch(updateWalletDetails(wallet, details)); + if (wallet.entityKind === EntityKind.VAULT) { + dispatch(updateVaultDetails(wallet as Vault, details)); + } else { + dispatch(updateWalletDetails(wallet as Wallet, details)); + } }; useEffect(() => { @@ -54,18 +66,23 @@ function EditWalletSettings({ route }) { } }, [relayWalletUpdate, relayWalletError, realyWalletErrorMessage]); + useEffect(() => { + if (relayVaultError) { + showToast(realyVaultErrorMessage, ); + dispatch(resetRealyVaultState()); + } + if (relayVaultUpdate) { + navigtaion.goBack(); + showToast('Vault details updated', ); + dispatch(resetRealyVaultState()); + } + }, [relayVaultUpdate, relayVaultError, realyVaultErrorMessage]); + return ( - + diff --git a/src/screens/WalletDetails/ImportDescriptor.tsx b/src/screens/WalletDetails/ImportDescriptor.tsx index d9b665ebe..9eef6b4de 100644 --- a/src/screens/WalletDetails/ImportDescriptor.tsx +++ b/src/screens/WalletDetails/ImportDescriptor.tsx @@ -42,7 +42,10 @@ function ImportDescriptorScreen({ navigation }) { setWalletCreationLoading(false); const navigationState = { index: 1, - routes: [{ name: 'Home' }, { name: 'VaultDetails', params: { collaborativeWalletId } }], + routes: [ + { name: 'Home' }, + { name: 'VaultDetails', params: { vaultId: collaborativeWalletId } }, + ], }; navigation.dispatch(CommonActions.reset(navigationState)); dispatch(resetVaultFlags()); @@ -62,15 +65,15 @@ function ImportDescriptorScreen({ navigation }) { if (parsedText) { const signers: VaultSigner[] = []; parsedText.signersDetails.forEach((config) => { - const signer = generateSignerFromMetaData({ + const { key } = generateSignerFromMetaData({ xpub: config.xpub, derivationPath: config.path, - xfp: config.masterFingerprint, + masterFingerprint: config.masterFingerprint, signerType: SignerType.KEEPER, storageType: SignerStorage.WARM, isMultisig: config.isMultisig, }); - signers.push(signer); + signers.push(key); }); const parentCollaborativeWallet = @@ -116,7 +119,7 @@ function ImportDescriptorScreen({ navigation }) { /> state.bhr); @@ -87,7 +87,7 @@ function UpdateWalletDetails({ route }) { ...wallet.derivationDetails, xDerivationPath: path, }; - const specs = generateWalletSpecs( + const specs = generateWalletSpecsFromMnemonic( derivationDetails.mnemonic, WalletUtilities.getNetworkByType(wallet.networkType), derivationDetails.xDerivationPath @@ -103,13 +103,13 @@ function UpdateWalletDetails({ route }) { scriptType, }); if (isUpdated) { - setWarringsVisible(false) + setWarringsVisible(false); updateAppImageWorker({ payload: { wallet } }); navigtaion.goBack(); showToast(walletTranslation.walletDetailsUpdate, ); } else showToast(walletTranslation.failToUpdate, ); } catch (error) { - setWarringsVisible(false) + setWarringsVisible(false); console.log(error); showToast(walletTranslation.failToUpdate, ); } @@ -132,18 +132,18 @@ function UpdateWalletDetails({ route }) { { - setWarringsVisible(false) + setWarringsVisible(false); }} primaryText="I understand, Proceed" primaryCallback={() => { - updateWallet() + updateWallet(); }} primaryLoading={relayWalletUpdateLoading} /> - ) + ); } return ( @@ -157,9 +157,7 @@ function UpdateWalletDetails({ route }) { @@ -229,12 +227,19 @@ function UpdateWalletDetails({ route }) { navigtaion.goBack(); }} primaryText={common.save} - primaryDisable={path === wallet?.derivationDetails.xDerivationPath && wallet?.specs?.balances?.confirmed === 0 && wallet?.specs?.balances?.unconfirmed === 0} + primaryDisable={ + path === wallet?.derivationDetails.xDerivationPath && + wallet?.specs?.balances?.confirmed === 0 && + wallet?.specs?.balances?.unconfirmed === 0 + } primaryCallback={() => { - if (wallet?.specs?.balances?.confirmed === 0 && wallet?.specs?.balances?.unconfirmed === 0) { - setWarringsVisible(true) + if ( + wallet?.specs?.balances?.confirmed === 0 && + wallet?.specs?.balances?.unconfirmed === 0 + ) { + setWarringsVisible(true); } else { - showToast(walletTranslation.walletBalanceMsg, ) + showToast(walletTranslation.walletBalanceMsg, ); } }} primaryLoading={relayWalletUpdateLoading} @@ -329,8 +334,8 @@ const styles = StyleSheet.create({ fontSize: 13, paddingHorizontal: 1, paddingVertical: 5, - letterSpacing: 0.65 - } + letterSpacing: 0.65, + }, }); export default UpdateWalletDetails; diff --git a/src/screens/WalletDetails/WalletDetails.tsx b/src/screens/WalletDetails/WalletDetails.tsx index c1a02dc9d..de643be9b 100644 --- a/src/screens/WalletDetails/WalletDetails.tsx +++ b/src/screens/WalletDetails/WalletDetails.tsx @@ -1,15 +1,16 @@ -import { StyleSheet, TouchableOpacity } from 'react-native'; -import { Box, HStack, Pressable, StatusBar, useColorMode, VStack } from 'native-base'; +import { StyleSheet } from 'react-native'; +import { Box, HStack, StatusBar, useColorMode, VStack } from 'native-base'; import React, { useContext, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import idx from 'idx'; import { useNavigation } from '@react-navigation/native'; import AddWalletIcon from 'src/assets/images/addWallet_illustration.svg'; -import WalletInsideGreen from 'src/assets/images/Wallet_inside_green.svg'; +import WalletIcon from 'src/assets/images/hexagontile_wallet.svg'; + import WhirlpoolAccountIcon from 'src/assets/images/whirlpool_account.svg'; -import Arrow from 'src/assets/images/arrow_brown.svg'; -import { hp, windowHeight, wp } from 'src/constants/responsive'; +import CoinsIcon from 'src/assets/images/whirlpool.svg'; +import { wp } from 'src/constants/responsive'; import Text from 'src/components/KeeperText'; import { refreshWallets } from 'src/store/sagaActions/wallets'; import { setIntroModal } from 'src/store/reducers/wallets'; @@ -17,17 +18,18 @@ import { useAppSelector } from 'src/store/hooks'; import KeeperHeader from 'src/components/KeeperHeader'; import useWallets from 'src/hooks/useWallets'; -import { EntityKind, WalletType } from 'src/core/wallets/enums'; -import IconArrowBlack from 'src/assets/images/icon_arrow_black.svg'; -import useCurrencyCode from 'src/store/hooks/state-selectors/useCurrencyCode'; -import useExchangeRates from 'src/hooks/useExchangeRates'; +import { WalletType } from 'src/core/wallets/enums'; import ActivityIndicatorView from 'src/components/AppActivityIndicator/ActivityIndicatorView'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import CardPill from 'src/components/CardPill'; +import ActionCard from 'src/components/ActionCard'; import Transactions from './components/Transactions'; import TransactionFooter from './components/TransactionFooter'; import RampModal from './components/RampModal'; import LearnMoreModal from './components/LearnMoreModal'; -import CurrencyInfo from '../HomeScreen/components/CurrencyInfo'; -import { LocalizationContext } from 'src/context/Localization/LocContext'; +import CurrencyInfo from '../Home/components/CurrencyInfo'; +import { AppStackParams } from 'src/navigation/types'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; export const allowedSendTypes = [ WalletType.DEFAULT, @@ -56,15 +58,14 @@ function TransactionsAndUTXOs({ transactions, setPullRefresh, pullRefresh, walle ); } -function WalletDetails({ route }) { +type ScreenProps = NativeStackScreenProps; +function WalletDetails({ route }: ScreenProps) { const { colorMode } = useColorMode(); const navigation = useNavigation(); const dispatch = useDispatch(); const { translations } = useContext(LocalizationContext); const { common } = translations; - const currencyCode = useCurrencyCode(); - const exchangeRates = useExchangeRates(); - const { autoRefresh, walletId, walletIndex } = route?.params || {}; + const { autoRefresh = false, walletId } = route.params || {}; const wallet = useWallets({ walletIds: [walletId] })?.wallets[0]; const { presentationData: { name, description } = { name: '', description: '' }, @@ -81,8 +82,6 @@ function WalletDetails({ route }) { const syncing = walletSyncing && wallet ? !!walletSyncing[wallet.id] : false; const isWhirlpoolWallet = Boolean(wallet?.whirlpoolConfig?.whirlpoolWalletDetails); const introModal = useAppSelector((state) => state.wallet.introModal) || false; - const currentCurrency = useAppSelector((state) => state.settings.currencyKind); - const { satsEnabled } = useAppSelector((state) => state.settings); const [showBuyRampModal, setShowBuyRampModal] = useState(false); const [pullRefresh, setPullRefresh] = useState(false); @@ -102,129 +101,61 @@ function WalletDetails({ route }) { dispatch(refreshWallets([wallet], { hardRefresh: true })); setPullRefresh(false); }; - const onPressBuyBitcoin = () => setShowBuyRampModal(true); return ( - + - + dispatch(setIntroModal(true))} contrastScreen={true} + title={name} + titleColor={`${colorMode}.seashellWhite`} + subtitle={walletType === 'IMPORTED' ? 'Imported wallet' : description} + subTitleColor={`${colorMode}.seashellWhite`} + icon={isWhirlpoolWallet ? : } /> - - - - - {isWhirlpoolWallet ? : } - - - - - {name} - - - {walletType === 'IMPORTED' ? 'Imported wallet' : description} - - + + + + - - - {common.unconfirmed} - - - - {common.availableBalance} - - + + - + + + + + navigation.navigate('UTXOManagement', { + data: wallet, + routeName: 'Wallet', + accountType: WalletType.DEFAULT, + }) + } + icon={} + customStyle={{ paddingTop: 0 }} + /> - { - navigation.navigate('WalletDetailsSettings', { - wallet, - editPolicy: true, - }); - }} - > - - - - {common.transferPolicySet} - {' '} - - - - - - - - {wallet ? ( <> - + {common.transactions} - {wallet?.specs.transactions.length ? ( - - navigation.navigate('AllTransactions', { - title: 'Wallet Transactions', - subtitle: 'All incoming and outgoing transactions', - entityKind: EntityKind.WALLET, - }) - } - > - - - {common.viewAll} - - - - - ) : null} - + ) : ( @@ -270,18 +197,17 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', flex: 1, }, + topContainer: { + paddingHorizontal: 20, + paddingTop: 15, + }, walletContainer: { paddingHorizontal: wp(28), - paddingTop: wp(28), + paddingTop: wp(60), paddingBottom: 20, - borderTopLeftRadius: 20, flex: 1, justifyContent: 'space-between', }, - transactionsListContainer: { - height: windowHeight > 800 ? '66%' : '58%', - position: 'relative', - }, addNewWalletText: { fontSize: 12, letterSpacing: 0.6, @@ -295,12 +221,14 @@ const styles = StyleSheet.create({ flex: 1, }, walletHeaderWrapper: { - margin: wp(15), + marginTop: -10, + marginHorizontal: wp(15), flexDirection: 'row', width: '100%', }, walletIconWrapper: { width: '15%', + marginRight: 7, }, walletNameWrapper: { width: '85%', @@ -321,11 +249,13 @@ const styles = StyleSheet.create({ balanceWrapper: { flexDirection: 'row', width: '90%', - marginVertical: wp(20), + marginVertical: wp(30), marginHorizontal: wp(20), }, unconfirmBalanceView: { width: '50%', + flexDirection: 'row', + gap: 5, }, availableBalanceView: { width: '50%', @@ -337,22 +267,18 @@ const styles = StyleSheet.create({ alignItems: 'center', paddingLeft: 5, }, - transferPolicyCard: { - paddingHorizontal: wp(10), - height: hp(50), - width: '95%', - borderRadius: hp(5), + actionCard: { + marginTop: 20, + marginBottom: -50, + zIndex: 10, flexDirection: 'row', - justifyContent: 'space-between', + gap: 10, alignItems: 'center', - alignSelf: 'center', + justifyContent: 'center', }, - transferPolicyContent: { - paddingLeft: wp(10), - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - width: '100%', + transactionHeading: { + fontSize: 16, + letterSpacing: 0.16, }, }); export default WalletDetails; diff --git a/src/screens/WalletDetails/WalletDetailsSettings.tsx b/src/screens/WalletDetails/WalletDetailsSettings.tsx index 2a86ad7c6..19e2aef43 100644 --- a/src/screens/WalletDetails/WalletDetailsSettings.tsx +++ b/src/screens/WalletDetails/WalletDetailsSettings.tsx @@ -9,24 +9,25 @@ import KeeperModal from 'src/components/KeeperModal'; import useToastMessage from 'src/hooks/useToastMessage'; import { LocalizationContext } from 'src/context/Localization/LocContext'; import Note from 'src/components/Note/Note'; -import TransferPolicy from 'src/components/XPub/TransferPolicy'; import TickIcon from 'src/assets/images/icon_tick.svg'; import OptionCard from 'src/components/OptionCard'; import ScreenWrapper from 'src/components/ScreenWrapper'; function WalletDetailsSettings({ route }) { const { colorMode } = useColorMode(); - const { wallet, editPolicy = false } = route.params || {}; + const { wallet } = route.params || {}; const navigation = useNavigation(); const { showToast } = useToastMessage(); const [xpubVisible, setXPubVisible] = useState(false); - const [transferPolicyVisible, setTransferPolicyVisible] = useState(editPolicy); const { translations } = useContext(LocalizationContext); const walletTranslation = translations.wallet; const { importWallet, common } = translations; return ( - + - { - setTransferPolicyVisible(true); - }} - /> )} /> - { - setTransferPolicyVisible(false); - }} - title={walletTranslation.editTransPolicy} - subTitle={walletTranslation.editTransPolicySubTitle} - modalBackground={`${colorMode}.modalWhiteBackground`} - subTitleColor={`${colorMode}.secondaryText`} - textColor={`${colorMode}.primaryText`} - DarkCloseIcon={colorMode === 'dark'} - Content={() => ( - { - showToast(walletTranslation.TransPolicyChange, ); - setTransferPolicyVisible(false); - }} - secondaryBtnPress={() => { - setTransferPolicyVisible(false); - }} - /> - )} - />
); diff --git a/src/screens/WalletDetails/WalletSettings.tsx b/src/screens/WalletDetails/WalletSettings.tsx index d18c00a36..e64cd5d3f 100644 --- a/src/screens/WalletDetails/WalletSettings.tsx +++ b/src/screens/WalletDetails/WalletSettings.tsx @@ -4,7 +4,6 @@ import { Box, ScrollView, useColorMode } from 'native-base'; import { useDispatch } from 'react-redux'; import { CommonActions, useNavigation } from '@react-navigation/native'; import ShowXPub from 'src/components/XPub/ShowXPub'; -// import SeedConfirmPasscode from 'src/components/XPub/SeedConfirmPasscode'; import KeeperHeader from 'src/components/KeeperHeader'; import { wp, hp } from 'src/constants/responsive'; import KeeperModal from 'src/components/KeeperModal'; @@ -14,22 +13,20 @@ import { useAppSelector } from 'src/store/hooks'; import { setTestCoinsFailed, setTestCoinsReceived } from 'src/store/reducers/wallets'; import { LocalizationContext } from 'src/context/Localization/LocContext'; import { signCosignerPSBT } from 'src/core/wallets/factories/WalletFactory'; -import Note from 'src/components/Note/Note'; import TickIcon from 'src/assets/images/icon_tick.svg'; import config from 'src/core/config'; import { EntityKind, NetworkType, SignerType } from 'src/core/wallets/enums'; import useExchangeRates from 'src/hooks/useExchangeRates'; import useCurrencyCode from 'src/store/hooks/state-selectors/useCurrencyCode'; -import BtcWallet from 'src/assets/images/btc_black.svg'; -import BitcoinWhite from 'src/assets/images/btc_white.svg'; import useWallets from 'src/hooks/useWallets'; -import { getAmt, getCurrencyImageByRegion } from 'src/constants/Bitcoin'; import { AppContext } from 'src/context/AppContext'; import { StyleSheet } from 'react-native'; import OptionCard from 'src/components/OptionCard'; import ScreenWrapper from 'src/components/ScreenWrapper'; import PasscodeVerifyModal from 'src/components/Modal/PasscodeVerify'; import { captureError } from 'src/services/sentry'; +import WalletFingerprint from 'src/components/WalletFingerPrint'; +import TransferPolicy from 'src/components/XPub/TransferPolicy'; function WalletSettings({ route }) { const { colorMode } = useColorMode(); @@ -40,42 +37,15 @@ function WalletSettings({ route }) { const { setAppLoading, setLoadingContent } = useContext(AppContext); const [xpubVisible, setXPubVisible] = useState(false); const [confirmPassVisible, setConfirmPassVisible] = useState(false); + const [transferPolicyVisible, setTransferPolicyVisible] = useState(editPolicy); const { wallets } = useWallets(); const wallet = wallets.find((item) => item.id === walletRoute.id); const { testCoinsReceived, testCoinsFailed } = useAppSelector((state) => state.wallet); - const exchangeRates = useExchangeRates(); - const currencyCode = useCurrencyCode(); - const currentCurrency = useAppSelector((state) => state.settings.currencyKind); - const { satsEnabled } = useAppSelector((state) => state.settings); const { translations } = useContext(LocalizationContext); const walletTranslation = translations.wallet; const { settings, common } = translations; - // eslint-disable-next-line react/no-unstable-nested-components - function WalletCard({ walletName, walletBalance, walletDescription, Icon }: any) { - return ( - - - - - {walletName} - - - {walletDescription} - - - - {Icon} - - {walletBalance} - - - - - ); - } - const getTestSats = () => { dispatch(testSatsRecieve(wallet)); }; @@ -144,30 +114,6 @@ function WalletSettings({ route }) { return ( - - - + { + setTransferPolicyVisible(true); + }} + /> {config.NETWORK_TYPE === NetworkType.TESTNET && ( - { - setAppLoading(true); - getTestSats(); - }} - /> + + { + setAppLoading(true); + getTestSats(); + }} + /> + )} - - + + + + { + setTransferPolicyVisible(false); + }} + title={walletTranslation.editTransPolicy} + subTitle={walletTranslation.editTransPolicySubTitle} + modalBackground={`${colorMode}.modalWhiteBackground`} + subTitleColor={`${colorMode}.secondaryText`} + textColor={`${colorMode}.primaryText`} + DarkCloseIcon={colorMode === 'dark'} + Content={() => ( + { + showToast(walletTranslation.TransPolicyChange, ); + setTransferPolicyVisible(false); + }} + secondaryBtnPress={() => { + setTransferPolicyVisible(false); + }} + /> + )} + /> + setConfirmPassVisible(false)} @@ -287,8 +264,8 @@ const styles = StyleSheet.create({ padding: 20, position: 'relative', }, - note: { - marginHorizontal: '5%', + fingerprint: { + alignItems: 'center', }, walletCardContainer: { borderRadius: hp(20), diff --git a/src/screens/WalletDetails/components/LearnMoreModal.tsx b/src/screens/WalletDetails/components/LearnMoreModal.tsx index 187519f4c..430bf7ca0 100644 --- a/src/screens/WalletDetails/components/LearnMoreModal.tsx +++ b/src/screens/WalletDetails/components/LearnMoreModal.tsx @@ -19,7 +19,7 @@ function LinkedWalletContent() { You can use the individual wallet’s Recovery Phrases to connect other bitcoin apps to Keeper - When the funds in a wallet cross a threshold, a transfer to the Vault is triggered. This + When the funds in a wallet cross a threshold, a transfer to the vault is triggered. This ensures you don’t have more sats in hot wallets than you need. diff --git a/src/screens/WalletDetails/components/RampModal.tsx b/src/screens/WalletDetails/components/RampModal.tsx index 50818e49b..42112cff2 100644 --- a/src/screens/WalletDetails/components/RampModal.tsx +++ b/src/screens/WalletDetails/components/RampModal.tsx @@ -160,6 +160,7 @@ const styles = StyleSheet.create({ fontSize: 19, }, ctcWrapper: { + marginTop: 20, paddingRight: 5, }, }); diff --git a/src/screens/WalletDetails/components/TransactionFooter.tsx b/src/screens/WalletDetails/components/TransactionFooter.tsx index 50862aadf..e935bfb1c 100644 --- a/src/screens/WalletDetails/components/TransactionFooter.tsx +++ b/src/screens/WalletDetails/components/TransactionFooter.tsx @@ -1,37 +1,31 @@ import React from 'react'; import { CommonActions, useNavigation } from '@react-navigation/native'; -import Recieve from 'src/assets/images/receive.svg'; -import Send from 'src/assets/images/send.svg'; -import IconSettings from 'src/assets/images/icon_settings.svg'; -import BuyBitcoin from 'src/assets/images/icon_buy.svg'; -import { allowedRecieveTypes, allowedSendTypes } from '../WalletDetails'; +import SendIcon from 'src/assets/images/icon_sent_footer.svg'; +import RecieveIcon from 'src/assets/images/icon_received_footer.svg'; +import SettingIcon from 'src/assets/images/settings_footer.svg'; + import KeeperFooter from 'src/components/KeeperFooter'; +import { allowedRecieveTypes, allowedSendTypes } from '../WalletDetails'; -function TransactionFooter({ currentWallet, onPressBuyBitcoin }) { +function TransactionFooter({ currentWallet }) { const navigation = useNavigation(); const footerItems = [ { - Icon: Send, + Icon: SendIcon, text: 'Send', onPress: () => navigation.dispatch(CommonActions.navigate('Send', { sender: currentWallet })), hideItems: !allowedSendTypes.includes(currentWallet.type), }, { - Icon: Recieve, + Icon: RecieveIcon, text: 'Receive', onPress: () => navigation.dispatch(CommonActions.navigate('Receive', { wallet: currentWallet })), hideItems: !allowedRecieveTypes.includes(currentWallet.type), }, { - Icon: BuyBitcoin, - text: 'Buy', - onPress: onPressBuyBitcoin, - hideItems: !allowedRecieveTypes.includes(currentWallet.type), - }, - { - Icon: IconSettings, + Icon: SettingIcon, text: 'Settings', onPress: () => navigation.dispatch(CommonActions.navigate('WalletSettings', { wallet: currentWallet })), diff --git a/src/screens/WalletDetails/components/Transactions.tsx b/src/screens/WalletDetails/components/Transactions.tsx index 57dfdad51..7c0d71faa 100644 --- a/src/screens/WalletDetails/components/Transactions.tsx +++ b/src/screens/WalletDetails/components/Transactions.tsx @@ -1,5 +1,5 @@ import { FlatList, RefreshControl } from 'react-native'; -import React from 'react'; +import React, { useContext } from 'react'; import { useDispatch } from 'react-redux'; import { refreshWallets } from 'src/store/sagaActions/wallets'; import EmptyStateView from 'src/components/EmptyView/EmptyStateView'; @@ -8,6 +8,7 @@ import TransactionElement from 'src/components/TransactionElement'; import { CommonActions, useNavigation } from '@react-navigation/native'; import { Transaction } from 'src/core/wallets/interfaces'; import { useAppSelector } from 'src/store/hooks'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; function TransactionItem({ item, wallet, navigation, index }) { return ( @@ -29,6 +30,8 @@ function TransactionItem({ item, wallet, navigation, index }) { function Transactions({ transactions, setPullRefresh, pullRefresh, currentWallet }) { const dispatch = useDispatch(); const navigation = useNavigation(); + const { translations } = useContext(LocalizationContext); + const { common } = translations; const pullDownRefresh = () => { setPullRefresh(true); @@ -50,11 +53,7 @@ function Transactions({ transactions, setPullRefresh, pullRefresh, currentWallet keyExtractor={(item: Transaction) => `${item.txid}${item.transactionType}`} showsVerticalScrollIndicator={false} ListEmptyComponent={ - + } /> ); diff --git a/src/screens/WalletDetails/components/WalletList.tsx b/src/screens/WalletDetails/components/WalletList.tsx index 0eb928bf9..ffd591d70 100644 --- a/src/screens/WalletDetails/components/WalletList.tsx +++ b/src/screens/WalletDetails/components/WalletList.tsx @@ -23,7 +23,7 @@ function AddNewWalletTile({ walletIndex, isActive, wallet, navigation }) { onPress={() => navigation.navigate('EnterWalletDetail', { name: `Wallet ${walletIndex + 1}`, - description: 'Single-sig Wallet', + description: '', type: WalletType.DEFAULT, }) } diff --git a/src/services/electrum/client.ts b/src/services/electrum/client.ts index ffd33a51f..82ee4d5e1 100644 --- a/src/services/electrum/client.ts +++ b/src/services/electrum/client.ts @@ -222,6 +222,10 @@ export default class ElectrumClient { return peers[ELECTRUM_CLIENT.currentPeerIndex]; } + public static resetCurrentPeerIndex() { + ELECTRUM_CLIENT.currentPeerIndex = -1; + } + // if current peer to use is not provided, it will try to get the active peer from the saved list of private nodes // if current peer to use is provided, it will use that peer public static setActivePeer( diff --git a/src/services/interfaces/index.ts b/src/services/interfaces/index.ts index f32fdd931..af468ac81 100644 --- a/src/services/interfaces/index.ts +++ b/src/services/interfaces/index.ts @@ -66,6 +66,11 @@ export interface InheritancePolicy { alert?: InheritanceAlert; } +export interface EncryptedInheritancePolicy { + notification: InheritanceNotification; + alert?: string; +} + export interface InheritanceKeyInfo { configuration: InheritanceConfiguration; policy?: InheritancePolicy; diff --git a/src/services/operations/InheritanceKey.ts b/src/services/operations/InheritanceKey.ts index 5b5b79294..74d882cfd 100644 --- a/src/services/operations/InheritanceKey.ts +++ b/src/services/operations/InheritanceKey.ts @@ -1,17 +1,32 @@ import { AxiosResponse } from 'axios'; import config from 'src/core/config'; import { + EncryptedInheritancePolicy, IKSCosignersMapUpdate, - InheritanceAlert, InheritanceConfiguration, - InheritanceNotification, InheritancePolicy, } from '../interfaces'; import RestClient from '../rest/RestClient'; +import { asymmetricEncrypt } from '../operations/encryption/index'; -const { HEXA_ID, SIGNING_SERVER } = config; +const { HEXA_ID, SIGNING_SERVER, SIGNING_SERVER_RSA_PUBKEY } = config; export default class InheritanceKeyServer { + static getEncryptedInheritancePolicy = async ( + policy: InheritancePolicy + ): Promise => { + let encryptedPolicy: EncryptedInheritancePolicy; + if (policy) { + encryptedPolicy = { + ...policy, + alert: policy.alert + ? await asymmetricEncrypt(JSON.stringify(policy.alert), SIGNING_SERVER_RSA_PUBKEY) + : undefined, + }; + } + return encryptedPolicy; + }; + /** * @returns Promise */ @@ -53,12 +68,14 @@ export default class InheritanceKeyServer { setupSuccessful: boolean; }> => { let res: AxiosResponse; + const updatedEncryptedPolicy = await this.getEncryptedInheritancePolicy(policy); + try { res = await RestClient.post(`${SIGNING_SERVER}v3/finalizeIKSetup`, { HEXA_ID, id, configuration, - policy, + updatedEncryptedPolicy, }); } catch (err) { if (err.response) throw new Error(err.response.data.err); @@ -112,20 +129,18 @@ export default class InheritanceKeyServer { */ static updateInheritancePolicy = async ( id: string, - updates: { - notification?: InheritanceNotification; - alert?: InheritanceAlert; - }, + updatedPolicy: InheritancePolicy, thresholdDescriptors: string[] ): Promise<{ updated: boolean; }> => { let res: AxiosResponse; + const updatedEncryptedPolicy = await this.getEncryptedInheritancePolicy(updatedPolicy); try { res = await RestClient.post(`${SIGNING_SERVER}v3/updateInheritancePolicy`, { HEXA_ID, id, - updates, + updatedEncryptedPolicy, thresholdDescriptors, }); } catch (err) { @@ -187,11 +202,11 @@ export default class InheritanceKeyServer { isDeclined: boolean; }; setupInfo?: { + id: string; inheritanceXpub: string; masterFingerprint: string; derivationPath: string; configuration: InheritanceConfiguration; - policy: InheritancePolicy; }; }> => { let res: AxiosResponse; @@ -262,6 +277,37 @@ export default class InheritanceKeyServer { }; }; + static findIKSSetup = async ( + ids: string[], + thresholdDescriptors: string[] + ): Promise<{ + setupInfo: { + id: string; + inheritanceXpub: string; + masterFingerprint: string; + derivationPath: string; + configuration: InheritanceConfiguration; + policy: InheritancePolicy; + }; + }> => { + let res: AxiosResponse; + try { + res = await RestClient.post(`${SIGNING_SERVER}v3/findIKSSetup`, { + HEXA_ID, + ids, + thresholdDescriptors, + }); + } catch (err) { + if (err.response) throw new Error(err.response.data.err); + if (err.code) throw new Error(err.code); + } + + const { setupInfo } = res.data; + return { + setupInfo, + }; + }; + static migrateSignersV2ToV3 = async ( vaultId: string, cosignersMapIKSUpdates: IKSCosignersMapUpdate[] diff --git a/src/services/operations/Relay.ts b/src/services/operations/Relay.ts index 89f7b816e..242b22bf3 100644 --- a/src/services/operations/Relay.ts +++ b/src/services/operations/Relay.ts @@ -1,13 +1,12 @@ -/* eslint-disable consistent-return */ import { NetworkType } from 'src/core/wallets/enums'; import { SATOSHIS_IN_BTC } from 'src/constants/Bitcoin'; import { SubScriptionPlan } from 'src/models/interfaces/Subscription'; import { AxiosResponse } from 'axios'; import { AverageTxFeesByNetwork, UTXOInfo } from 'src/core/wallets/interfaces'; +import config from 'src/core/config'; import { INotification } from '../interfaces'; import RestClient from '../rest/RestClient'; import { captureError } from '../sentry'; -import config from 'src/core/config'; const { HEXA_ID, RELAY } = config; const TOR_ENDPOINT = 'https://check.torproject.org/api/ip'; @@ -392,7 +391,7 @@ export default class Relay { return data; } catch (err) { captureError(err); - throw new Error('Failed get Vault Meta Data'); + throw new Error('Failed get vault Meta Data'); } }; diff --git a/src/services/operations/SigningServer.ts b/src/services/operations/SigningServer.ts index c0a39e1bd..064617ee1 100644 --- a/src/services/operations/SigningServer.ts +++ b/src/services/operations/SigningServer.ts @@ -147,6 +147,44 @@ export default class SigningServer { }; }; + static findSignerSetup = async ( + ids: string[], + verificationToken + ): Promise<{ + valid: boolean; + id?: string; + xpub?: string; + masterFingerprint?: string; + derivationPath?: string; + policy?: SignerPolicy; + }> => { + let res: AxiosResponse; + try { + res = await RestClient.post(`${SIGNING_SERVER}v3/findSignerSetup`, { + HEXA_ID, + ids, + verificationToken, + }); + } catch (err) { + if (err.response) throw new Error(err.response.data.err); + if (err.code) throw new Error(err.code); + } + + const { valid } = res.data; + if (!valid) throw new Error('Signer validation failed'); + + const { id, xpub, masterFingerprint, derivationPath, policy } = res.data; + + return { + valid, + id, + xpub, + masterFingerprint, + derivationPath, + policy, + }; + }; + static updatePolicy = async ( id: string, verificationToken, diff --git a/src/services/operations/encryption/index.ts b/src/services/operations/encryption/index.ts index 35199cf27..a6481f173 100644 --- a/src/services/operations/encryption/index.ts +++ b/src/services/operations/encryption/index.ts @@ -1,10 +1,12 @@ import cryptoJS from 'crypto-js'; -import crypto from 'crypto'; +import { randomBytes } from 'crypto'; +import { RSA } from 'react-native-rsa-native'; export const hash256 = (data: string) => cryptoJS.SHA256(data).toString(cryptoJS.enc.Hex); export const hash512 = (data: string) => cryptoJS.SHA512(data).toString(cryptoJS.enc.Hex); -export const getRandomBytes = (size: number = 32) => crypto.randomBytes(size).toString('hex'); +export const getRandomBytes = (size: number = 32) => randomBytes(size).toString('hex'); + export const generateEncryptionKey = (entropy?: string, randomBytesSize?: number): string => entropy ? hash256(entropy) : hash256(getRandomBytes(randomBytesSize)); @@ -23,3 +25,7 @@ export const generateKey = (length: number): string => { } return result; }; + +export const asymmetricEncrypt = (data: string, publicKey: string): Promise => { + return RSA.encrypt(data, publicKey); +}; diff --git a/src/services/sentry/index.ts b/src/services/sentry/index.ts index a3d40eadb..92e6d403e 100644 --- a/src/services/sentry/index.ts +++ b/src/services/sentry/index.ts @@ -7,7 +7,7 @@ import config from 'src/core/config'; // Construct a new instrumentation instance. This is needed to communicate between the integration and React const routingInstrumentation = new Sentry.ReactNavigationInstrumentation(); -export const sentryConfig = { +export const sentryConfig: Sentry.ReactNativeOptions = { maxBreadcrumbs: 50, tracesSampleRate: 1.0, dsn: config.SENTRY_DNS, @@ -25,11 +25,15 @@ export const identifyUser = (id: string) => { }; export const captureError = (error: Error, context?: CaptureContext) => { - if (__DEV__) { - // eslint-disable-next-line no-console - console.log('@captureError: ', error); + try { + if (__DEV__) { + // eslint-disable-next-line no-console + console.log('@captureError: ', error); + } + return Sentry.captureException(error, context); + } catch (err) { + console.log(err); } - return Sentry.captureException(error, context); }; export const logMessage = (message: string, captureContext?: CaptureContext | SeverityLevel) => diff --git a/src/storage/realm/RealmProvider.tsx b/src/storage/realm/RealmProvider.tsx index d6e0330cf..d900be92f 100644 --- a/src/storage/realm/RealmProvider.tsx +++ b/src/storage/realm/RealmProvider.tsx @@ -1,7 +1,7 @@ -import React from 'react'; - +import React, { useEffect } from 'react'; +import * as Sentry from '@sentry/react-native'; import { KeeperApp } from 'src/models/interfaces/KeeperApp'; -import config from 'src/core/config'; +import config, { APP_STAGE } from 'src/core/config'; import { RealmProvider as Provider, useQuery } from '@realm/react'; import { stringToArrayBuffer } from 'src/store/sagas/login'; import { useAppSelector } from 'src/store/hooks'; @@ -9,6 +9,8 @@ import { RealmDatabase } from './realm'; import { RealmSchema } from './enum'; import { getJSONFromRealmObject } from './utils'; import schema from './schema'; +import { sentryConfig } from 'src/services/sentry'; +import dbManager from './dbManager'; export const realmConfig = (key) => ({ path: RealmDatabase.file, @@ -18,8 +20,19 @@ export const realmConfig = (key) => ({ }); const AppWithNetwork = ({ children }) => { - const { networkType }: KeeperApp = useQuery(RealmSchema.KeeperApp).map(getJSONFromRealmObject)[0]; + const { networkType, id }: KeeperApp = useQuery(RealmSchema.KeeperApp).map( + getJSONFromRealmObject + )[0]; config.setNetwork(networkType); + + useEffect(() => { + if (__DEV__ || config.ENVIRONMENT === APP_STAGE.DEVELOPMENT) { + console.log('running..'); + Sentry.init(sentryConfig); + dbManager.updateObjectById(RealmSchema.KeeperApp, id, { enableAnalytics: true }); + } + }, []); + return children; }; diff --git a/src/storage/realm/dbManager.ts b/src/storage/realm/dbManager.ts index 17d82d9fa..854c81986 100644 --- a/src/storage/realm/dbManager.ts +++ b/src/storage/realm/dbManager.ts @@ -24,9 +24,9 @@ const deleteRealm = (key: ArrayBuffer | ArrayBufferView | Int8Array) => realm.de * @param {RealmSchema} schema * @param {any} object */ -const createObject = (schema: RealmSchema, object: any) => { +const createObject = (schema: RealmSchema, object: any, updateMode = Realm.UpdateMode.All) => { try { - const hasCreated = realm.create(schema, object); + const hasCreated = realm.create(schema, object, updateMode); return hasCreated; } catch (err) { console.log(err); @@ -38,9 +38,13 @@ const createObject = (schema: RealmSchema, object: any) => { * @param {RealmSchema} schema * @param {any[]} objects */ -const createObjectBulk = (schema: RealmSchema, objects: any[]) => { +const createObjectBulk = ( + schema: RealmSchema, + objects: any[], + updateMode = Realm.UpdateMode.All +) => { try { - const hasCreated = realm.createBulk(schema, objects); + const hasCreated = realm.createBulk(schema, objects, updateMode); return hasCreated; } catch (err) { console.log(err); @@ -67,6 +71,16 @@ const getObjectById = (schema: RealmSchema, id: string) => { return objects.filtered(`id == '${id}'`)[0]; }; +/** + * generic :: fetches an object corresponding to provided schema and the supplied id + * @param {RealmSchema} schema + * @param {string} id + */ +const getObjectByPrimaryId = (schema: RealmSchema, name: string, primaryId: string) => { + const objects = realm.get(schema); + return objects.filtered(`${name} == '${primaryId}'`)[0]; +}; + /** * generic :: updates the object, corresponding to provided schema and id, w/ supplied props * @param {RealmSchema} schema @@ -88,6 +102,32 @@ const updateObjectById = (schema: RealmSchema, id: string, updateProps: any) => } }; +/** + * generic :: updates the object, corresponding to provided schema and id, w/ supplied props + * @param {RealmSchema} schema + * @param {string} id + * @param {any} updateProps + */ +const updateObjectByPrimaryId = ( + schema: RealmSchema, + name: string, + primaryId: string, + updateProps: any +) => { + try { + const object = getObjectByPrimaryId(schema, name, primaryId); + for (const [key, value] of Object.entries(updateProps)) { + realm.write(() => { + object[key] = value; + }); + } + return true; + } catch (err) { + console.error({ err }); + return false; + } +}; + /** * generic :: fetched the object corresponding to the fieldName and Value * @param {RealmSchema} schema @@ -131,7 +171,9 @@ export default { createObject, getObjectByIndex, getObjectById, + getObjectByPrimaryId, updateObjectById, + updateObjectByPrimaryId, getCollection, getObjectByField, }; diff --git a/src/storage/realm/enum.ts b/src/storage/realm/enum.ts index 8ca7881f8..ad922c94b 100644 --- a/src/storage/realm/enum.ts +++ b/src/storage/realm/enum.ts @@ -17,7 +17,10 @@ export enum RealmSchema { Vault = 'Vault', VaultSpecs = 'VaultSpecs', VaultSigner = 'VaultSigner', - XpubDetails = 'XpubDetails', + RegistrationInfo = 'RegistrationInfo', + Signer = 'Signer', + SignerXpubs = 'SignerXpubs', + KeySpecs = 'KeySpecs', VaultPresentationData = 'VaultPresentationData', SignerPolicy = 'SignerPolicy', InheritanceKeyInfo = 'InheritanceKeyInfo', diff --git a/src/storage/realm/migrations.ts b/src/storage/realm/migrations.ts new file mode 100644 index 000000000..d7e9f94b4 --- /dev/null +++ b/src/storage/realm/migrations.ts @@ -0,0 +1,72 @@ +import { Signer } from 'src/core/wallets/interfaces/vault'; +import { RealmSchema } from './enum'; +import { getJSONFromRealmObject } from './utils'; + +export const runRealmMigrations = ({ + oldRealm, + newRealm, +}: { + oldRealm: Realm; + newRealm: Realm; +}) => { + // vault migrations for seggregated key management + if (oldRealm.schemaVersion < 63) { + const oldVaults = oldRealm.objects(RealmSchema.Vault) as any; + const newVaults = newRealm.objects(RealmSchema.Vault); + for (const objectIndex in oldVaults) { + if (oldVaults[objectIndex]?.signers?.length) { + const newVaultKeys = newVaults[objectIndex].signers; + oldVaults[objectIndex].signers.forEach((signer, index) => { + newVaultKeys[index].xfp = signer.signerId; + newVaultKeys[index].registeredVaults.push({ + vaultId: oldVaults[objectIndex].id, + registered: signer.registered, + registrationInfo: signer.deviceInfo ? JSON.stringify(signer.deviceInfo) : '', + }); + }); + } + } + oldVaults.forEach((vault) => { + if (vault.signers.length) { + vault.signers.forEach((signer) => { + signer = getJSONFromRealmObject(signer); + const signerXpubs = {}; + Object.keys(signer.xpubDetails).forEach((type) => { + if (signer.xpubDetails[type].xpub) { + if (signerXpubs[type]) { + signerXpubs[type].push({ + xpub: signer.xpubDetails[type].xpub, + xpriv: signer.xpubDetails[type].xpriv, + derivationPath: signer.xpubDetails[type].derivationPath, + }); + } else { + signerXpubs[type] = [ + { + xpub: signer.xpubDetails[type].xpub, + xpriv: signer.xpubDetails[type].xpriv, + derivationPath: signer.xpubDetails[type].derivationPath, + }, + ]; + } + } + }); + const signerObject: Signer = { + masterFingerprint: signer.masterFingerprint, + type: signer.type, + signerName: signer.signerName, + signerDescription: signer.signerDescription, + lastHealthCheck: signer.lastHealthCheck, + addedOn: signer.addedOn, + isMock: signer.isMock, + storageType: signer.storageType, + signerPolicy: signer.signerPolicy, + inheritanceKeyInfo: signer.inheritanceKeyInfo, + hidden: false, + signerXpubs, + }; + newRealm.create(RealmSchema.Signer, signerObject, Realm.UpdateMode.All); + }); + } + }); + } +}; diff --git a/src/storage/realm/realm.ts b/src/storage/realm/realm.ts index 6c4138dfd..39aa00b6a 100644 --- a/src/storage/realm/realm.ts +++ b/src/storage/realm/realm.ts @@ -2,13 +2,14 @@ import Realm from 'realm'; import { captureError } from 'src/services/sentry'; import { RealmSchema } from './enum'; import schema from './schema'; +import { runRealmMigrations } from './migrations'; export class RealmDatabase { private realm: Realm; public static file = 'keeper.realm'; - public static schemaVersion = 61; + public static schemaVersion = 66; /** * initializes/opens realm w/ appropriate configuration @@ -25,7 +26,9 @@ export class RealmDatabase { schema, schemaVersion: RealmDatabase.schemaVersion, encryptionKey: key, - migration: () => {}, + onMigration: (oldRealm, newRealm) => { + runRealmMigrations({ oldRealm, newRealm }); + }, }; this.realm = await Realm.open(realmConfig); return true; @@ -82,12 +85,13 @@ export class RealmDatabase { * creates an object corresponding to the provided schema * @param {RealmSchema} schema * @param {any} object + * @param {Realm.UpdateMode} updateMode */ - public create = (schema: RealmSchema, object: any) => { + public create = (schema: RealmSchema, object: any, updateMode) => { const realm = this.getDatabase(); try { this.writeTransaction(realm, () => { - realm.create(schema, object, Realm.UpdateMode.All); + realm.create(schema, object, updateMode); }); return true; } catch (err) { @@ -100,13 +104,14 @@ export class RealmDatabase { * creates an object corresponding to the provided schema * @param {RealmSchema} schema * @param {any[]} object + * @param {Realm.UpdateMode} updateMode */ - public createBulk = (schema: RealmSchema, objects: any[]) => { + public createBulk = (schema: RealmSchema, objects: any[], updateMode) => { const realm = this.getDatabase(); try { for (const object of objects) { this.writeTransaction(realm, () => { - realm.create(schema, object, Realm.UpdateMode.All); + realm.create(schema, object, updateMode); }); } diff --git a/src/storage/realm/schema/app.ts b/src/storage/realm/schema/app.ts index 3d2de6e26..cb3e9e27f 100644 --- a/src/storage/realm/schema/app.ts +++ b/src/storage/realm/schema/app.ts @@ -14,6 +14,7 @@ export const KeeperAppSchema: ObjectSchema = { version: 'string', subscription: RealmSchema.StoreSubscription, backup: RealmSchema.Backup, + enableAnalytics: { type: 'bool', default: false }, }, primaryKey: 'id', }; diff --git a/src/storage/realm/schema/index.ts b/src/storage/realm/schema/index.ts index dfd294d65..354d9c8d8 100644 --- a/src/storage/realm/schema/index.ts +++ b/src/storage/realm/schema/index.ts @@ -22,7 +22,10 @@ import { VaultSpecsSchema, VaultSignerSchema, SignerPolicy, - XpubDetailsSchema, + SignerXpubsSchema, + KeySpecsSchema, + SignerSchema, + RegistrationInfoSchema, InheritanceKeyInfoSchema, InheritanceConfigurationSchema, InheritancePolicySchema, @@ -54,7 +57,10 @@ export default [ WalletSpecsSchema, TransferPolicySchema, VaultSchema, - XpubDetailsSchema, + SignerXpubsSchema, + KeySpecsSchema, + SignerSchema, + RegistrationInfoSchema, VaultPresentationDataSchema, SignerPolicy, InheritanceConfigurationSchema, diff --git a/src/storage/realm/schema/vault.ts b/src/storage/realm/schema/vault.ts index 7d6c2f863..d5c1cd135 100644 --- a/src/storage/realm/schema/vault.ts +++ b/src/storage/realm/schema/vault.ts @@ -11,18 +11,6 @@ const Scheme = { }, }; -const propertyType = { - type: '{}?', - properties: { xpub: 'string', derivationPath: 'string', xpriv: 'string?' }, -}; - -const deviceInfo = { - type: '{}?', - properties: { - registerdWallet: 'string?', - }, -}; - export const SignerPolicy: ObjectSchema = { name: RealmSchema.SignerPolicy, embedded: true, @@ -96,41 +84,67 @@ export const InheritanceKeyInfoSchema: ObjectSchema = { }, }; -export const XpubDetailsSchema: ObjectSchema = { +export const KeySpecsSchema: ObjectSchema = { + name: RealmSchema.KeySpecs, + properties: { + xpub: 'string', + derivationPath: 'string', + xpriv: 'string?', + }, +}; + +export const SignerXpubsSchema: ObjectSchema = { + embedded: true, + name: RealmSchema.SignerXpubs, + properties: { + [XpubTypes.AMF]: `${RealmSchema.KeySpecs}[]`, + [XpubTypes.P2PKH]: `${RealmSchema.KeySpecs}[]`, + [XpubTypes['P2SH-P2WPKH']]: `${RealmSchema.KeySpecs}[]`, + [XpubTypes['P2SH-P2WSH']]: `${RealmSchema.KeySpecs}[]`, + [XpubTypes.P2TR]: `${RealmSchema.KeySpecs}[]`, + [XpubTypes.P2WPKH]: `${RealmSchema.KeySpecs}[]`, + [XpubTypes.P2WSH]: `${RealmSchema.KeySpecs}[]`, + }, +}; + +export const RegistrationInfoSchema: ObjectSchema = { + name: RealmSchema.RegistrationInfo, embedded: true, - name: RealmSchema.XpubDetails, properties: { - [XpubTypes.AMF]: propertyType, - [XpubTypes.P2PKH]: propertyType, - [XpubTypes['P2SH-P2WPKH']]: propertyType, - [XpubTypes['P2SH-P2WSH']]: propertyType, - [XpubTypes.P2TR]: propertyType, - [XpubTypes.P2WPKH]: propertyType, - [XpubTypes.P2WSH]: propertyType, + vaultId: 'string', + registered: 'bool', + registrationInfo: 'string?', }, }; export const VaultSignerSchema: ObjectSchema = { name: RealmSchema.VaultSigner, - embedded: true, properties: { - signerId: 'string', - type: 'string', + masterFingerprint: 'string', xpub: 'string', xpriv: 'string?', + xfp: 'string', + derivationPath: 'string', + registeredVaults: `${RealmSchema.RegistrationInfo}[]`, + }, +}; + +export const SignerSchema: ObjectSchema = { + name: RealmSchema.Signer, + primaryKey: 'masterFingerprint', + properties: { + masterFingerprint: 'string', + type: 'string', + signerXpubs: `${RealmSchema.SignerXpubs}`, signerName: 'string?', signerDescription: 'string?', lastHealthCheck: 'date', addedOn: 'date', isMock: 'bool?', - registered: { type: 'bool?', default: false }, storageType: 'string', - derivationPath: 'string', - masterFingerprint: 'string', - xpubDetails: RealmSchema.XpubDetails, signerPolicy: `${RealmSchema.SignerPolicy}?`, inheritanceKeyInfo: `${RealmSchema.InheritanceKeyInfo}?`, - deviceInfo: deviceInfo, + hidden: { type: 'bool', default: false }, }, }; diff --git a/src/storage/realm/schema/wallet.ts b/src/storage/realm/schema/wallet.ts index 335f07f96..6a9731512 100644 --- a/src/storage/realm/schema/wallet.ts +++ b/src/storage/realm/schema/wallet.ts @@ -106,7 +106,7 @@ export const WalletDerivationDetailsSchema: ObjectSchema = { embedded: true, properties: { instanceNum: 'int?', - mnemonic: 'string', + mnemonic: 'string?', bip85Config: `${RealmSchema.BIP85Config}?`, xDerivationPath: 'string', }, diff --git a/src/store/hooks/sending-utils/UseAvailableTransactionPriorities.tsx b/src/store/hooks/sending-utils/UseAvailableTransactionPriorities.tsx index dc53f904c..16c0de5b2 100644 --- a/src/store/hooks/sending-utils/UseAvailableTransactionPriorities.tsx +++ b/src/store/hooks/sending-utils/UseAvailableTransactionPriorities.tsx @@ -3,7 +3,12 @@ import { useMemo } from 'react'; import { TxPriority } from 'src/core/wallets/enums/index'; // import useSendingState from '../state-selectors/sending/UseSendingState' -const defaultTransactionPrioritiesAvailable = [TxPriority.HIGH, TxPriority.MEDIUM, TxPriority.LOW]; +const defaultTransactionPrioritiesAvailable = [ + TxPriority.HIGH, + TxPriority.MEDIUM, + TxPriority.LOW, + TxPriority.CUSTOM, +]; export default function useAvailableTransactisonPriorities() { return defaultTransactionPrioritiesAvailable; diff --git a/src/store/reducers/bhr.ts b/src/store/reducers/bhr.ts index f0e5a0a8c..a0be8eea4 100644 --- a/src/store/reducers/bhr.ts +++ b/src/store/reducers/bhr.ts @@ -27,6 +27,11 @@ const initialState: { relayWalletError: boolean; realyWalletErrorMessage: string; + relaySignersUpdateLoading: boolean; + relaySignersUpdate: boolean; + relaySignerUpdateError: boolean; + realySignersUpdateErrorMessage: string; + relayVaultUpdateLoading: boolean; relayVaultUpdate: boolean; relayVaultError: boolean; @@ -61,6 +66,10 @@ const initialState: { relayVaultError: false, realyVaultErrorMessage: null, relayVaultReoveryShellId: null, + relaySignersUpdateLoading: false, + relaySignersUpdate: false, + relaySignerUpdateError: false, + realySignersUpdateErrorMessage: null, }; const bhrSlice = createSlice({ @@ -142,6 +151,27 @@ const bhrSlice = createSlice({ state.realyWalletErrorMessage = null; }, + setRelaySignersUpdateLoading: (state, action: PayloadAction) => { + state.relaySignersUpdateLoading = action.payload; + }, + relaySignersUpdateSuccess: (state) => { + state.relaySignersUpdate = true; + state.relaySignerUpdateError = false; + state.relaySignersUpdateLoading = false; + state.realySignersUpdateErrorMessage = null; + }, + relaySignersUpdateFail: (state, action: PayloadAction) => { + state.relaySignerUpdateError = true; + state.relaySignersUpdateLoading = false; + state.realySignersUpdateErrorMessage = action.payload; + }, + resetSignersUpdateState: (state) => { + state.relaySignersUpdate = false; + state.relaySignerUpdateError = false; + state.relaySignersUpdateLoading = false; + state.realySignersUpdateErrorMessage = null; + }, + setRelayVaultUpdateLoading: (state, action: PayloadAction) => { state.relayVaultUpdateLoading = action.payload; }, @@ -189,6 +219,11 @@ export const { relayWalletUpdateFail, resetRealyWalletState, + setRelaySignersUpdateLoading, + relaySignersUpdateSuccess, + relaySignersUpdateFail, + resetSignersUpdateState, + setRelayVaultUpdateLoading, relayVaultUpdateSuccess, relayVaultUpdateFail, @@ -213,14 +248,21 @@ const bhrPersistConfig = { 'recoverBackupFailed', 'invalidPassword', 'backupWarning', + 'relayWalletUpdateLoading', 'relayWalletUpdate', 'relayWalletError', 'realyWalletErrorMessage', + 'relayVaultUpdateLoading', 'relayVaultUpdate', 'relayVaultError', 'realyVaultErrorMessage', + + 'relaySignersUpdateLoading', + 'relaySignersUpdate', + 'relaySignerUpdateError', + 'realySignersUpdateErrorMessage', ], }; diff --git a/src/store/reducers/send_and_receive.ts b/src/store/reducers/send_and_receive.ts index dd1576ef8..a692d823c 100644 --- a/src/store/reducers/send_and_receive.ts +++ b/src/store/reducers/send_and_receive.ts @@ -2,7 +2,6 @@ import { SerializedPSBTEnvelop, SigningPayload, TransactionPrerequisite, - TransactionPrerequisiteElements, } from 'src/core/wallets/interfaces/'; import { PayloadAction, createSlice } from '@reduxjs/toolkit'; @@ -24,6 +23,19 @@ export interface SendPhaseOneExecutedPayload { err?: string; } +export interface CustomFeeCalculatedPayload { + successful: boolean; + outputs: { + customTxPrerequisites: TransactionPrerequisite; + recipients?: { + address: string; + amount: number; + name?: string; + }[]; + }; + err?: string | null; +} + export interface SendPhaseTwoExecutedPayload { successful: boolean; serializedPSBTEnvelops?: SerializedPSBTEnvelop[]; @@ -34,7 +46,7 @@ export interface SendPhaseTwoExecutedPayload { export interface UpdatePSBTPayload { signedSerializedPSBT?: string; signingPayload?: SigningPayload[]; - signerId: string; + xfp: string; txHex?: string; } @@ -62,7 +74,10 @@ const initialState: { hasFailed: boolean; failedErrorMessage: string | null; isSuccessful: boolean; - outputs: { customTxPrerequisites: TransactionPrerequisiteElements } | null; + outputs: { + customTxPrerequisites: TransactionPrerequisite; + recipients: { address: string; amount: number }[]; + } | null; }; sendPhaseTwo: { inProgress: boolean; @@ -125,15 +140,15 @@ const initialState: { transactionFeeInfo: { [TxPriority.LOW]: { amount: 0, - estimatedBlocksBeforeConfirmation: 50, + estimatedBlocksBeforeConfirmation: 0, }, [TxPriority.MEDIUM]: { amount: 0, - estimatedBlocksBeforeConfirmation: 20, + estimatedBlocksBeforeConfirmation: 0, }, [TxPriority.HIGH]: { amount: 0, - estimatedBlocksBeforeConfirmation: 4, + estimatedBlocksBeforeConfirmation: 0, }, [TxPriority.CUSTOM]: { amount: 0, @@ -178,6 +193,34 @@ const sendAndReceiveSlice = createSlice({ state.transactionFeeInfo = transactionFeeInfo; }, + customFeeCalculated: (state, action: PayloadAction) => { + const { transactionFeeInfo } = state; + let customTxPrerequisites: TransactionPrerequisite; + let recipients; + const { successful, outputs, err } = action.payload; + if (successful) { + customTxPrerequisites = outputs.customTxPrerequisites; + Object.keys(customTxPrerequisites).forEach((priority) => { + transactionFeeInfo[priority].amount = customTxPrerequisites[priority].fee; + transactionFeeInfo[priority].estimatedBlocksBeforeConfirmation = + customTxPrerequisites[priority].estimatedBlocks; + }); + recipients = outputs.recipients; + } + state.customPrioritySendPhaseOne = { + ...state.customPrioritySendPhaseOne, + inProgress: false, + hasFailed: !successful, + failedErrorMessage: !successful ? err : null, + isSuccessful: successful, + outputs: { + customTxPrerequisites, + recipients, + }, + }; + state.transactionFeeInfo = transactionFeeInfo; + }, + sendPhaseTwoExecuted: (state, action: PayloadAction) => { const { successful, txid, serializedPSBTEnvelops, err } = action.payload; state.sendPhaseTwo = { @@ -191,11 +234,11 @@ const sendAndReceiveSlice = createSlice({ }, updatePSBTEnvelops: (state, action: PayloadAction) => { - const { signerId, signingPayload, signedSerializedPSBT, txHex } = action.payload; + const { xfp, signingPayload, signedSerializedPSBT, txHex } = action.payload; state.sendPhaseTwo = { ...state.sendPhaseTwo, serializedPSBTEnvelops: state.sendPhaseTwo.serializedPSBTEnvelops.map((envelop) => { - if (envelop.signerId === signerId) { + if (envelop.xfp === xfp) { envelop.serializedPSBT = signedSerializedPSBT || envelop.serializedPSBT; envelop.isSigned = signedSerializedPSBT || @@ -232,13 +275,29 @@ const sendAndReceiveSlice = createSlice({ sendPhasesReset: (state) => { state.sendMaxFee = 0; state.sendPhaseOne = initialState.sendPhaseOne; + state.customPrioritySendPhaseOne = initialState.customPrioritySendPhaseOne; state.sendPhaseTwo = initialState.sendPhaseTwo; state.sendPhaseThree = initialState.sendPhaseThree; + state.transactionFeeInfo = initialState.transactionFeeInfo; }, sendPhaseOneReset: (state) => { state.sendPhaseOne = initialState.sendPhaseOne; + state.customPrioritySendPhaseOne = initialState.customPrioritySendPhaseOne; state.sendPhaseTwo = initialState.sendPhaseTwo; state.sendPhaseThree = initialState.sendPhaseThree; + state.transactionFeeInfo = initialState.transactionFeeInfo; + }, + customPrioritySendPhaseOneReset: (state) => { + state.customPrioritySendPhaseOne = initialState.customPrioritySendPhaseOne; + state.sendPhaseTwo = initialState.sendPhaseTwo; + state.sendPhaseThree = initialState.sendPhaseThree; + state.transactionFeeInfo = { + ...state.transactionFeeInfo, + [TxPriority.CUSTOM]: { + amount: 0, + estimatedBlocksBeforeConfirmation: 0, + }, + }; }, sendPhaseTwoReset: (state) => { state.sendPhaseTwo = initialState.sendPhaseTwo; @@ -259,6 +318,7 @@ const sendAndReceiveSlice = createSlice({ export const { setSendMaxFee, sendPhaseOneExecuted, + customFeeCalculated, sendPhaseTwoExecuted, sendPhaseThreeExecuted, crossTransferExecuted, @@ -266,6 +326,7 @@ export const { crossTransferReset, sendPhasesReset, sendPhaseOneReset, + customPrioritySendPhaseOneReset, sendPhaseTwoReset, sendPhaseThreeReset, updatePSBTEnvelops, diff --git a/src/store/reducers/vaults.ts b/src/store/reducers/vaults.ts index e37f868b0..7a34fe0e4 100644 --- a/src/store/reducers/vaults.ts +++ b/src/store/reducers/vaults.ts @@ -1,10 +1,10 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; -import { Vault, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Vault } from 'src/core/wallets/interfaces/vault'; import _ from 'lodash'; import { reduxStorage } from 'src/storage'; import persistReducer from 'redux-persist/es/persistReducer'; -import { ADD_NEW_VAULT, ADD_SIGINING_DEVICE } from '../sagaActions/vaults'; +import { ADD_NEW_VAULT } from '../sagaActions/vaults'; export interface VaultCreationPayload { hasNewVaultGenerationSucceeded: boolean; @@ -24,7 +24,6 @@ export interface VaultMigrationCompletionPayload { } export type VaultState = { - signers: VaultSigner[]; isGeneratingNewVault: boolean; hasNewVaultGenerationSucceeded: boolean; hasNewVaultGenerationFailed: boolean; @@ -41,13 +40,11 @@ export type VaultState = { }; export type SignerUpdatePayload = { - signer: VaultSigner; key: string; value: any; }; const initialState: VaultState = { - signers: [], isGeneratingNewVault: false, hasNewVaultGenerationSucceeded: false, hasNewVaultGenerationFailed: false, @@ -67,58 +64,6 @@ const vaultSlice = createSlice({ name: 'vault', initialState, reducers: { - addSigningDevice: (state, action: PayloadAction) => { - const newSigners = action.payload.filter((signer) => signer && signer.signerId); - if (newSigners.length === 0) { - return state; - } - let updatedSigners = [...state.signers]; - if (newSigners.length === 1) { - const newSigner = newSigners[0]; - const existingSignerIndex = updatedSigners.findIndex( - (signer) => signer.masterFingerprint === newSigner.masterFingerprint - ); - if (existingSignerIndex !== -1) { - const existingSigner = updatedSigners[existingSignerIndex]; - const combinedSigner: VaultSigner = { - ...newSigner, - lastHealthCheck: existingSigner.lastHealthCheck, - signerDescription: existingSigner.signerDescription, - xpubDetails: { ...existingSigner.xpubDetails, ...newSigner.xpubDetails }, - }; - - updatedSigners[existingSignerIndex] = combinedSigner; - } else { - updatedSigners.push(newSigner); - } - } else { - updatedSigners.push(...newSigners); - } - updatedSigners = _.uniqBy(updatedSigners, 'signerId'); - return { ...state, signers: updatedSigners }; - }, - removeSigningDevice: (state, action: PayloadAction) => { - const signerToRemove = - action.payload && action.payload.masterFingerprint ? action.payload : null; - if (signerToRemove) { - state.signers = state.signers.filter( - (signer) => signer.masterFingerprint !== signerToRemove.masterFingerprint - ); - } - }, - clearSigningDevice: (state) => { - state.signers = []; - }, - updateSigningDevice: (state, action: PayloadAction) => { - const { signer, key, value } = action.payload; - state.signers = state.signers.map((item) => { - if (item && item.signerId === signer.signerId) { - item[key] = value; - return item; - } - return item; - }); - }, vaultCreated: (state, action: PayloadAction) => { const { hasNewVaultGenerationFailed = false, @@ -173,9 +118,6 @@ const vaultSlice = createSlice({ }, }, extraReducers: (builder) => { - builder.addCase(ADD_SIGINING_DEVICE, (state) => { - state.isGeneratingNewVault = true; - }); builder.addCase(ADD_NEW_VAULT, (state) => { state.isGeneratingNewVault = false; }); @@ -183,16 +125,12 @@ const vaultSlice = createSlice({ }); export const { - addSigningDevice, vaultCreated, initiateVaultMigration, vaultMigrationCompleted, - removeSigningDevice, setIntroModal, setSdIntroModal, setWhirlpoolIntro, - updateSigningDevice, - clearSigningDevice, resetVaultMigration, setTempShellId, setBackupBSMSForIKS, diff --git a/src/store/sagaActions/bhr.ts b/src/store/sagaActions/bhr.ts index 980dc99b4..624ad2eb7 100644 --- a/src/store/sagaActions/bhr.ts +++ b/src/store/sagaActions/bhr.ts @@ -1,5 +1,5 @@ import { BackupHistory } from 'src/models/enums/BHR'; -import { Vault, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Signer, Vault } from 'src/core/wallets/interfaces/vault'; export const UPDATE_APP_IMAGE = 'UPDATE_APP_IMAGE'; export const GET_APP_IMAGE = 'GET_APP_IMAGE'; @@ -61,7 +61,7 @@ export const recoverBackup = (password: string, encData: string) => ({ // HealthChecks -export const healthCheckSigner = (signers: VaultSigner[]) => ({ +export const healthCheckSigner = (signers: Signer[]) => ({ type: UPADTE_HEALTH_CHECK_SIGNER, payload: { signers, diff --git a/src/store/sagaActions/send_and_receive.ts b/src/store/sagaActions/send_and_receive.ts index 7dd9acab0..568af8920 100644 --- a/src/store/sagaActions/send_and_receive.ts +++ b/src/store/sagaActions/send_and_receive.ts @@ -1,4 +1,4 @@ -import { TransactionPrerequisiteElements, UTXO } from 'src/core/wallets/interfaces'; +import { UTXO } from 'src/core/wallets/interfaces'; import { Action } from 'redux'; import { Recipient } from 'src/models/interfaces/Recipient'; @@ -32,7 +32,6 @@ export const CALCULATE_SEND_MAX_FEE = 'CALCULATE_SEND_MAX_FEE'; export const CLEAR_SEND_MAX_FEE = 'CLEAR_SEND_MAX_FEE'; export const SEND_MAX_FEE_CALCULATED = 'SEND_MAX_FEE_CALCULATED'; export const CALCULATE_CUSTOM_FEE = 'CALCULATE_CUSTOM_FEE'; -export const CUSTOM_FEE_CALCULATED = 'CUSTOM_FEE_CALCULATED'; export const CUSTOM_SEND_MAX_CALCULATED = 'CUSTOM_SEND_MAX_CALCULATED'; export const SEND_TX_NOTIFICATION = 'SEND_TX_NOTIFICATION'; @@ -274,6 +273,7 @@ export interface CalculateCustomFeeAction extends Action { }[]; feePerByte: string; customEstimatedBlocks: string; + selectedUTXOs?: UTXO[]; }; } @@ -285,29 +285,11 @@ export const calculateCustomFee = (payload: { }[]; feePerByte: string; customEstimatedBlocks: string; + selectedUTXOs?: UTXO[]; }): CalculateCustomFeeAction => ({ type: CALCULATE_CUSTOM_FEE, payload, }); - -export interface CustomFeeCalculatedAction extends Action { - type: typeof CUSTOM_FEE_CALCULATED; - payload: { - successful: boolean; - carryOver?: { customTxPrerequisites: TransactionPrerequisiteElements }; - err?: string | null; - }; -} - -export const customFeeCalculated = (payload: { - successful: boolean; - carryOver?: { customTxPrerequisites: TransactionPrerequisiteElements }; - err?: string | null; -}): CustomFeeCalculatedAction => ({ - type: CUSTOM_FEE_CALCULATED, - payload, -}); - export interface CustomSendMaxCalculatedAction extends Action { type: typeof CUSTOM_SEND_MAX_CALCULATED; payload: { diff --git a/src/store/sagaActions/vaults.ts b/src/store/sagaActions/vaults.ts index 107289efd..55e266e7b 100644 --- a/src/store/sagaActions/vaults.ts +++ b/src/store/sagaActions/vaults.ts @@ -1,4 +1,4 @@ -import { Vault, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Signer, Vault } from 'src/core/wallets/interfaces/vault'; import { NewVaultInfo } from '../sagas/wallets'; // types and action creators: dispatched by components and sagas @@ -6,7 +6,6 @@ export const ADD_NEW_VAULT = 'ADD_NEW_VAULT'; export const ADD_SIGINING_DEVICE = 'ADD_SIGINING_DEVICE'; export const MIGRATE_VAULT = 'MIGRATE_VAULT'; export const FINALISE_VAULT_MIGRATION = 'FINALISE_VAULT_MIGRATION'; -export const FINALIZE_IK_SETUP = 'FINALIZE_IK_SETUP'; export const addNewVault = (payload: { newVaultInfo: NewVaultInfo; @@ -19,9 +18,9 @@ export const addNewVault = (payload: { payload, }); -export const addSigningDevice = (payload: VaultSigner) => ({ +export const addSigningDevice = (signers: Signer[]) => ({ type: ADD_SIGINING_DEVICE, - payload, + payload: { signers }, }); export const migrateVault = (newVaultInfo: NewVaultInfo, vaultShellId: string) => ({ @@ -33,8 +32,3 @@ export const finaliseVaultMigration = (payload: string) => ({ type: FINALISE_VAULT_MIGRATION, payload: { vaultId: payload }, }); - -export const finaliseIKSetup = (vault: Vault) => ({ - type: FINALIZE_IK_SETUP, - payload: { vault }, -}); diff --git a/src/store/sagaActions/wallets.ts b/src/store/sagaActions/wallets.ts index fa6356b47..00107fd87 100644 --- a/src/store/sagaActions/wallets.ts +++ b/src/store/sagaActions/wallets.ts @@ -1,4 +1,4 @@ -import { Vault, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Signer, Vault, VaultSigner } from 'src/core/wallets/interfaces/vault'; import { VisibilityType } from 'src/core/wallets/enums'; import { Wallet } from 'src/core/wallets/interfaces/wallet'; import { SignerException, SignerRestriction } from 'src/services/interfaces'; @@ -30,7 +30,9 @@ export const RESET_TWO_FA_LOADER = 'RESET_TWO_FA_LOADER'; export const TEST_SATS_RECIEVE = 'TEST_SATS_RECIEVE'; export const UAI_VAULT_TO_WALLET = 'UAI_VAULT_TO_WALLET'; export const UPDATE_WALLET_DETAILS = 'UPDATE_WALLET_DETAILS'; +export const UPDATE_VAULT_DETAILS = 'UPDATE_VAULT_DETAILS'; export const UPDATE_SIGNER_DETAILS = 'UPDATE_SIGNER_DETAILS'; +export const UPDATE_KEY_DETAILS = 'UPDATE_KEY_DETAILS'; export const ADD_WHIRLPOOL_WALLETS = 'ADD_WHIRLPOOL_WALLETS'; export const ADD_WHIRLPOOL_WALLETS_LOCAL = 'ADD_WHIRLPOOL_WALLETS_LOCAL'; export const UPDATE_WALLET_PATH_PURPOSE_DETAILS = 'UPDATE_WALLET_PATH_PURPOSE_DETAILS'; @@ -112,7 +114,8 @@ export const incrementAddressIndex = ( }); export const updateSignerPolicy = ( - signer: VaultSigner, + signer: Signer, + signingKey: VaultSigner, updates: { restrictions?: SignerRestriction; exceptions?: SignerException; @@ -122,6 +125,7 @@ export const updateSignerPolicy = ( type: UPDATE_SIGNER_POLICY, payload: { signer, + signingKey, updates, verificationToken, }, @@ -300,6 +304,20 @@ export const updateWalletDetails = ( details, }, }); + +export const updateVaultDetails = ( + vault: Vault, + details: { + name: string; + description: string; + } +) => ({ + type: UPDATE_VAULT_DETAILS, + payload: { + vault, + details, + }, +}); export const updateWalletPathAndPurposeDetails = ( wallet: Wallet, details: { @@ -314,7 +332,7 @@ export const updateWalletPathAndPurposeDetails = ( }, }); -export const updateSignerDetails = (signer: VaultSigner, key: string, value: any) => ({ +export const updateSignerDetails = (signer: Signer, key: string, value: any) => ({ type: UPDATE_SIGNER_DETAILS, payload: { signer, @@ -322,3 +340,12 @@ export const updateSignerDetails = (signer: VaultSigner, key: string, value: any value, }, }); + +export const updateKeyDetails = (signer: VaultSigner, key: string, value: any) => ({ + type: UPDATE_KEY_DETAILS, + payload: { + signer, + key, + value, + }, +}); diff --git a/src/store/sagas/bhr.ts b/src/store/sagas/bhr.ts index 8709dc45d..f910a44ff 100644 --- a/src/store/sagas/bhr.ts +++ b/src/store/sagas/bhr.ts @@ -15,7 +15,7 @@ import DeviceInfo from 'react-native-device-info'; import { KeeperApp } from 'src/models/interfaces/KeeperApp'; import { RealmSchema } from 'src/storage/realm/enum'; import Relay from 'src/services/operations/Relay'; -import { Vault, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { Signer, Vault, VaultSigner } from 'src/core/wallets/interfaces/vault'; import { captureError } from 'src/services/sentry'; import crypto from 'crypto'; import dbManager from 'src/storage/realm/dbManager'; @@ -60,27 +60,49 @@ import { generateSignerFromMetaData } from 'src/hardware'; import { SignerStorage, SignerType, VaultType, XpubTypes } from 'src/core/wallets/enums'; import { getCosignerDetails } from 'src/core/wallets/factories/WalletFactory'; import { NewVaultInfo, addNewVaultWorker } from './wallets'; +import { getJSONFromRealmObject } from 'src/storage/realm/utils'; +import { KEY_MANAGEMENT_VERSION } from './upgrade'; -export function* updateAppImageWorker({ payload }) { - const { wallets } = payload; +export function* updateAppImageWorker({ + payload, +}: { + payload: { + wallets?: Wallet[]; + signers?: Signer[]; + }; +}) { + const { wallets, signers } = payload; const { primarySeed, id, publicId, subscription, networkType, version }: KeeperApp = yield call( dbManager.getObjectByIndex, RealmSchema.KeeperApp ); const walletObject = {}; + const signersObjects = {}; const encryptionKey = generateEncryptionKey(primarySeed); if (wallets) { for (const wallet of wallets) { const encrytedWallet = encrypt(encryptionKey, JSON.stringify(wallet)); walletObject[wallet.id] = encrytedWallet; } + } else if (signers) { + for (const signer of signers) { + const encrytedWallet = encrypt(encryptionKey, JSON.stringify(signer)); + signersObjects[signer.masterFingerprint] = encrytedWallet; + } } else { + //update all wallets and signers const wallets: Wallet[] = yield call(dbManager.getCollection, RealmSchema.Wallet); for (const index in wallets) { const wallet = wallets[index]; const encrytedWallet = encrypt(encryptionKey, JSON.stringify(wallet)); walletObject[wallet.id] = encrytedWallet; } + const signers: Signer[] = yield call(dbManager.getCollection, RealmSchema.Signer); + for (const index in signers) { + const signer = signers[index]; + const encrytedSigner = encrypt(encryptionKey, JSON.stringify(signer)); + signersObjects[signer.masterFingerprint] = encrytedSigner; + } } const nodes: NodeDetail[] = yield call(dbManager.getCollection, RealmSchema.NodeConnect); @@ -93,11 +115,14 @@ export function* updateAppImageWorker({ payload }) { nodesToUpdate.push(encrytedNode); } } + + // API call to Relay to do modular updates try { const response = yield call(Relay.updateAppImage, { appId: id, publicId, walletObject, + signersObjects, networkType, subscription: JSON.stringify(subscription), version, @@ -129,11 +154,12 @@ export function* updateVaultImageWorker({ const vaultEncrypted = encrypt(encryptionKey, JSON.stringify(vault)); if (isUpdate) { - Relay.updateVaultImage({ + const response = Relay.updateVaultImage({ isUpdate, vaultId: vault.id, vault: vaultEncrypted, }); + return response; } const signersData: Array<{ @@ -142,27 +168,14 @@ export function* updateVaultImageWorker({ }> = []; const signerIdXpubMap = {}; for (const signer of vault.signers) { - signerIdXpubMap[signer.signerId] = signer.xpub; + signerIdXpubMap[signer.xfp] = signer.xpub; signersData.push({ - signerId: signer.signerId, + signerId: signer.xfp, xfpHash: hash256(signer.masterFingerprint), }); } - // updating signerIdXpubMap if the signer was created through automated mock flow - // const signerIdsToFilter = []; - // for (const signer of vault.signers) { - // if (signer.amfData && signer.amfData.xpub) { - // signerIdXpubMap[signer.amfData.signerId] = signer.amfData.xpub; - // signersData.push({ - // signerId: signer.amfData.signerId, - // xfpHash: hash256(signer.xpubInfo.xfp), - // }); - // signerIdsToFilter.push(signer.signerId); - // } - // } - // signersData = signersData.filter((signer) => !signerIdsToFilter.includes(signer.signerId)); - - // TO-DO to be removed + + // TODO to be removed const subscriptionStrings = JSON.stringify(subscription); try { @@ -245,7 +258,7 @@ function* getAppImageWorker({ payload }) { const primarySeed = bip39.mnemonicToSeedSync(primaryMnemonic); const appID = crypto.createHash('sha256').update(primarySeed).digest('hex'); const encryptionKey = generateEncryptionKey(primarySeed.toString('hex')); - const { appImage, vaultImage, subscription, UTXOinfos, labels } = yield call( + const { appImage, subscription, UTXOinfos, vaultImage, labels, allVaultImages } = yield call( Relay.getAppImage, appID ); @@ -273,9 +286,10 @@ function* getAppImageWorker({ payload }) { appID, subscription, appImage, - vaultImage, + allVaultImages, UTXOinfos, - labels + labels, + previousVersion ); } else { const plebSubscription = { @@ -293,9 +307,10 @@ function* getAppImageWorker({ payload }) { appID, plebSubscription, appImage, - vaultImage, + allVaultImages, UTXOinfos, - labels + labels, + previousVersion ); } } @@ -315,9 +330,10 @@ function* recoverApp( appID, subscription, appImage, - vaultImage, + allVaultImages, UTXOinfos, - labels + labels, + previousVersion ) { const entropy = yield call( BIP85.bip39MnemonicToEntropy, @@ -365,10 +381,10 @@ function* recoverApp( if (parsedText) { const signers: VaultSigner[] = []; parsedText.signersDetails.forEach((config) => { - const signer = generateSignerFromMetaData({ + const { signer } = generateSignerFromMetaData({ xpub: config.xpub, derivationPath: config.path, - xfp: config.masterFingerprint, + masterFingerprint: config.masterFingerprint, signerType: SignerType.KEEPER, storageType: SignerStorage.WARM, isMultisig: config.isMultisig, @@ -376,7 +392,7 @@ function* recoverApp( signers.push(signer); }); - const { xpubDetails } = getCosignerDetails(decrytpedWallet, app.id); + const { xpubDetails } = getCosignerDetails(decrytpedWallet); const isValidDescriptor = signers.find( (signer) => signer.xpub === xpubDetails[XpubTypes.P2WSH].xpub ); @@ -403,11 +419,77 @@ function* recoverApp( } } + //Signers recreatin + if (appImage.signers) { + for (const [key, value] of Object.entries(appImage.signers)) { + const decrytpedSigner: Signer = JSON.parse(decrypt(encryptionKey, value)); + yield call(dbManager.createObject, RealmSchema.Signer, decrytpedSigner); + } + } + // Vault recreation - if (vaultImage) { - const vault = JSON.parse(decrypt(encryptionKey, vaultImage.vault)); + if (allVaultImages.length > 0) { + for (const vaultImage of allVaultImages) { + const vault = JSON.parse(decrypt(encryptionKey, vaultImage.vault)); + + if (semver.lt(previousVersion, KEY_MANAGEMENT_VERSION)) { + if (vault?.signers?.length) { + vault.signers.forEach((signer, index) => { + signer.xfp = signer.signerId; + signer.registeredVaults = [ + { + vaultId: vault.id, + registered: signer.registered, + registrationInfo: signer.deviceInfo ? JSON.stringify(signer.deviceInfo) : '', + }, + ]; + }); + } - yield call(dbManager.createObject, RealmSchema.Vault, vault); + if (vault.signers.length) { + for (const signer of vault.signers) { + const signerXpubs = {}; + Object.keys(signer.xpubDetails).forEach((type) => { + if (signer.xpubDetails[type].xpub) { + if (signerXpubs[type]) { + signerXpubs[type].push({ + xpub: signer.xpubDetails[type].xpub, + xpriv: signer.xpubDetails[type].xpriv, + derivationPath: signer.xpubDetails[type].derivationPath, + }); + } else { + signerXpubs[type] = [ + { + xpub: signer.xpubDetails[type].xpub, + xpriv: signer.xpubDetails[type].xpriv, + derivationPath: signer.xpubDetails[type].derivationPath, + }, + ]; + } + } + }); + const signerObject = { + masterFingerprint: signer.masterFingerprint, + type: signer.type, + signerName: signer.signerName, + signerDescription: signer.signerDescription, + lastHealthCheck: signer.lastHealthCheck, + addedOn: signer.addedOn, + isMock: signer.isMock, + storageType: signer.storageType, + signerPolicy: signer.signerPolicy, + inheritanceKeyInfo: signer.inheritanceKeyInfo, + hidden: false, + signerXpubs, + }; + yield call(dbManager.createObject, RealmSchema.Signer, signerObject); + } + } + + yield call(dbManager.createObject, RealmSchema.Vault, vault); + } + yield call(dbManager.createObject, RealmSchema.Vault, vault); + } } // UTXOinfo restore diff --git a/src/store/sagas/index.ts b/src/store/sagas/index.ts index 0e3805637..e659622a8 100644 --- a/src/store/sagas/index.ts +++ b/src/store/sagas/index.ts @@ -14,11 +14,12 @@ import { updateWalletDetailWatcher, updateWalletSettingsWatcher, updateSignerDetails, + updateKeyDetails, updateWalletsPropertyWatcher, addWhirlpoolWalletsWatcher, addWhirlpoolWalletsLocalWatcher, updateWalletPathAndPuposeDetailWatcher, - finaliseIKSetupWatcher, + updateVaultDetailsWatcher, } from './wallets'; import { addUaiStackWatcher, @@ -98,8 +99,9 @@ const rootSaga = function* () { addSigningDeviceWatcher, migrateVaultWatcher, finaliseVaultMigrationWatcher, - finaliseIKSetupWatcher, + updateVaultDetailsWatcher, updateSignerDetails, + updateKeyDetails, // send and receive fetchExchangeRatesWatcher, diff --git a/src/store/sagas/login.ts b/src/store/sagas/login.ts index 2611db0d9..063dc45fa 100644 --- a/src/store/sagas/login.ts +++ b/src/store/sagas/login.ts @@ -174,7 +174,6 @@ function* credentialsAuthWorker({ payload }) { uaiChecks([ uaiType.SIGNING_DEVICES_HEALTH_CHECK, uaiType.SECURE_VAULT, - uaiType.VAULT_MIGRATION, uaiType.DEFAULT, uaiType.VAULT_TRANSFER, ]) diff --git a/src/store/sagas/restoreUpgrade.ts b/src/store/sagas/restoreUpgrade.ts index efb218dc1..0946d8d1b 100644 --- a/src/store/sagas/restoreUpgrade.ts +++ b/src/store/sagas/restoreUpgrade.ts @@ -3,7 +3,6 @@ import semver from 'semver'; import { decrypt, encrypt } from 'src/services/operations/encryption'; import Relay from 'src/services/operations/Relay'; import { Vault } from 'src/core/wallets/interfaces/vault'; -import { ADDITION_OF_VAULTSHELL_VERSION } from './upgrade'; export function* applyRestoreSequence({ previousVersion, @@ -19,8 +18,6 @@ export function* applyRestoreSequence({ encryptionKey: string; }) { console.log(`applying restore upgarde sequence - from: ${previousVersion} to ${newVersion}`); - if (semver.lte(previousVersion, ADDITION_OF_VAULTSHELL_VERSION)) - yield call(additionOfVaultShellId, vaultImage, appImage, encryptionKey); yield call(updateVersion, appImage, newVersion); } @@ -46,7 +43,7 @@ function* additionOfVaultShellId(vaultImage, appImage, encryptionKey) { } //while restoring, updating the version on the appImage -//TO-DO +//TODO function* updateVersion(appImage, newVersion) { try { yield call(Relay.updateAppImage, { diff --git a/src/store/sagas/send_and_receive.ts b/src/store/sagas/send_and_receive.ts index 30e0c1ba7..f1fcabbd9 100644 --- a/src/store/sagas/send_and_receive.ts +++ b/src/store/sagas/send_and_receive.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-plusplus */ -/* eslint-disable no-restricted-syntax */ import { AverageTxFeesByNetwork, SerializedPSBTEnvelop } from 'src/core/wallets/interfaces'; import { EntityKind, LabelRefType, TxPriority } from 'src/core/wallets/enums'; import { call, put, select } from 'redux-saga/effects'; @@ -12,12 +10,13 @@ import WalletUtilities from 'src/core/wallets/operations/utils'; import _ from 'lodash'; import idx from 'idx'; import { TransferType } from 'src/models/enums/TransferType'; -import { +import ElectrumClient, { + ELECTRUM_CLIENT, ELECTRUM_NOT_CONNECTED_ERR, ELECTRUM_NOT_CONNECTED_ERR_TOR, } from 'src/services/electrum/client'; -import { createWatcher } from '../utilities'; import dbManager from 'src/storage/realm/dbManager'; +import { createWatcher } from '../utilities'; import { SendPhaseOneExecutedPayload, sendPhaseOneExecuted, @@ -27,6 +26,7 @@ import { crossTransferExecuted, crossTransferFailed, sendPhaseTwoStarted, + customFeeCalculated, } from '../reducers/send_and_receive'; import { setAverageTxFee, setExchangeRates } from '../reducers/network'; import { @@ -44,11 +44,11 @@ import { SendPhaseOneAction, SendPhaseThreeAction, SendPhaseTwoAction, - customFeeCalculated, feeIntelMissing, } from '../sagaActions/send_and_receive'; import { addLabelsWorker } from './utxos'; import { setElectrumNotConnectedErr } from '../reducers/login'; +import { connectToNodeWorker } from './network'; export function* fetchFeeRatesWorker() { try { @@ -125,23 +125,40 @@ function* sendPhaseOneWorker({ payload }: SendPhaseOneAction) { export const sendPhaseOneWatcher = createWatcher(sendPhaseOneWorker, SEND_PHASE_ONE); function* sendPhaseTwoWorker({ payload }: SendPhaseTwoAction) { + if (!ELECTRUM_CLIENT.isClientConnected) { + ElectrumClient.resetCurrentPeerIndex(); + yield call(connectToNodeWorker); + } yield put(sendPhaseTwoStarted()); const sendPhaseOneResults: SendPhaseOneExecutedPayload = yield select( (state) => state.sendAndReceive.sendPhaseOne ); + const customSendPhaseOneResults = yield select( + (state) => state.sendAndReceive.customPrioritySendPhaseOne + ); + const { wallet, txnPriority, note, label, transferType } = payload; const txPrerequisites = _.cloneDeep(idx(sendPhaseOneResults, (_) => _.outputs.txPrerequisites)); // cloning object(mutable) as reducer states are immutable + const customTxPrerequisites = _.cloneDeep( + idx(customSendPhaseOneResults, (_) => _.outputs.customTxPrerequisites) + ); + const recipients = idx(sendPhaseOneResults, (_) => _.outputs.recipients); - const network = WalletUtilities.getNetworkByType(wallet.networkType); + const signerMap = {}; + if (wallet.entityKind === EntityKind.VAULT) { + dbManager + .getCollection(RealmSchema.Signer) + .forEach((signer) => (signerMap[signer.masterFingerprint as string] = signer)); + } try { const { txid, serializedPSBTEnvelops, finalOutputs } = yield call( WalletOperations.transferST2, wallet, txPrerequisites, txnPriority, - network, - recipients - // customTxPrerequisites + recipients, + customTxPrerequisites, + signerMap ); switch (wallet.entityKind) { @@ -170,8 +187,9 @@ function* sendPhaseTwoWorker({ payload }: SendPhaseTwoAction) { break; case EntityKind.VAULT: - if (!serializedPSBTEnvelops.length) + if (!serializedPSBTEnvelops.length) { throw new Error('Send failed: unable to generate serializedPSBTEnvelop'); + } yield put( sendPhaseTwoExecuted({ successful: true, @@ -181,7 +199,7 @@ function* sendPhaseTwoWorker({ payload }: SendPhaseTwoAction) { break; default: - throw new Error('Invalid Entity: not a Vault/Wallet'); + throw new Error('Invalid Entity: not a vault/Wallet'); } if (wallet.entityKind === EntityKind.WALLET) { const enabledTransferTypes = [TransferType.WALLET_TO_VAULT]; @@ -202,8 +220,9 @@ function* sendPhaseTwoWorker({ payload }: SendPhaseTwoAction) { } } } catch (err) { - if ([ELECTRUM_NOT_CONNECTED_ERR, ELECTRUM_NOT_CONNECTED_ERR_TOR].includes(err?.message)) + if ([ELECTRUM_NOT_CONNECTED_ERR, ELECTRUM_NOT_CONNECTED_ERR_TOR].includes(err?.message)) { yield put(setElectrumNotConnectedErr(err?.message)); + } yield put( sendPhaseTwoExecuted({ @@ -220,10 +239,18 @@ function* sendPhaseThreeWorker({ payload }: SendPhaseThreeAction) { const sendPhaseOneResults: SendPhaseOneExecutedPayload = yield select( (state) => state.sendAndReceive.sendPhaseOne ); + const customSendPhaseOneResults = yield select( + (state) => state.sendAndReceive.customPrioritySendPhaseOne + ); const serializedPSBTEnvelops: SerializedPSBTEnvelop[] = yield select( (state) => state.sendAndReceive.sendPhaseTwo.serializedPSBTEnvelops ); + const txPrerequisites = _.cloneDeep(idx(sendPhaseOneResults, (_) => _.outputs.txPrerequisites)); // cloning object(mutable) as reducer states are immutable + const customTxPrerequisites = _.cloneDeep( + idx(customSendPhaseOneResults, (_) => _.outputs.customTxPrerequisites) + ); + const recipients = idx(sendPhaseOneResults, (_) => _.outputs.recipients); const { wallet, txnPriority, note, label } = payload; try { @@ -238,10 +265,11 @@ function* sendPhaseThreeWorker({ payload }: SendPhaseThreeAction) { txHex = serializedPSBTEnvelop.txHex; // txHex is given out by COLDCARD, KEYSTONE and TREZOR post signing } } - if (availableSignatures < threshold) + if (availableSignatures < threshold) { throw new Error( `Insufficient signatures, required:${threshold} provided:${availableSignatures}` ); + } const { txid, finalOutputs } = yield call( WalletOperations.transferST3, @@ -249,6 +277,7 @@ function* sendPhaseThreeWorker({ payload }: SendPhaseThreeAction) { serializedPSBTEnvelops, txPrerequisites, txnPriority, + customTxPrerequisites, txHex ); if (!txid) throw new Error('Send failed: unable to generate txid using the signed PSBT'); @@ -286,8 +315,9 @@ function* sendPhaseThreeWorker({ payload }: SendPhaseThreeAction) { }); } } catch (err) { - if ([ELECTRUM_NOT_CONNECTED_ERR, ELECTRUM_NOT_CONNECTED_ERR_TOR].includes(err?.message)) + if ([ELECTRUM_NOT_CONNECTED_ERR, ELECTRUM_NOT_CONNECTED_ERR_TOR].includes(err?.message)) { yield put(setElectrumNotConnectedErr(err?.message)); + } yield put( sendPhaseThreeExecuted({ @@ -331,13 +361,11 @@ function* corssTransferWorker({ payload }: CrossTransferAction) { ); if (txPrerequisites) { - const network = WalletUtilities.getNetworkByType(sender.networkType); const { txid } = yield call( WalletOperations.transferST2, sender, txPrerequisites, TxPriority.LOW, - network, recipients ); @@ -385,12 +413,11 @@ export const calculateSendMaxFeeWatcher = createWatcher( ); function* calculateCustomFee({ payload }: CalculateCustomFeeAction) { - // feerate should be > minimum relay feerate(default: 1000 satoshis per kB or 1 sat/byte). if (parseInt(payload.feePerByte, 10) < 1) { yield put( customFeeCalculated({ successful: false, - carryOver: { + outputs: { customTxPrerequisites: null, }, err: 'Custom fee minimum: 1 sat/byte', @@ -399,12 +426,14 @@ function* calculateCustomFee({ payload }: CalculateCustomFeeAction) { return; } - const { wallet, recipients, feePerByte, customEstimatedBlocks } = payload; - const sending: any = {}; - const txPrerequisites = idx(sending, (_) => _.sendST1.carryOver.txPrerequisites); + const { wallet, recipients, feePerByte, customEstimatedBlocks, selectedUTXOs } = payload; + const sendPhaseOneResults: SendPhaseOneExecutedPayload = yield select( + (state) => state.sendAndReceive.sendPhaseOne + ); + const txPrerequisites = idx(sendPhaseOneResults, (_) => _.outputs.txPrerequisites); let outputs; - if (sending.feeIntelMissing) { + if (!txPrerequisites) { // process recipients & generate outputs(normally handled by transfer ST1 saga) const outputsArray = []; for (const recipient of recipients) { @@ -414,24 +443,24 @@ function* calculateCustomFee({ payload }: CalculateCustomFeeAction) { }); } outputs = outputsArray; - } else { - if (!txPrerequisites) throw new Error('ST1 carry-over missing'); - outputs = txPrerequisites[TxPriority.LOW].outputs.filter((output) => output.address); - } + } else outputs = txPrerequisites[TxPriority.LOW].outputs.filter((output) => output.address); const customTxPrerequisites = WalletOperations.prepareCustomTransactionPrerequisites( wallet, outputs, - parseInt(feePerByte, 10) + parseInt(feePerByte, 10), + selectedUTXOs ); - if (customTxPrerequisites.inputs) { - customTxPrerequisites.estimatedBlocks = parseInt(customEstimatedBlocks, 10); + if (customTxPrerequisites[TxPriority.CUSTOM]?.inputs) { + customTxPrerequisites[TxPriority.CUSTOM].estimatedBlocks = parseInt(customEstimatedBlocks, 10); + yield put( customFeeCalculated({ successful: true, - carryOver: { + outputs: { customTxPrerequisites, + recipients, }, err: null, }) @@ -444,7 +473,7 @@ function* calculateCustomFee({ payload }: CalculateCustomFeeAction) { yield put( customFeeCalculated({ successful: false, - carryOver: { + outputs: { customTxPrerequisites: null, }, err: `Insufficient balance to pay: amount ${totalAmount} + fee(${customTxPrerequisites.fee}) at ${feePerByte} sats/byte`, diff --git a/src/store/sagas/storage.ts b/src/store/sagas/storage.ts index d29c22262..239ec3870 100644 --- a/src/store/sagas/storage.ts +++ b/src/store/sagas/storage.ts @@ -23,6 +23,8 @@ import { resetRealyWalletState } from '../reducers/bhr'; export const defaultTransferPolicyThreshold = config.NETWORK_TYPE === NetworkType.MAINNET ? 1000000 : 5000; +export const maxTransferPolicyThreshold = 1e11; + export function* setupKeeperAppWorker({ payload }) { try { const { appName, fcmToken }: { appName: string; fcmToken: string } = payload; @@ -76,7 +78,7 @@ export function* setupKeeperAppWorker({ payload }) { walletType: WalletType.DEFAULT, walletDetails: { name: 'Wallet 1', - description: 'Single-sig bitcoin wallet', + description: '', transferPolicy: { id: uuidv4(), threshold: defaultTransferPolicyThreshold, @@ -138,7 +140,7 @@ function* setupKeeperVaultRecoveryAppWorker({ payload }) { walletType: WalletType.DEFAULT, walletDetails: { name: 'Mobile Wallet', - description: 'Single-sig bitcoin wallet', + description: '', transferPolicy: { id: uuidv4(), threshold: defaultTransferPolicyThreshold, diff --git a/src/store/sagas/uai.ts b/src/store/sagas/uai.ts index d3ac1314f..08bd84d76 100644 --- a/src/store/sagas/uai.ts +++ b/src/store/sagas/uai.ts @@ -8,6 +8,8 @@ import { v4 as uuidv4 } from 'uuid'; import { Wallet } from 'src/core/wallets/interfaces/wallet'; import { KeeperApp } from 'src/models/interfaces/KeeperApp'; import { SUBSCRIPTION_SCHEME_MAP } from 'src/hooks/usePlan'; +import { isTestnet } from 'src/constants/Bitcoin'; +import { EntityKind, VaultType } from 'src/core/wallets/enums'; import { setRefreshUai } from '../reducers/uai'; import { addToUaiStack, @@ -19,8 +21,6 @@ import { UAI_CHECKS, } from '../sagaActions/uai'; import { createWatcher } from '../utilities'; -import { isTestnet } from 'src/constants/Bitcoin'; -import { EntityKind, VaultType } from 'src/core/wallets/enums'; const healthCheckRemider = (signer: VaultSigner) => { const today = new Date(); @@ -119,7 +119,7 @@ function* uaiChecksWorker({ payload }) { if (!secureVaultUai) { yield put( addToUaiStack({ - title: 'Add a signing device to activate your Vault', + title: 'Add a signer to activate your Vault', isDisplay: false, uaiType: uaiType.SECURE_VAULT, prirority: 100, @@ -152,7 +152,7 @@ function* uaiChecksWorker({ payload }) { } else { yield put( addToUaiStack({ - title: `Transfer fund to Vault from ${wallet.presentationData.name}`, + title: `Transfer fund to vault from ${wallet.presentationData.name}`, isDisplay: false, uaiType: uaiType.VAULT_TRANSFER, prirority: 80, @@ -184,7 +184,7 @@ function* uaiChecksWorker({ payload }) { if (!migrationUai) { yield put( addToUaiStack({ - title: 'To use the Vault, reconfigure signing device', + title: 'To use the vault, reconfigure signer', isDisplay: false, uaiType: uaiType.VAULT_MIGRATION, prirority: 100, @@ -204,7 +204,7 @@ function* uaiChecksWorker({ payload }) { if (!defaultUai) { yield put( addToUaiStack({ - title: 'Make sure your signing devices are safe and accessible', + title: 'Make sure your signers are safe and accessible', isDisplay: false, uaiType: uaiType.DEFAULT, prirority: 10, diff --git a/src/store/sagas/upgrade.ts b/src/store/sagas/upgrade.ts index 39d9ba0ad..15ec4f176 100644 --- a/src/store/sagas/upgrade.ts +++ b/src/store/sagas/upgrade.ts @@ -25,10 +25,12 @@ import SigningServer from 'src/services/operations/SigningServer'; import { generateCosignerMapUpdates } from 'src/core/wallets/factories/VaultFactory'; import InheritanceKeyServer from 'src/services/operations/InheritanceKey'; import { CosignersMapUpdate, IKSCosignersMapUpdate } from 'src/services/interfaces'; +import { updateAppImageWorker, updateVaultImageWorker } from './bhr'; export const LABELS_INTRODUCTION_VERSION = '1.0.4'; export const BIP329_INTRODUCTION_VERSION = '1.0.7'; export const ASSISTED_KEYS_MIGRATION_VERSION = '1.1.9'; +export const KEY_MANAGEMENT_VERSION = '1.1.9'; export function* applyUpgradeSequence({ previousVersion, @@ -46,6 +48,10 @@ export function* applyUpgradeSequence({ yield put(migrateLabelsToBip329()); if (semver.lt(previousVersion, ASSISTED_KEYS_MIGRATION_VERSION)) yield call(migrateAssistedKeys); + if (semver.lt(previousVersion, KEY_MANAGEMENT_VERSION)) { + yield call(migrateStructureforSignersInAppImage); + yield call(migrateStructureforVaultInAppImage); + } yield put(setAppVersion(newVersion)); yield put(updateVersionHistory(previousVersion, newVersion)); @@ -149,11 +155,18 @@ function* migrateAssistedKeys() { const app: KeeperApp = yield call(dbManager.getObjectByIndex, RealmSchema.KeeperApp); const { signers } = activeVault; + const signerMap = {}; + dbManager + .getCollection(RealmSchema.Signer) + .forEach((signer) => (signerMap[signer.masterFingerprint as string] = signer)); for (let signer of signers) { - if (signer.type === SignerType.POLICY_SERVER) { + const signerType = signerMap[signer.masterFingerprint].type; + + if (signerType === SignerType.POLICY_SERVER) { const cosignersMapUpdates: CosignersMapUpdate[] = yield call( generateCosignerMapUpdates, + signerMap, signers, signer ); @@ -165,9 +178,10 @@ function* migrateAssistedKeys() { ); if (!migrationSuccessful) throw new Error('Failed to migrate assisted keys(SS)'); - } else if (signer.type === SignerType.INHERITANCEKEY) { + } else if (signerType === SignerType.INHERITANCEKEY) { const cosignersMapUpdates: IKSCosignersMapUpdate[] = yield call( generateCosignerMapUpdates, + signerMap, signers, signer ); @@ -184,3 +198,28 @@ function* migrateAssistedKeys() { console.log({ error }); } } + +function* migrateStructureforSignersInAppImage() { + try { + const response = yield call(updateAppImageWorker, { payload: {} }); + if (response.updated) { + console.log('Updated the Signers in app image'); + } else { + console.log('Failed to update the update the app image with the updated the structure'); + } + } catch (err) {} +} + +function* migrateStructureforVaultInAppImage() { + try { + const vaults: Vault[] = yield call(dbManager.getCollection, RealmSchema.Vault); + const activeVault: Vault = vaults.filter((vault) => !vault.archived)[0] || null; + + console.log('updating vault'); + const vaultResponse = yield call(updateVaultImageWorker, { + payload: { isUpdate: true, vault: activeVault }, + }); + } catch (err) { + console.log('Something went wrong in updating the vault image', err); + } +} diff --git a/src/store/sagas/wallets.ts b/src/store/sagas/wallets.ts index ae0363a0c..03f6eb4bf 100644 --- a/src/store/sagas/wallets.ts +++ b/src/store/sagas/wallets.ts @@ -9,14 +9,22 @@ import { VaultType, VisibilityType, WalletType, + XpubTypes, } from 'src/core/wallets/enums'; import { InheritanceConfiguration, + InheritanceKeyInfo, InheritancePolicy, SignerException, SignerRestriction, } from 'src/services/interfaces'; -import { Vault, VaultScheme, VaultSigner } from 'src/core/wallets/interfaces/vault'; +import { + Signer, + Vault, + VaultPresentationData, + VaultScheme, + VaultSigner, +} from 'src/core/wallets/interfaces/vault'; import { TransferPolicy, Wallet, @@ -46,7 +54,10 @@ import config from 'src/core/config'; import { createWatcher } from 'src/store/utilities'; import dbManager from 'src/storage/realm/dbManager'; import { generateVault } from 'src/core/wallets/factories/VaultFactory'; -import { generateWallet, generateWalletSpecs } from 'src/core/wallets/factories/WalletFactory'; +import { + generateWallet, + generateWalletSpecsFromMnemonic, +} from 'src/core/wallets/factories/WalletFactory'; import { getJSONFromRealmObject } from 'src/storage/realm/utils'; import { generateKey, hash256 } from 'src/services/operations/encryption'; import { uaiType } from 'src/models/interfaces/Uai'; @@ -59,12 +70,7 @@ import ElectrumClient, { import InheritanceKeyServer from 'src/services/operations/InheritanceKey'; import { genrateOutputDescriptors } from 'src/core/utils'; import { RootState } from '../store'; -import { - addSigningDevice, - initiateVaultMigration, - vaultCreated, - vaultMigrationCompleted, -} from '../reducers/vaults'; +import { initiateVaultMigration, vaultCreated, vaultMigrationCompleted } from '../reducers/vaults'; import { ADD_NEW_WALLETS, AUTO_SYNC_WALLETS, @@ -83,22 +89,25 @@ import { ADD_WHIRLPOOL_WALLETS_LOCAL, UPDATE_WALLET_PATH_PURPOSE_DETAILS, INCREMENT_ADDRESS_INDEX, + UPDATE_KEY_DETAILS, + UPDATE_VAULT_DETAILS, } from '../sagaActions/wallets'; import { ADD_NEW_VAULT, ADD_SIGINING_DEVICE, FINALISE_VAULT_MIGRATION, - FINALIZE_IK_SETUP, MIGRATE_VAULT, - finaliseIKSetup, } from '../sagaActions/vaults'; import { uaiChecks } from '../sagaActions/uai'; import { updateAppImageWorker, updateVaultImageWorker } from './bhr'; import { + relaySignersUpdateFail, + relaySignersUpdateSuccess, relayVaultUpdateFail, relayVaultUpdateSuccess, relayWalletUpdateFail, relayWalletUpdateSuccess, + setRelaySignersUpdateLoading, setRelayVaultUpdateLoading, setRelayWalletUpdateLoading, } from '../reducers/bhr'; @@ -353,7 +362,7 @@ function* addNewWallet( type: WalletType.DEFAULT, instanceNum: defaultWalletInstacnes, // zero-indexed walletName: walletName || 'Default Wallet', - walletDescription: walletDescription || 'Bitcoin Wallet', + walletDescription: walletDescription || '', derivationConfig, primaryMnemonic, networkType: config.NETWORK_TYPE, @@ -366,7 +375,7 @@ function* addNewWallet( type: WalletType.IMPORTED, instanceNum: null, // bip-85 instance number is null for imported wallets walletName: walletName || 'Imported Wallet', - walletDescription: walletDescription || 'Bitcoin Wallet', + walletDescription: walletDescription || '', importDetails, networkType: config.NETWORK_TYPE, transferPolicy, @@ -379,7 +388,7 @@ function* addNewWallet( type: WalletType.PRE_MIX, instanceNum, // deposit account's index walletName: 'Pre mix Wallet', - walletDescription: 'Bitcoin Wallet', + walletDescription: '', derivationConfig, networkType: config.NETWORK_TYPE, parentMnemonic, @@ -391,7 +400,7 @@ function* addNewWallet( type: WalletType.POST_MIX, instanceNum, // deposit account's index walletName: 'Post mix Wallet', - walletDescription: 'Bitcoin Wallet', + walletDescription: '', derivationConfig, networkType: config.NETWORK_TYPE, parentMnemonic, @@ -403,7 +412,7 @@ function* addNewWallet( type: WalletType.BAD_BANK, instanceNum, // deposit account's index walletName: 'Bad Bank Wallet', - walletDescription: 'Bitcoin Wallet', + walletDescription: '', derivationConfig, networkType: config.NETWORK_TYPE, parentMnemonic, @@ -494,7 +503,11 @@ export function* addNewVaultWorker({ try { const { newVaultInfo, isMigrated, oldVaultId, isRecreation = false } = payload; let { vault } = payload; + const signerMap = {}; + const signingDevices: Signer[] = yield call(dbManager.getCollection, RealmSchema.Signer); + signingDevices.forEach((signer) => (signerMap[signer.masterFingerprint as string] = signer)); + let isNewVault = false; // When the vault is passed directly during upgrade/downgrade process if (!vault) { const { @@ -521,7 +534,20 @@ export function* addNewVaultWorker({ networkType, vaultShellId, collaborativeWalletId, + signerMap, }); + isNewVault = true; + } + + if (isNewVault || isMigrated) { + // update IKS, if inheritance key has been added(new Vault) or needs an update(vault migration) + const [ikVaultKey] = vault.signers.filter( + (vaultKey) => signerMap[vaultKey.masterFingerprint]?.type === SignerType.INHERITANCEKEY + ); + if (ikVaultKey) { + const ikSigner: Signer = signerMap[ikVaultKey.masterFingerprint]; + yield call(finaliseIKSetupWorker, { payload: { ikSigner, ikVaultKey, vault } }); + } } if (newVaultInfo && newVaultInfo.collaborativeWalletId && !isRecreation) { @@ -559,7 +585,6 @@ export function* addNewVaultWorker({ yield put(vaultCreated({ hasNewVaultGenerationSucceeded: true })); yield put(relayVaultUpdateSuccess()); - yield put(finaliseIKSetup(vault)); // update IKS, if inheritance key has been added return true; } throw new Error('Relay updation failed'); @@ -578,8 +603,46 @@ export function* addNewVaultWorker({ export const addNewVaultWatcher = createWatcher(addNewVaultWorker, ADD_NEW_VAULT); -function* addSigningDeviceWorker({ payload: signer }: { payload: VaultSigner }) { - yield put(addSigningDevice([signer])); +function* addSigningDeviceWorker({ payload: { signers } }: { payload: { signers: Signer[] } }) { + if (!!signers.length) { + const signerMap = {}; + const existingSigners: Signer[] = yield call(dbManager.getCollection, RealmSchema.Signer); + existingSigners.forEach((signer) => (signerMap[signer.masterFingerprint as string] = signer)); + + // not letting user added multiple accounts for the same signer yet + for (const newSigner of signers) { + const existingSigner = signerMap[newSigner.masterFingerprint]; + if (existingSigner) { + // TODO: we're not YET supporting multiple keys (accounts) for the same script type + if ( + (newSigner.signerXpubs[XpubTypes.P2WPKH].length && + existingSigner.signerXpubs[XpubTypes.P2WPKH].length) || + (newSigner.signerXpubs[XpubTypes.P2WSH].length && + existingSigner.signerXpubs[XpubTypes.P2WSH].length) + ) { + yield put( + relaySignersUpdateFail( + 'A different account has already been added. Please use the existing key for this signer.' + ) + ); + return false; + } + } + } + yield put(setRelaySignersUpdateLoading(true)); + const response = yield call(updateAppImageWorker, { payload: { signers } }); + if (response.updated) { + yield call( + dbManager.createObjectBulk, + RealmSchema.Signer, + signers, + Realm.UpdateMode.Modified + ); + return true; + } + yield put(relaySignersUpdateFail(response.error)); + return false; + } } export const addSigningDeviceWatcher = createWatcher(addSigningDeviceWorker, ADD_SIGINING_DEVICE); @@ -603,6 +666,10 @@ function* migrateVaultWorker({ const networkType = config.NETWORK_TYPE; + const signerMap = {}; + const signingDevices: Signer[] = yield call(dbManager.getCollection, RealmSchema.Signer); + signingDevices.forEach((signer) => (signerMap[signer.masterFingerprint as string] = signer)); + const vault: Vault = yield call(generateVault, { type: vaultType, vaultName: vaultDetails.name, @@ -611,7 +678,9 @@ function* migrateVaultWorker({ signers: vaultSigners, networkType, vaultShellId, + signerMap, }); + yield put(initiateVaultMigration({ isMigratingNewVault: true, intrimVault: vault })); } catch (error) { yield put( @@ -643,7 +712,6 @@ function* finaliseVaultMigrationWorker({ payload }: { payload: { vaultId: string error: null, }) ); - yield put(uaiChecks([uaiType.VAULT_MIGRATION])); } } catch (error) { yield put( @@ -662,14 +730,15 @@ export const finaliseVaultMigrationWatcher = createWatcher( FINALISE_VAULT_MIGRATION ); -function* finaliseIKSetupWorker({ payload }: { payload: { vault: Vault } }) { +function* finaliseIKSetupWorker({ + payload, +}: { + payload: { ikSigner: Signer; ikVaultKey: VaultSigner; vault: Vault }; +}) { // finalise the IK setup - const { vault } = payload; - const [ikSigner] = vault.signers.filter((signer) => signer.type === SignerType.INHERITANCEKEY); + const { ikSigner, ikVaultKey, vault } = payload; const backupBSMSForIKS = yield select((state: RootState) => state.vault.backupBSMSForIKS); - - if (!ikSigner) return; - let updatedIkSigner: VaultSigner = null; + let updatedInheritanceKeyInfo: InheritanceKeyInfo = null; if (ikSigner.inheritanceKeyInfo) { // case: updating config for this new vault which already had IKS as one of its signers @@ -679,32 +748,29 @@ function* finaliseIKSetupWorker({ payload }: { payload: { vault: Vault } }) { const newIKSConfiguration: InheritanceConfiguration = { m: vault.scheme.m, n: vault.scheme.n, - descriptors: vault.signers.map((signer) => signer.signerId), + descriptors: vault.signers.map((signer) => signer.xfp), bsms: backupBSMSForIKS ? genrateOutputDescriptors(vault) : null, }; const { updated } = yield call( InheritanceKeyServer.updateInheritanceConfig, - ikSigner.signerId, + ikVaultKey.xfp, existingThresholdDescriptors, newIKSConfiguration ); if (updated) { - updatedIkSigner = { - ...ikSigner, - inheritanceKeyInfo: { - ...ikSigner.inheritanceKeyInfo, - configuration: newIKSConfiguration, - }, + updatedInheritanceKeyInfo = { + ...ikSigner.inheritanceKeyInfo, + configuration: newIKSConfiguration, }; - } else Alert.alert('Failed to update the inheritance key configuration'); + } else throw new Error('Failed to update the inheritance key configuration'); } else { // case: setting up a vault w/ IKS for the first time const config: InheritanceConfiguration = { m: vault.scheme.m, n: vault.scheme.n, - descriptors: vault.signers.map((signer) => signer.signerId), + descriptors: vault.signers.map((signer) => signer.xfp), bsms: backupBSMSForIKS ? genrateOutputDescriptors(vault) : null, }; @@ -715,37 +781,33 @@ function* finaliseIKSetupWorker({ payload }: { payload: { vault: Vault } }) { const { setupSuccessful } = yield call( InheritanceKeyServer.finalizeIKSetup, - ikSigner.signerId, + ikVaultKey.xfp, config, policy ); if (setupSuccessful) { - updatedIkSigner = { - ...ikSigner, - inheritanceKeyInfo: { - configuration: config, - policy, - }, + updatedInheritanceKeyInfo = { + configuration: config, + policy, }; - } else Alert.alert('Failed to finalise the inheritance key setup'); + } else throw new Error('Failed to finalise the inheritance key setup'); } - if (updatedIkSigner) { + if (updatedInheritanceKeyInfo) { // send updates to realm - const updatedSigners = vault.signers.map((signer) => { - if (signer.type === SignerType.INHERITANCEKEY) return updatedIkSigner; - return signer; - }); - - yield call(dbManager.updateObjectById, RealmSchema.Vault, vault.id, { - signers: updatedSigners, - }); + yield call( + dbManager.updateObjectByPrimaryId, + RealmSchema.Signer, + 'masterFingerprint', + ikSigner.masterFingerprint, + { + inheritanceKeyInfo: updatedInheritanceKeyInfo, + } + ); } } -export const finaliseIKSetupWatcher = createWatcher(finaliseIKSetupWorker, FINALIZE_IK_SETUP); - function* syncWalletsWorker({ payload, }: { @@ -783,6 +845,7 @@ function* refreshWalletsWorker({ const { wallets, options } = payload; try { if (!ELECTRUM_CLIENT.isClientConnected) { + ElectrumClient.resetCurrentPeerIndex(); yield call(connectToNodeWorker); } @@ -963,17 +1026,19 @@ export const updateWalletSettingsWatcher = createWatcher( export function* updateSignerPolicyWorker({ payload, }: { - payload: { signer; updates; verificationToken }; + payload: { signer; signingKey; updates; verificationToken }; }) { const vaults: Vault[] = yield call(dbManager.getCollection, RealmSchema.Vault); const activeVault: Vault = vaults.filter((vault) => !vault.archived)[0] || null; const { signer, + signingKey, updates, verificationToken, }: { - signer: VaultSigner; + signer: Signer; + signingKey: VaultSigner; updates: { restrictions?: SignerRestriction; exceptions?: SignerException; @@ -982,7 +1047,7 @@ export function* updateSignerPolicyWorker({ } = payload; const { updated } = yield call( SigningServer.updatePolicy, - signer.signerId, + signingKey.xfp, verificationToken, updates ); @@ -993,18 +1058,25 @@ export function* updateSignerPolicyWorker({ const { signers } = activeVault; for (const current of signers) { - if (current.signerId === signer.signerId) { - current.signerPolicy = { - ...current.signerPolicy, + if (current.xfp === signingKey.xfp) { + const updatedSignerPolicy = { + ...signer.signerPolicy, restrictions: updates.restrictions, exceptions: updates.exceptions, }; + + yield call( + dbManager.updateObjectByPrimaryId, + RealmSchema.Signer, + 'masterFingerprint', + signer.masterFingerprint, + { + signerPolicy: updatedSignerPolicy, + } + ); break; } } - yield call(dbManager.updateObjectById, RealmSchema.Vault, activeVault.id, { - signers, - }); } export const updateSignerPolicyWatcher = createWatcher( @@ -1048,7 +1120,7 @@ function* updateWalletDetailsWorker({ payload }) { shell: wallet.presentationData.shell, }; yield put(setRelayWalletUpdateLoading(true)); - // API-TO-DO: based on response call the DB + // API-TODO: based on response call the DB wallet.presentationData = presentationData; const response = yield call(updateAppImageWorker, { payload: { walletId: wallet.id } }); if (response.updated) { @@ -1069,6 +1141,51 @@ export const updateWalletDetailWatcher = createWatcher( UPDATE_WALLET_DETAILS ); +function* updateVaultDetailsWorker({ payload }) { + const { + vault, + details, + }: { + vault: Vault; + details: { + name: string; + description: string; + }; + } = payload; + try { + const presentationData: VaultPresentationData = { + name: details.name, + description: details.description, + visibility: vault.presentationData.visibility, + shell: vault.presentationData.shell, + }; + yield put(setRelayVaultUpdateLoading(true)); + // API-TODO: based on response call the DB + vault.presentationData = presentationData; + + console.log(vault.presentationData); + const response = yield call(updateVaultImageWorker, { + payload: { vault: vault }, + }); + if (response.updated) { + yield put(relayVaultUpdateSuccess()); + yield call(dbManager.updateObjectById, RealmSchema.Vault, vault.id, { + presentationData, + }); + } else { + yield put(relayVaultUpdateFail(response.error)); + } + } catch (err) { + console.log('err', err); + yield put(relayVaultUpdateFail('Something went wrong!')); + } +} + +export const updateVaultDetailsWatcher = createWatcher( + updateVaultDetailsWorker, + UPDATE_VAULT_DETAILS +); + function* updateWalletPathAndPuposeDetailsWorker({ payload }) { const { wallet, @@ -1085,14 +1202,14 @@ function* updateWalletPathAndPuposeDetailsWorker({ payload }) { ...wallet.derivationDetails, xDerivationPath: details.path, }; - const specs = generateWalletSpecs( + const specs = generateWalletSpecsFromMnemonic( derivationDetails.mnemonic, WalletUtilities.getNetworkByType(wallet.networkType), derivationDetails.xDerivationPath ); // recreate the specs yield put(setRelayWalletUpdateLoading(true)); - // API-TO-DO: based on response call the DB + // API-TODO: based on response call the DB wallet.derivationDetails = derivationDetails; wallet.specs = specs; @@ -1116,6 +1233,42 @@ export const updateWalletPathAndPuposeDetailWatcher = createWatcher( ); function* updateSignerDetailsWorker({ payload }) { + const { + signer, + key, + value, + }: { + signer: Signer; + key: string; + value: any; + } = payload; + + yield put(setRelaySignersUpdateLoading(true)); + try { + const response = yield call(updateAppImageWorker, { payload: { signers: [signer] } }); + if (response.updated) { + yield call( + dbManager.updateObjectByPrimaryId, + RealmSchema.Signer, + 'masterFingerprint', + signer.masterFingerprint, + { + [key]: value, + } + ); + yield put(relaySignersUpdateSuccess()); + } else { + yield put(relaySignersUpdateFail(response.error)); + } + } catch (err) { + console.error(err); + yield put(relaySignersUpdateFail('Something went wrong')); + } +} + +export const updateSignerDetails = createWatcher(updateSignerDetailsWorker, UPDATE_SIGNER_DETAILS); + +function* updateKeyDetailsWorker({ payload }) { const { signer, key, @@ -1125,25 +1278,34 @@ function* updateSignerDetailsWorker({ payload }) { key: string; value: any; } = payload; - // TO_DO_VAULT_API - const activeVault: Vault = dbManager - .getCollection(RealmSchema.Vault) - .filter((vault: Vault) => !vault.archived)[0]; - - const updatedSigners = activeVault.signers.map((item) => { - if (item.signerId === signer.signerId) { - item[key] = value; - return item; + + const vaultSigner = dbManager.getObjectByPrimaryId(RealmSchema.VaultSigner, 'xpub', signer.xpub); + const vaultSignerJSON: VaultSigner = vaultSigner.toJSON(); + if (key === 'registered') { + let updatedFlag = false; + const updatedRegsteredVaults = vaultSignerJSON.registeredVaults.map((info) => { + if (info.vaultId === value.vaultId) { + updatedFlag = true; + return { ...info, ...value }; + } else { + return info; + } + }); + if (!updatedFlag) { + updatedRegsteredVaults.push(value); } - return item; - }); + yield call(dbManager.updateObjectByPrimaryId, RealmSchema.VaultSigner, 'xpub', signer.xpub, { + registeredVaults: updatedRegsteredVaults, + }); + return; + } - yield call(dbManager.updateObjectById, RealmSchema.Vault, activeVault.id, { - signers: updatedSigners, + yield call(dbManager.updateObjectByPrimaryId, RealmSchema.VaultSigner, 'xpub', signer.xpub, { + [key]: value, }); } -export const updateSignerDetails = createWatcher(updateSignerDetailsWorker, UPDATE_SIGNER_DETAILS); +export const updateKeyDetails = createWatcher(updateKeyDetailsWorker, UPDATE_KEY_DETAILS); function* updateWalletsPropertyWorker({ payload, diff --git a/src/theme/Colors.ts b/src/theme/Colors.ts index 78ba54e72..c092d876d 100644 --- a/src/theme/Colors.ts +++ b/src/theme/Colors.ts @@ -43,6 +43,34 @@ const Colors = { ForestGreen: 'rgba(0,131,105,1)', PearlGrey: 'rgba(250,252,252,1)', pantoneGreen: 'rgba(45,103,89,1)', + pantoneGreenLight: 'rgba(45,103,89,0.08)', + Champagne: 'rgba(247,242,236,1)', + Warmbeige: 'rgba(247, 242, 236, 1)', + RussetBrown: 'rgba(145, 120, 93, 1)', + RussetBrownLight: 'rgba(210, 194, 179, 1)', + MintWhisper: 'rgba(45, 103, 89, 0.08)', + GreenishGrey: 'rgba(62, 82, 77, 1)', + brownColor: 'rgba(145, 120, 92, 1)', + learMoreTextcolor: 'rgba(242, 237, 230, 1)', + Linen: 'rgba(240, 235, 230, 1)', + LightBrown: 'rgba(145, 120, 93, 0.08)', + SlateGreen: 'rgba(165, 180, 174, 1)', + OffWhite: 'rgba(230,230,223,1)', + SageGreen: 'rgba(141,157,150,1)', + SlateGrey: 'rgba(36,49,46,1)', + LightKhaki: 'rgba(217,209,169,1)', + Eggshell: 'rgba(238,227,216,1)', + Teal: 'rgba(46,103,89,1)', + SmokeGreen: 'rgba(154,164,159,1)', + DeepOlive: 'rgba(35, 82, 71, 1)', + PaleKhaki: 'rgba(95,106,103,1)', + PaleTurquoise: 'rgba(184,214,207,1)', + darkGreen: 'rgb(46, 103, 89)', + PearlWhite: 'rgba(252, 252, 252, 1)', + deepTeal: 'rgba(28,73,64,1)', + PaleIvory: 'rgba(238,232,225,1)', + DarkSage: 'rgba(111,124,119,1)', + Smoke: 'rgba(162,162,162,1)', // Dark LightYellowDark: 'rgba(50,60,58,1)', GraniteGrayDark: 'rgba(136,136,136,1)', diff --git a/src/utils/GenerateLetterToAtternyPDF.ts b/src/utils/GenerateLetterToAtternyPDF.ts index 1f670c59a..be097a191 100644 --- a/src/utils/GenerateLetterToAtternyPDF.ts +++ b/src/utils/GenerateLetterToAtternyPDF.ts @@ -25,13 +25,15 @@ const GenerateLetterToAtternyPDF = async (fingerPrints) => {

Letter to the Attorney

-

This document is one of three Inheritance Planning documents provided by Keeper. The other 2 being Key Security Tips and Restoring Inheritance Vault. This document is auto-produced by the Bitcoin Keeper app. The data shared in this document is sensitive. Please be cautious about revealing part or all of its contents to anyone. To learn more, please visit bitcoinkeeper.app

+

This document is one of three Inheritance Planning documents provided by Keeper. The other 2 being Key Security Tips and Restoring Inheritance vault. This document is auto-produced by the Bitcoin Keeper app. The data shared in this document is sensitive. Please be cautious about revealing part or all of its contents to anyone. To learn more, please visit bitcoinkeeper.app

Subject: Bitcoin Bequest Information for Inclusion in My Will

Dear _________________ ,

I hope this letter finds you well. I am writing to provide you with the necessary information to include my bitcoin holdings in my estate plan. As a significant proportion of my wealth is held in bitcoin, it is crucial to address the legal transfer of these assets appropriately.

Below, I have outlined the specific details regarding my current bitcoin holdings:

1. Bitcoin Key Information:

-

${fingerPrints.map((keys, index) => `

Key ${index + 1} Fingerprint: ${keys}

`).join("")}

+

${fingerPrints + .map((keys, index) => `

Key ${index + 1} Fingerprint: ${keys}

`) + .join('')}

These fingerprints act as unique identifiers for the respective keys without revealing sensitive details. Following the BIP32 (Bitcoin Improvement Proposal 32) standard, each fingerprint helps identify the associated extended public key (xPub). The xPub serves as a distinct identifier that can be utilized by a digital asset expert or software, adhering to standard BIP32 derivation paths, to locate and validate the keys during the transfer process.

Please note that the funds associated with these keys may be held in any combination of single-key or multi-signature (multisig) wallets. Regardless of the specific configuration, I intend that any wealth controlled by these keys be legally transferred to the designated heir or intended beneficiary.

My explicit intention is to transfer the legal title to my bitcoin holdings to the designated heir or intended beneficiary. However, it is important to note that access to the actual keys and the bitcoin will be provided separately to the intended beneficiary. This letter solely addresses the transfer of legal title and the inclusion of my bitcoin assets in my estate plan.

@@ -39,13 +41,13 @@ const GenerateLetterToAtternyPDF = async (fingerPrints) => {

Thank you for your professional assistance in preparing my estate plan and ensuring the proper transfer of the legal title to my bitcoin assets according to my wishes.

Sincerely,


------------------------------------------------------------------------------------

-

This document is one of three Inheritance Planning documents provided by Keeper. The other 2 being Key Security Tips and Restoring Inheritance Vault. This document is auto-produced by the Bitcoin Keeper app. The data shared in this document is sensitive. Please be cautious about revealing part or all of its contents to anyone. To learn more, please visit bitcoinkeeper.app

+

This document is one of three Inheritance Planning documents provided by Keeper. The other 2 being Key Security Tips and Restoring Inheritance vault. This document is auto-produced by the Bitcoin Keeper app. The data shared in this document is sensitive. Please be cautious about revealing part or all of its contents to anyone. To learn more, please visit bitcoinkeeper.app

`; const options = { html, - fileName: `LetterToAtterny`, + fileName: 'LetterToAtterny', directory: 'Documents', base64: true, }; diff --git a/src/utils/GenerateRecoveryInstrPDF.ts b/src/utils/GenerateRecoveryInstrPDF.ts index fc2030262..42cdd9b25 100644 --- a/src/utils/GenerateRecoveryInstrPDF.ts +++ b/src/utils/GenerateRecoveryInstrPDF.ts @@ -27,9 +27,9 @@ const GenerateRecoveryInstrPDF = async (signers, descriptorString) => {

Vault Recovery Instructions with or without the Keeper App:

This document is one of three Inheritance Planning documents provided by Keeper. The other 2 are Letter to the Attorney and Key Security Tips. This document is auto-produced by the Bitcoin Keeper app. The data shared in this document is sensitive. Please be cautious about revealing part or all of its contents to anyone. To learn more, please visit bitcoinkeeper.app.

Getting Started:

-

An m-of-n multisig setup enhances security by distributing control and access, thus reducing the risk of unauthorized access or fraudulent activities. The bitcoin you inherited is within such a setup, i.e. the Vault. This document provides the method to recover the Vault and gain custody of your Inheritance.

+

An m-of-n multisig setup enhances security by distributing control and access, thus reducing the risk of unauthorized access or fraudulent activities. The bitcoin you inherited is within such a setup, i.e. the vault. This document provides the method to recover the vault and gain custody of your Inheritance.

If you have not previously handled bitcoin, we highly recommend contacting the people mentioned in the “Technical Assistance” section below.

-

Please note that failure to recover the Vault successfully may result in the loss of bitcoin forever. Great caution is advised.

+

Please note that failure to recover the vault successfully may result in the loss of bitcoin forever. Great caution is advised.

Technical Assistance:

Recovering a multi-sig vault has a few steps involved. If you need assistance, you could reach out to any of the people in the list below for help. They are the trusted contacts of the person giving away the bitcoin (You do not have to share the keys with them)

Person 1:

@@ -64,32 +64,40 @@ const GenerateRecoveryInstrPDF = async (signers, descriptorString) => {

Telegram: https://t.me/bitcoinkeeper

Twitter: https://twitter.com/bitcoinkeeper_

Email: info@bithyve.com


-

Restoring the Vault:

-

We have attached the Output Descriptor file as an Annexure to this document. To recover the Vault, please input the Output Descriptor file in a wallet (such as Electrum or Sparrow) that supports a multi-sig setup. You could, of course, use Keeper to recover the Vault, but it’s not necessary that you do. Look for the “Recovery” button/section when setting up a wallet. Follow the steps from there.

+

Restoring the vault:

+

We have attached the Wallet Configuration File as an Annexure to this document. To recover the vault, please input the Wallet Configuration File in a wallet (such as Electrum or Sparrow) that supports a multi-sig setup. You could, of course, use Keeper to recover the vault, but it’s not necessary that you do. Look for the “Recovery” button/section when setting up a wallet. Follow the steps from there.

Please note that the funds associated with these keys may be in any combination of single-key or multi-signature (multisig) wallets.

A) Key: Type Details

- ${signers.map((keys, index) => - `

Key ${index + 1}: ${keys.signerId}

-

Type: ${keys.type}

`).join("")}
- ${signers.map((keys, index) => - `

Key ${index + 1}: ${keys.signerId}

+ ${signers + .map( + (keys, index) => + `

Key ${index + 1}: ${keys.xfp}

+

Type: ${keys.type}

` + ) + .join('')}
+ ${signers + .map( + (keys, index) => + `

Key ${index + 1}: ${keys.xfp}

Location details:

-

Access details:


`).join("")} +

Access details:


` + ) + .join('')}

Any other information:






----------------------------------------------------------------------------------------------------

-

With the Output Descriptor file and the keys with you, you now have complete access to the Vault.

+

With the Wallet Configuration File and the keys with you, you now have complete access to the vault.

----------------------------------------------------------------------------------------------------

This document is one of three Inheritance Planning documents provided by Keeper. The other 2 are Letter to the Attorney and Key Security Tips. This document is auto-produced by the Bitcoin Keeper app. The data shared in this document is sensitive. Please be cautious about revealing part or all of its contents to anyone. To learn more, please visit bitcoinkeeper.app.

----------------------------------------------------------------------------------------------------

Annexure 1

-

Output Descriptor

+

Wallet Configuration File

${descriptorString}

`; const options = { html, - fileName: `RecoveryInstruction`, + fileName: 'RecoveryInstruction', directory: 'Documents', base64: true, }; diff --git a/src/utils/GenerateSecurityTipsPDF.ts b/src/utils/GenerateSecurityTipsPDF.ts index 7e9779dc2..775d72197 100644 --- a/src/utils/GenerateSecurityTipsPDF.ts +++ b/src/utils/GenerateSecurityTipsPDF.ts @@ -30,7 +30,7 @@ const GenerateSecurityTipsPDF = async () => {

Key Security Tips:

This document is one of three Inheritance Planning documents provided by Keeper. The other 2 being: Letter to the Attorney and Recovery Instructions for your heir. This document is auto-produced by the Bitcoin Keeper app. To learn more visit bitcoinkeeper.app

Getting Started:

-

A multisig enhances wallet security by distributing control and access, thus reducing the risk of unauthorized access, fraudulent activities, and loss of funds. The bitcoin your heir would inherit is within such a setup, i.e. your Vault. This document offers suggestions for storing keys and access & recovery mechanisms safely so that your intended beneficiary* can easily access your bitcoin when needed.

+

A multisig enhances wallet security by distributing control and access, thus reducing the risk of unauthorized access, fraudulent activities, and loss of funds. The bitcoin your heir would inherit is within such a setup, i.e. your vault. This document offers suggestions for storing keys and access & recovery mechanisms safely so that your intended beneficiary* can easily access your bitcoin when needed.

Also, the following information is only meant to help you start planning your bitcoin inheritance and should be considered as partial information for your bitcoin inheritance planning. We recommend you also speak with your estate planners to customize a plan that works best for you and to ensure that legal title also passes to your heir(s).

*Please note that the term intended beneficiary is not being used in legal terminology.

Checklist before storing away your keys:

@@ -118,7 +118,7 @@ const GenerateSecurityTipsPDF = async () => {

Recovery phrases come into play when a key becomes inaccessible. Some of the reasons for inaccessibility may be the degradation of devices storing the keys or the souring of relationships with people entrusted with them. Thus it is important that you not only backup the device storing a key properly, but also test out your recovery mechanism. Once satisfied, please consider etching the seed words onto stainless steel plates for longevity. Please consider etching the seed words of your hardware wallets onto steel plates and storing them in tamper-evident bags.

When you are satisfied with the arrangements you’ve made to store access and backup mechanisms, a point to decide is whether you want to store them along with the devices that have your keys or store them separately. This is an important decision and should be taken carefully.

To be avoided:

-

Treasure hunts: Please do not simply share keys with people you trust in your lifetime. They may collude and access the Vault after you pass away.

+

Treasure hunts: Please do not simply share keys with people you trust in your lifetime. They may collude and access the vault after you pass away.

Photographing things: Photographs of seed words and pins may give unintended access to your private keys. Please avoid photographing them.

Unprotected Storage: Avoid storing your pins/seed words in random files that could be easily accessed or hacked.

Entrusting all your keys to one person or storing them all in one place: This action beats the purpose of a multi-sig setup and creates a single point of failure.

@@ -137,7 +137,7 @@ const GenerateSecurityTipsPDF = async () => { `; const options = { html, - fileName: `KeySecurityTips`, + fileName: 'KeySecurityTips', directory: 'Documents', base64: true, }; diff --git a/src/utils/utilities.ts b/src/utils/utilities.ts index 97499cee8..547dfa5ec 100644 --- a/src/utils/utilities.ts +++ b/src/utils/utilities.ts @@ -119,3 +119,8 @@ export const crossInteractionHandler = (error): string => { export const getBackupDuration = () => config.ENVIRONMENT === APP_STAGE.PRODUCTION ? 1.555e7 : 1800; + +export const emailCheck = (email) => { + let reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w\w+)+$/; + return reg.test(email); +}; diff --git a/yarn.lock b/yarn.lock index 42040a17c..906d56ec0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -820,6 +820,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.10.tgz#94b5f8522356e69e8277276adf67ed280c90ecc1" integrity sha512-TYk3OA0HKL6qNryUayb5UUEhM/rkOQozIBEA5ITXh5DWrSp0TlUQXMyZmnWxG/DizSWBeeQ0Zbc5z8UGaaqoeg== +"@babel/parser@^7.14.0": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b" + integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== + "@babel/parser@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.1.tgz#6f6d6c2e621aad19a92544cc217ed13f1aac5b4c" @@ -3843,11 +3848,25 @@ query-string "^7.0.0" react-is "^16.13.0" +"@react-navigation/drawer@^6.6.6": + version "6.6.6" + resolved "https://registry.yarnpkg.com/@react-navigation/drawer/-/drawer-6.6.6.tgz#6a48e5ea2bf70dc6dfbe17e7e48b5a532d8886dc" + integrity sha512-DW/oNRisSOGOqvZfCzfhKBxnzT97Teqtg1Gal85g+K3gnVbM1jOBE2PdnYsKU0fULfFtDwvp/QZSbcgjDpr12A== + dependencies: + "@react-navigation/elements" "^1.3.21" + color "^4.2.3" + warn-once "^0.1.0" + "@react-navigation/elements@^1.3.17": version "1.3.17" resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.17.tgz#9cb95765940f2841916fc71686598c22a3e4067e" integrity sha512-sui8AzHm6TxeEvWT/NEXlz3egYvCUog4tlXA4Xlb2Vxvy3purVXDq/XsM56lJl344U5Aj/jDzkVanOTMWyk4UA== +"@react-navigation/elements@^1.3.21": + version "1.3.21" + resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.21.tgz#debac6becc6b6692da09ec30e705e476a780dfe1" + integrity sha512-eyS2C6McNR8ihUoYfc166O1D8VYVh9KIl0UQPI8/ZJVsStlfSTgeEEh+WXge6+7SFPnZ4ewzEJdSAHH+jzcEfg== + "@react-navigation/elements@^1.3.4": version "1.3.4" resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.4.tgz#abb48136508c1e60862dc14a101ce02ff685a337" @@ -6668,6 +6687,11 @@ crypto-js@^3.2.0: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.3.0.tgz#846dd1cce2f68aacfa156c8578f926a609b7976b" integrity sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q== +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + css-in-js-utils@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz#640ae6a33646d401fc720c54fc61c42cd76ae2bb" @@ -7924,6 +7948,11 @@ flow-parser@0.*: resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.183.1.tgz#633387855028cbeb38d65ed0a0d64729e1599a3b" integrity sha512-xBnvBk8D7aBY7gAilyjjGaNJe+9PGU6I/D2g6lGkkKyl4dW8nzn2eAc7Sc7RNRRr2NNYwpgHOOxBTjJKdKOXcA== +flow-parser@^0.121.0: + version "0.121.0" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.121.0.tgz#9f9898eaec91a9f7c323e9e992d81ab5c58e618f" + integrity sha512-1gIBiWJNR0tKUNv8gZuk7l9rVX06OuLzY9AoGio7y/JT4V1IZErEMEq2TJS+PFcw/y0RshZ1J/27VfK1UQzYVg== + flow-parser@^0.206.0: version "0.206.0" resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.206.0.tgz#f4f794f8026535278393308e01ea72f31000bfef" @@ -11687,6 +11716,16 @@ react-native-camera@^4.2.1: dependencies: prop-types "^15.6.2" +react-native-codegen@^0.70.7: + version "0.70.7" + resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.70.7.tgz#8f6b47a88740ae703209d57b7605538d86dacfa6" + integrity sha512-qXE8Jrhc9BmxDAnCmrHFDLJrzgjsE/mH57dtC4IO7K76AwagdXNCMRp5SA8XdHJzvvHWRaghpiFHEMl9TtOBcQ== + dependencies: + "@babel/parser" "^7.14.0" + flow-parser "^0.121.0" + jscodeshift "^0.14.0" + nullthrows "^1.1.1" + react-native-config@^1.4.5: version "1.4.6" resolved "https://registry.yarnpkg.com/react-native-config/-/react-native-config-1.4.6.tgz#2aefebf4d9cf02831e64bbc1307596bd212f6d42" @@ -11733,10 +11772,10 @@ react-native-fs@^2.20.0: base-64 "^0.1.0" utf8 "^3.0.0" -react-native-gesture-handler@^2.12.0: - version "2.12.1" - resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.12.1.tgz#f11a99fb95169810c6886fad5efa01a17fd81660" - integrity sha512-deqh36bw82CFUV9EC4tTo2PP1i9HfCOORGS3Zmv71UYhEZEHkzZv18IZNPB+2Awzj45vLIidZxGYGFxHlDSQ5A== +react-native-gesture-handler@^2.14.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.14.0.tgz#d6aec0d8b2e55c67557fd6107e828c0a1a248be8" + integrity sha512-cOmdaqbpzjWrOLUpX3hdSjsMby5wq3PIEdMq7okJeg9DmCzanysHSrktw1cXWNc/B5MAgxAn9J7Km0/4UIqKAQ== dependencies: "@egjs/hammerjs" "^2.0.17" hoist-non-react-statics "^3.3.0" @@ -11837,10 +11876,10 @@ react-native-randombytes@^3.0.0: buffer "^4.9.1" sjcl "^1.0.3" -react-native-reanimated@^3.3.0: - version "3.4.2" - resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.4.2.tgz#744154fead6d8d31d5bd9ac617d8c84d74a6f697" - integrity sha512-FbtG+f1PB005vDTJSv4zAnTK7nNXi+FjFgbAM5gOzIZDajfph2BFMSUstzIsN8T77+OKuugUBmcTqLnQ24EBVg== +react-native-reanimated@^3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.6.1.tgz#5add41efafac6d0befd9786e752e7f26dbe903b7" + integrity sha512-F4vG9Yf9PKmE3GaWtVGUpzj3SM6YY2cx1yRHCwiMd1uY7W0gU017LfcVUorboJnj0y5QZqEriEK1Usq2Y8YZqg== dependencies: "@babel/plugin-transform-object-assign" "^7.16.7" "@babel/preset-typescript" "^7.16.7" @@ -11852,6 +11891,11 @@ react-native-responsive-screen@^1.4.2: resolved "https://registry.yarnpkg.com/react-native-responsive-screen/-/react-native-responsive-screen-1.4.2.tgz#45280826d24f9accbfdf46a36cb8e6d780f76f28" integrity sha512-BLYz0UUpeohrib7jbz6wDmtBD5OmiuMRko4IT8kIF63taXPod/c5iZgmWnr5qOnK8hMuKiGMvsM3sC+eHX/lEQ== +react-native-rsa-native@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/react-native-rsa-native/-/react-native-rsa-native-2.0.5.tgz#7db4aef49405bb5b5bcaea12b9dfd1b251c690ab" + integrity sha512-gwwvFSwGW5WKrpDyBQ/eTf1UrVABeAvMcT4YWemzPSUo6aHZs1kbBm2rXmwN5okhUzJsry5zjjz/qdx5GXRugQ== + react-native-safe-area-context@^4.5.3: version "4.7.2" resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.7.2.tgz#1673aa99b6a9235e7faaf5a248e69795d6e54e07"