diff --git a/android/app/build.gradle b/android/app/build.gradle index 6bc78df4d..2c2ce0196 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -86,8 +86,8 @@ android { applicationId "io.hexawallet.keeper" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 384 - versionName "1.2.14" + versionCode 390 + versionName "1.2.15" missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'store', 'play' multiDexEnabled true diff --git a/ios/hexa_keeper.xcodeproj/project.pbxproj b/ios/hexa_keeper.xcodeproj/project.pbxproj index 82bf89db4..79fc22376 100644 --- a/ios/hexa_keeper.xcodeproj/project.pbxproj +++ b/ios/hexa_keeper.xcodeproj/project.pbxproj @@ -811,7 +811,7 @@ CODE_SIGN_ENTITLEMENTS = hexa_keeper/hexa_keeper.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 384; + CURRENT_PROJECT_VERSION = 390; DEVELOPMENT_TEAM = Y5TCB759QL; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -912,7 +912,7 @@ "$(inherited)", "\"$(SRCROOT)\"", ); - MARKETING_VERSION = 1.2.14; + MARKETING_VERSION = 1.2.15; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -936,7 +936,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = hexa_keeper/hexa_keeper.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: Bithyve UK Ltd (Y5TCB759QL)"; - CURRENT_PROJECT_VERSION = 384; + CURRENT_PROJECT_VERSION = 390; DEVELOPMENT_TEAM = Y5TCB759QL; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Y5TCB759QL; HEADER_SEARCH_PATHS = ( @@ -1037,7 +1037,7 @@ "$(inherited)", "\"$(SRCROOT)\"", ); - MARKETING_VERSION = 1.2.14; + MARKETING_VERSION = 1.2.15; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1192,7 +1192,7 @@ CODE_SIGN_ENTITLEMENTS = hexa_keeper_dev.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 384; + CURRENT_PROJECT_VERSION = 390; DEVELOPMENT_TEAM = Y5TCB759QL; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -1294,7 +1294,7 @@ "$(PROJECT_DIR)", "\"$(SRCROOT)\"", ); - MARKETING_VERSION = 1.2.14; + MARKETING_VERSION = 1.2.15; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1320,7 +1320,7 @@ CODE_SIGN_ENTITLEMENTS = hexa_keeper_dev.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 384; + CURRENT_PROJECT_VERSION = 390; DEVELOPMENT_TEAM = Y5TCB759QL; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Y5TCB759QL; HEADER_SEARCH_PATHS = ( @@ -1422,7 +1422,7 @@ "$(PROJECT_DIR)", "\"$(SRCROOT)\"", ); - MARKETING_VERSION = 1.2.14; + MARKETING_VERSION = 1.2.15; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/ios/hexa_keeper/Info.plist b/ios/hexa_keeper/Info.plist index f635b687c..922274419 100644 --- a/ios/hexa_keeper/Info.plist +++ b/ios/hexa_keeper/Info.plist @@ -36,7 +36,7 @@ CFBundleVersion - 384 + 390 LSRequiresIPhoneOS NFCReaderUsageDescription diff --git a/ios/hexa_keeperTests/Info.plist b/ios/hexa_keeperTests/Info.plist index b6d58badf..9ddaaa8ed 100644 --- a/ios/hexa_keeperTests/Info.plist +++ b/ios/hexa_keeperTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 384 + 390 diff --git a/ios/hexa_keeper_dev-Info.plist b/ios/hexa_keeper_dev-Info.plist index 56b5ec260..c8ee43b7b 100644 --- a/ios/hexa_keeper_dev-Info.plist +++ b/ios/hexa_keeper_dev-Info.plist @@ -36,7 +36,7 @@ CFBundleVersion - 384 + 390 LSRequiresIPhoneOS NFCReaderUsageDescription diff --git a/package.json b/package.json index 06c45952b..e35b05e02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hexa_keeper", - "version": "1.2.14", + "version": "1.2.15", "private": true, "scripts": { "ios": "react-native run-ios", diff --git a/src/assets/images/asterisks.svg b/src/assets/images/asterisks.svg new file mode 100644 index 000000000..1c9766857 --- /dev/null +++ b/src/assets/images/asterisks.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/illustration-inheritance-key.svg b/src/assets/images/illustration-inheritance-key.svg new file mode 100644 index 000000000..87cf57f33 --- /dev/null +++ b/src/assets/images/illustration-inheritance-key.svg @@ -0,0 +1,101 @@ + + + +Created with Fabric.js 5.2.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/invalidUTXO.svg b/src/assets/images/invalidUTXO.svg new file mode 100644 index 000000000..0d5f67fd2 --- /dev/null +++ b/src/assets/images/invalidUTXO.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/key-empty-state-illustration.svg b/src/assets/images/key-empty-state-illustration.svg new file mode 100644 index 000000000..11c275a30 --- /dev/null +++ b/src/assets/images/key-empty-state-illustration.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/manage-wallet-icon.svg b/src/assets/images/manage-wallet-icon.svg new file mode 100644 index 000000000..77e77f874 --- /dev/null +++ b/src/assets/images/manage-wallet-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/planCheckMark.svg b/src/assets/images/planCheckMark.svg new file mode 100644 index 000000000..76bb23ac6 --- /dev/null +++ b/src/assets/images/planCheckMark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/planCheckMarkSelected.svg b/src/assets/images/planCheckMarkSelected.svg new file mode 100644 index 000000000..0b4340a10 --- /dev/null +++ b/src/assets/images/planCheckMarkSelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/receive-green.svg b/src/assets/images/receive-green.svg new file mode 100644 index 000000000..49f7e7be0 --- /dev/null +++ b/src/assets/images/receive-green.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/seedsigner-setup-horizontal.svg b/src/assets/images/seedsigner-setup-horizontal.svg new file mode 100644 index 000000000..050562f98 --- /dev/null +++ b/src/assets/images/seedsigner-setup-horizontal.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/images/tagline.svg b/src/assets/images/tagline.svg index 0c29a8efd..dc09c26d6 100644 --- a/src/assets/images/tagline.svg +++ b/src/assets/images/tagline.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/components/CameraUnauthorized.tsx b/src/components/CameraUnauthorized.tsx index 1af308131..dc0a542c2 100644 --- a/src/components/CameraUnauthorized.tsx +++ b/src/components/CameraUnauthorized.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Linking, StyleSheet, TouchableOpacity, View } from 'react-native'; import { Box, useColorMode } from 'native-base'; -import { wp, hp } from 'src/constants/responsive'; +import { wp, hp, windowWidth } from 'src/constants/responsive'; import Text from './KeeperText'; function CameraUnauthorized() { @@ -12,42 +12,40 @@ function CameraUnauthorized() { }; return ( - - - + + Camera access is turned off + + + Turn on the camera in your device settings + + + - Camera access is turned off - - - Turn on the camera in your device settings - - - - - Tap to go to settings - - - - - + + Tap to go to settings + + + + ); } @@ -55,25 +53,25 @@ export default CameraUnauthorized; const styles = StyleSheet.create({ container: { - justifyContent: 'flex-end', + alignSelf: 'center', + backgroundColor: 'black', + justifyContent: 'center', alignItems: 'center', - flex: 1, - marginBottom: hp(40), + height: windowWidth * 0.7, + width: windowWidth * 0.8, }, learnMoreContainer: { borderWidth: 0.5, borderRadius: 5, - paddingHorizontal: 5, + paddingVertical: hp(3), + paddingHorizontal: wp(8), justifyContent: 'center', alignItems: 'center', + marginBottom: hp(30), }, learnMoreText: { fontSize: 12, letterSpacing: 0.6, alignSelf: 'center', }, - cameraView: { - height: hp(280), - width: wp(375), - }, }); diff --git a/src/components/Carousel/ChoosePlanCarousel.tsx b/src/components/Carousel/ChoosePlanCarousel.tsx index 085344367..1c0bfae3c 100644 --- a/src/components/Carousel/ChoosePlanCarousel.tsx +++ b/src/components/Carousel/ChoosePlanCarousel.tsx @@ -1,6 +1,6 @@ import { Box } from 'native-base'; import { FlatList, Dimensions } from 'react-native'; -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { hp } from 'src/constants/responsive'; import { KeeperApp } from 'src/models/interfaces/KeeperApp'; import { RealmSchema } from 'src/storage/realm/enum'; @@ -9,7 +9,7 @@ import { useQuery } from '@realm/react'; import ChoosePlanCarouselItem from './ChoosePlanCarouselItem'; const { width } = Dimensions.get('window'); -const itemWidth = width / 3.5 - 10; +const itemWidth = width * 0.6 - 10; interface Props { data: SubScriptionPlan[]; onPress?: any; @@ -21,11 +21,18 @@ interface Props { function ChoosePlanCarousel(props: Props) { const { subscription }: KeeperApp = useQuery(RealmSchema.KeeperApp)[0]; + const listRef = useRef(); const [currentPosition, setCurrentPosition] = useState( props.currentPosition !== 0 ? props.currentPosition : subscription.level - 1 ); + useEffect(() => { + setTimeout(() => { + listRef?.current?.scrollToIndex({ animated: true, index: currentPosition }); + }, 200); + }, []); + const _onSnapToItem = (index) => { setCurrentPosition(index); props.onChange(index); @@ -38,6 +45,8 @@ function ChoosePlanCarousel(props: Props) { }} > console.log('onScrollToIndexFailed', val)} data={props.data} horizontal showsHorizontalScrollIndicator={false} diff --git a/src/components/Carousel/ChoosePlanCarouselItem.tsx b/src/components/Carousel/ChoosePlanCarouselItem.tsx index 22d943f75..84f460766 100644 --- a/src/components/Carousel/ChoosePlanCarouselItem.tsx +++ b/src/components/Carousel/ChoosePlanCarouselItem.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { Box, useColorMode } from 'native-base'; +import { Box, useColorMode, HStack } from 'native-base'; import { Pressable, StyleSheet } from 'react-native'; import { hp, wp } from 'src/constants/responsive'; import { SubscriptionTier } from 'src/models/enums/SubscriptionTier'; @@ -9,21 +9,28 @@ 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'; +import Colors from 'src/theme/Colors'; +import PlanCheckMarkSelected from 'src/assets/images/planCheckMarkSelected.svg'; const styles = StyleSheet.create({ wrapperView: { borderRadius: 10, marginHorizontal: wp(4), position: 'relative', - paddingBottom: 20, + paddingVertical: 20, + borderWidth: 1, + borderColor: Colors.GrayX11, + height: 135, }, + circle: { width: 40, height: 40, borderRadius: 40 / 2, - marginTop: 10, alignItems: 'center', justifyContent: 'center', + marginLeft: 13, + marginRight: 15, }, }); @@ -100,35 +107,17 @@ function ChoosePlanCarouselItem({ return ( onPress(index)} testID="btn_selectPlan"> - + + + )} + - - {item.productIds.includes(subscription.productId.toLowerCase()) ? ( - - - CURRENT - - - ) : ( - - - - )} + {/* Icon */} + } {item.name === 'Diamond Hands' && } + + {/* Details */} + {item.name} - - {item.subTitle} - - {getAmt} + {`(${item.subTitle})`} - - {item.productType !== 'free' && item.isActive ? (isMonthly ? '/month' : '/year') : ''} + + {getAmt + + (item.productType !== 'free' && item.isActive + ? isMonthly + ? '/month' + : '/year' + : '')} - - {getFreeTrail} + + + {getFreeTrail ? '- ' + getFreeTrail : ''} - {canSelectPlan === true && - !item.productIds.includes(subscription.productId.toLowerCase()) ? ( - - onSelect(item, index)} - value={getBtnTitle} - disabled={!item.isActive || requesting} - titleColor={`${colorMode}.pantoneGreen`} - backgroundColor={`${colorMode}.seashellWhite`} - boldTitle - /> - - ) : null} - + ); } diff --git a/src/components/CustomButton/CustomGreenButton.tsx b/src/components/CustomButton/CustomGreenButton.tsx index 39525d974..a2c6ff71d 100644 --- a/src/components/CustomButton/CustomGreenButton.tsx +++ b/src/components/CustomButton/CustomGreenButton.tsx @@ -7,19 +7,27 @@ export interface Props { value: string; onPress?: Function; disabled?: boolean; + fullWidth?: boolean; } function CustomGreenButton(props: Props) { const { colorMode } = useColorMode(); return ( { props.onPress(); }} > - + {props.value} @@ -43,6 +51,13 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + fullWidth: { + flex: 1, + width: '100%', + }, + disabledBtnOpacity: { + opacity: 0.5, + }, }); export default CustomGreenButton; diff --git a/src/components/KeeperTextInput.tsx b/src/components/KeeperTextInput.tsx index 68b35f925..d8a2b4da7 100644 --- a/src/components/KeeperTextInput.tsx +++ b/src/components/KeeperTextInput.tsx @@ -2,6 +2,7 @@ import { StyleSheet } from 'react-native'; import React from 'react'; import { Input, useColorMode, Box } from 'native-base'; import KeeperText from './KeeperText'; +import Colors from 'src/theme/Colors'; function KeeperTextInput({ placeholder, @@ -13,11 +14,16 @@ function KeeperTextInput({ maxLength = null, inputRef = null, height = 10, + isError = false, + onBlur = () => {}, + onFocus = () => {}, }) { const { colorMode } = useColorMode(); return ( diff --git a/src/components/NotificationStack.tsx b/src/components/NotificationStack.tsx index f26adb127..5356b296d 100644 --- a/src/components/NotificationStack.tsx +++ b/src/components/NotificationStack.tsx @@ -98,6 +98,7 @@ interface uaiDefinationInterface { cta: any; }; }; + hideHiddenVaults?: boolean; }; } @@ -246,6 +247,7 @@ const Card = memo(({ uai, index, totalLength, activeIndex, skipUaiHandler }: Car cta: () => skipUaiHandler(uai), }, }, + hideHiddenVaults: true, }, }; case uaiType.IKS_REQUEST: @@ -383,7 +385,7 @@ const Card = memo(({ uai, index, totalLength, activeIndex, skipUaiHandler }: Car case uaiType.SIGN_TRANSACTION: return { heading: uai.uaiDetails.heading, - body: uai.uaiDetails.body, + body: 'Transaction would be signed automatically if not declined', btnConfig: { primary: { text: 'Decline', @@ -564,6 +566,7 @@ const Card = memo(({ uai, index, totalLength, activeIndex, skipUaiHandler }: Car setShowModal={setShowSelectVault} onlyVaults={true} onlyWallets={false} + hideHiddenVaults={uaiConfig?.modalDetails?.hideHiddenVaults} buttonCallback={(vault) => { const entityWallet = allWallets.find((wallet) => wallet.id === uai.entityId); navigtaion.navigate('AddSendAmount', { diff --git a/src/components/SelectWalletModal.tsx b/src/components/SelectWalletModal.tsx index 3bb583855..15aec19b7 100644 --- a/src/components/SelectWalletModal.tsx +++ b/src/components/SelectWalletModal.tsx @@ -6,7 +6,7 @@ import { wp, hp } from 'src/constants/responsive'; import { LocalizationContext } from 'src/context/Localization/LocContext'; import useWallets from 'src/hooks/useWallets'; import SignerCard from 'src/screens/AddSigner/SignerCard'; -import { EntityKind, VaultType } from 'src/services/wallets/enums'; +import { EntityKind, VaultType, VisibilityType } from 'src/services/wallets/enums'; 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'; @@ -68,6 +68,7 @@ const SelectWalletModal = ({ subTitle = 'Please select vault to which you want to transfer your funds', buttonText = 'Transfer', secondaryButtonText = 'Skip', + hideHiddenVaults = false, }) => { const { colorMode } = useColorMode(); const { translations } = useContext(LocalizationContext); @@ -79,7 +80,13 @@ const SelectWalletModal = ({ const walletsData = useMemo(() => { if (onlyWallets) return wallets; - if (onlyVaults) return allVaults; + if (onlyVaults) { + if (hideHiddenVaults) + return allVaults.filter( + (vault) => vault.presentationData.visibility !== VisibilityType.HIDDEN // To hide vaults which are hidden + ); + return allVaults; + } return [...wallets, ...allVaults]; }, [wallets, allVaults, onlyWallets, onlyVaults]); diff --git a/src/components/SigningDevices/KeyCard.tsx b/src/components/SigningDevices/KeyCard.tsx new file mode 100644 index 000000000..59d67fdf2 --- /dev/null +++ b/src/components/SigningDevices/KeyCard.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { Box, VStack, HStack, useColorMode } from 'native-base'; +import { StyleSheet, ActivityIndicator } from 'react-native'; +import Text from 'src/components/KeeperText'; +import ActionChip from 'src/components/ActionChip'; +import HexagonIcon from 'src/components/HexagonIcon'; +import Colors from 'src/theme/Colors'; +import { hp } from 'src/constants/responsive'; + +function KeyCard({ + icon, + name, + description, + descriptionTitle, + isLoading, + primaryAction, + secondaryAction, + primaryText, + secondaryText, + primaryIcon, + secondaryIcon, + dateAdded, +}) { + const { colorMode } = useColorMode(); + return ( + + + + + + + {name} + + + + {dateAdded} + + + + + {descriptionTitle && ( + + {descriptionTitle} + + )} + {description && ( + + {description} + + )} + + + + + : secondaryIcon} + /> + + + + ); +} + +const styles = StyleSheet.create({ + signerContainer: { + width: '90%', + borderRadius: 10, + padding: 15, + marginBottom: hp(15), + alignSelf: 'center', + }, + dateAdded: { + marginBottom: hp(30), + }, + descriptionTitleText: { + marginTop: hp(8), + lineHeight: 17, + fontSize: 14, + fontWeight: '500', + }, + descriptionText: { + fontSize: 12, + lineHeight: 14, + }, + nameText: { + fontSize: 14, + fontWeight: '500', + }, + iconContainer: { + alignItems: 'center', + }, +}); + +export default KeyCard; diff --git a/src/components/XPub/TransferPolicy.tsx b/src/components/XPub/TransferPolicy.tsx index b5db92c55..3884e97e2 100644 --- a/src/components/XPub/TransferPolicy.tsx +++ b/src/components/XPub/TransferPolicy.tsx @@ -57,7 +57,7 @@ function TransferPolicy({ const onPressNumber = (digit) => { let temp = policyText; if (digit !== 'x') { - temp += digit; + temp == null ? (temp = digit) : (temp += digit); setPolicyText(temp); } }; @@ -74,6 +74,7 @@ function TransferPolicy({ } useEffect(() => { + if (storedPolicy == '0' && !policyText) return; if (!policyText) { !isBitcoin ? setPolicyText(convertSatsToFiat(parseFloat(storedPolicy)).toFixed(0).toString()) diff --git a/src/constants/defaultData.tsx b/src/constants/defaultData.tsx index 4536c33c6..e2cb651bb 100644 --- a/src/constants/defaultData.tsx +++ b/src/constants/defaultData.tsx @@ -66,7 +66,7 @@ export const securityTips = [ 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.', + 'You can change your subscription at any time within the app or through your App Store/Play Store subscription details.', }, { title: 'Keep your signing devices safe', @@ -80,3 +80,4 @@ export const getSecurityTip = () => { const selected = Math.floor(cryptoRandom() * securityTips.length); // Comment for creating wallet modal WP return securityTips[selected]; }; +export const RECOVERY_KEY_SIGNER_NAME = 'RECOVERY_KEY_SIGNER_NAME'; diff --git a/src/context/Localization/language/en.json b/src/context/Localization/language/en.json index c72d83f5f..74d338b1d 100644 --- a/src/context/Localization/language/en.json +++ b/src/context/Localization/language/en.json @@ -144,7 +144,7 @@ "EnterNew": "Enter New", "Passcode": "Passcode", "ReEnter": "Re Enter", - "newPasscode": "Enter a New passcode", + "newPasscode": "Enter a new passcode", "confirmNewPasscode": "Confirm the new passcode", "MismatchPasscode": "Passcodes do NOT match", "enter_your": "Enter your ", @@ -534,7 +534,10 @@ "highCustom": "Network fee is more than 10% of the amount being sent. Consider using custom fee option.", "highWait": "Network fee is 10% higher than usual. If not urgent, consider waiting for the fee to go down.", "highUsual": "Network fee is within usual range or lower. A good time to broadcast transactions.", - "lowFee": "Network fee is less than 10% of the amount being sent." + "lowFee": "Network fee is less than 10% of the amount being sent.", + "discardTnxTitle": "Invalid UTXO set", + "discardTnxSubTitle": "This transaction's UTXO has already been used.", + "discardTnxDesc": "Please discard this transaction and initiate a new one" }, "vault": { "SetupyourVault": "Setup a Vault", @@ -630,7 +633,8 @@ "archivedVaultsTitle": "Archived Vaults", "archivedVaultsNote": "These are previous versions of this vault for reference or withdrawn if funds remaining.", "archivedVaultEmptyTitle": "View Archived Vaults", - "archivedVaultEmptySubtitle": "All archived vaults for this vault would be available here." + "archivedVaultEmptySubtitle": "All archived vaults for this vault would be available here.", + "verifyAddDesc": "Please download the Bitcoin Keeper desktop app from our website to connect" }, "seed": { "EnterSeed": "Enter Seeds", @@ -656,7 +660,10 @@ "mobileKeyVerified": "Mobile Key verified successfully", "seedWordVerified": "Seed Words verified successfully", "keeperVerified": "Keeper Verified Successfully", - "SeedErrorToast": "Please enter correct seed word" + "SeedErrorToast": "Please enter correct seed word", + "appRecoveredSuccessfulTitle": "App Recovered Successfully!", + "appRecoveredSuccessfulSubTitle": "All your wallets and data about your vault has been recovered", + "appRecoveredSuccessfulDesc": "The vault keys always stay within the signing devices and therefore do not have to be recovered" }, "healthcheck": { "ChangeSigningDevice": "Change signer", @@ -850,6 +857,12 @@ "SingerSettingsSubtitle": "For all keys", "ManageWalletsEmptyTitle": "Hide Unhide Wallets", "ManageWalletsEmptySubtitle": "Avoid clutter on the homescreen and keep wallets away from prying eyes.", + "DeleteWalletModalTitle": "You have funds in your wallet", + "DeleteVaultModalTitle": "Delete vault with funds?", + "DeleteWalletModalSubTitle": "You have sats in your wallet. Are you sure you want to delete it?", + "DeleteVaultModalSubTitle": "You are about to delete a vault with sats in it. Please ensure that you have backed-up your keys and vault descriptor to retrieve the vault later.", + "DeleteWalletModalDesc": "To delete this wallet, please transfer your funds to another wallet or vault first.", + "DeleteVaultModalDesc": "This action is irreversible without the keys and descriptor backed-up. Proceed to delete vault?", "RKHealthCheckTitle": "Health Check", "RKHealthCheckSubtitle": "For the Recovery Phrase", "RKHealthCheckDesc": "Please Back up your Recovery Key in order to change your current Passcode", @@ -882,7 +895,7 @@ "choosePlantitle": "Manage Subscription", "choosePlanSubTitle": "You are currently on the basic plan", "choosePlanSubtitle": "Upgrade or downgrade", - "noteSubTitle": "Restore Purchases defaults to your previous plan if issues arise", + "noteSubTitle": "Restore Purchases defaults to your previous plans if issue arises*", "confirming": "Confirming your subscription", "pleaseStay": "Please stay on this screen as it may take a little while...", "youCanChange": "You can change your subscription at anytime from within the app or from your subscription details in your profile", @@ -989,10 +1002,29 @@ "MKHealthCheckModalDesc": "Mobile Key is derived from the app’s master key using BIP-85", "hideSignerTitle": "Hide/Unhide Keys", "hideSignerSubtitle": "Hiding keys ensures only the ones you frequently use are visible in Manage Keys while the others are readily available here.", - "signerAddedSuccessMessage": "Signer added Successfully!", + "signerAddedSuccessMessage": "Signer Added Successfully!", "signerAvailableMessage": "The signer key is now available to use for creating vaults", - "signerDeatils": "Signer Details", - "signerAddedDesc": "Perform regular health checks on your signer key" + "signerDetails": "Signer Details", + "signerAddedDesc": "Perform regular health checks on your signer key", + "hiddenKeys": "Hidden Keys", + "showingHiddenKeys": "Unhide or delete keys", + "delete": "Delete", + "unhide": "Unhide", + "deletingKey": "Deleting Key", + "keyWillBeDeleted": "Your Key will be deleted, to use the key again you will have to make a new Key", + "keyDeletedSuccessfully": "Key Deleted Successfully", + "keyDeletedSuccessMessage": "Your Key has been deleted successfully. You will have to add the Key again", + "manageKeys": "Manage Keys", + "deleteVaultWarning": "Key is being used for Vault", + "vaultWarningSubtitle": "The Key you are trying to hide is used in one of the visible vaults.", + "deleteVaultInstruction": "Please delete the vault to perform this operation.", + "viewVault": "View Vault", + "back": "Back", + "enterPasscode": "Enter Passcode", + "confirmPasscodeToDeleteKey": "Confirm passcode to delete key", + "thisStepTakesTime": "This step will take a few seconds.", + "forgot2FA": "Forgot 2FA", + "forgot2FANote": "If you have lost your 2FA app, it is recommended that you remove SS and add a different key or SS again" }, "inheritancePlanning": { "commingSoon": "COMING SOON", diff --git a/src/context/Localization/language/es.json b/src/context/Localization/language/es.json index afa4c5854..17cd9ef15 100644 --- a/src/context/Localization/language/es.json +++ b/src/context/Localization/language/es.json @@ -145,7 +145,7 @@ "welcome": "Welcome", "EnterNew": "Enter New", "Passcode": "Passcode", - "newPasscode": "Enter a New passcode", + "newPasscode": "Enter a new passcode", "confirmNewPasscode": "Confirm the new passcode", "ReEnter": "Re Enter", "MismatchPasscode": "Passcodes do NOT match", @@ -534,7 +534,10 @@ "highCustom": "Network fee is more than 10% of the amount being sent. Consider using custom fee option.", "highWait": "Network fee is 10% higher than usual. If not urgent, consider waiting for the fee to go down.", "highUsual": "Network fee is within usual range or lower. A good time to broadcast transactions.", - "lowFee": "Network fee is less than 10% of the amount being sent." + "lowFee": "Network fee is less than 10% of the amount being sent.", + "discardTnxTitle": "Invalid UTXO set", + "discardTnxSubTitle": "This transaction's UTXO has already been used.", + "discardTnxDesc": "Please discard this transaction and initiate a new one" }, "vault": { "SetupyourVault": "Setup a Vault", @@ -630,7 +633,8 @@ "archivedVaultsTitle": "Archived Vaults", "archivedVaultsNote": "These are previous versions of this vault for reference or withdrawn if funds remaining.", "archivedVaultEmptyTitle": "View Archived Vaults", - "archivedVaultEmptySubtitle": "All archived vaults for this vault would be available here." + "archivedVaultEmptySubtitle": "All archived vaults for this vault would be available here.", + "verifyAddDesc": "Please download the Bitcoin Keeper desktop app from our website to connect" }, "seed": { "EnterSeed": "Enter Seeds", @@ -654,7 +658,10 @@ "mobileKeyVerified": "Mobile Key verified successfully", "seedWordVerified": "Seed Words verified successfully", "keeperVerified": "Keeper Verified Successfully", - "SeedErrorToast": "Please enter correct seed word" + "SeedErrorToast": "Please enter correct seed word", + "appRecoveredSuccessfulTitle": "App Recovered Successfully!", + "appRecoveredSuccessfulSubTitle": "All your wallets and data about your vault has been recovered", + "appRecoveredSuccessfulDesc": "The vault keys always stay within the signing devices and therefore do not have to be recovered" }, "healthcheck": { "ChangeSigningDevice": "Change signer", @@ -845,6 +852,12 @@ "SingerSettingsSubtitle": "For all keys", "ManageWalletsEmptyTitle": "Hide Unhide Wallets", "ManageWalletsEmptySubtitle": "Avoid clutter on the homescreen and keep wallets away from prying eyes.", + "DeleteWalletModalTitle": "You have funds in your wallet", + "DeleteVaultModalTitle": "Delete vault with funds?", + "DeleteWalletModalSubTitle": "You have sats in your wallet. Are you sure you want to delete it?", + "DeleteVaultModalSubTitle": "You are about to delete a vault with sats in it. Please ensure that you have backed-up your keys and vault descriptor to retrieve the vault later.", + "DeleteWalletModalDesc": "To delete this wallet, please transfer your funds to another wallet or vault first.", + "DeleteVaultModalDesc": "This action is irreversible without the keys and descriptor backed-up. Proceed to delete vault?", "RKHealthCheckTitle": "Health Check", "RKHealthCheckSubtitle": "For the Recovery Phrase", "RKHealthCheckDesc": "Please Back up your Recovery Key in order to change your current Passcode", @@ -877,7 +890,7 @@ "choosePlantitle": "Manage Subscription", "choosePlanSubTitle": "You are currently on the basic plan", "choosePlanSubtitle": "Upgrade or downgrade", - "noteSubTitle": "Restore Purchases defaults to your previous plan if issues arise", + "noteSubTitle": "Restore Purchases defaults to your previous plans if issue arises*", "confirming": "Confirming your subscription", "pleaseStay": "Please stay on this screen as it may take a little while...", "youCanChange": "You can change your subscription at anytime from within the app or from your subscription details in your profile", @@ -984,10 +997,29 @@ "MKHealthCheckModalDesc": "Mobile Key is derived from the app’s master key using BIP-85", "hideSignerTitle": "Hide/Unhide Keys", "hideSignerSubtitle": "Hiding keys ensures only the ones you frequently use are visible in Manage Keys while the others are readily available here.", - "signerAddedSuccessMessage": "Signer added Successfully!", + "signerAddedSuccessMessage": "Signer Added Successfully!", "signerAvailableMessage": "The signer key is now available to use for creating vaults", - "signerDeatils": "Signer Details", - "signerAddedDesc": "Perform regular health checks on your signer key" + "signerDetails": "Signer Details", + "signerAddedDesc": "Perform regular health checks on your signer key", + "hiddenKeys": "Hidden Keys", + "showingHiddenKeys": "Unhide or delete keys", + "delete": "Delete", + "unhide": "Unhide", + "deletingKey": "Deleting Key", + "keyWillBeDeleted": "Your Key will be deleted, to use the key again you will have to make a new Key", + "keyDeletedSuccessfully": "Key Deleted Successfully", + "keyDeletedSuccessMessage": "Your Key has been deleted successfully. You will have to add the Key again", + "manageKeys": "Manage Keys", + "deleteVaultWarning": "Key is being used for Vault", + "vaultWarningSubtitle": "The Key you are trying to hide is used in one of the visible vaults.", + "deleteVaultInstruction": "Please delete the vault to perform this operation.", + "viewVault": "View Vault", + "back": "Back", + "enterPasscode": "Enter Passcode", + "confirmPasscodeToDeleteKey": "Confirm passcode to delete key", + "thisStepTakesTime": "This step will take a few seconds.", + "forgot2FA": "Forgot 2FA", + "forgot2FANote": "If you have lost your 2FA app, it is recommended that you remove SS and add a different key or SS again" }, "inheritancePlanning": { "commingSoon": "COMING SOON", diff --git a/src/hardware/index.ts b/src/hardware/index.ts index 89f0a26f4..09b142813 100644 --- a/src/hardware/index.ts +++ b/src/hardware/index.ts @@ -128,7 +128,7 @@ export const getSignerDescription = ( } else if (signerType === SignerType.KEEPER) { return 'External'; } else { - return `Added ${moment(signer.addedOn).calendar()}`; + return `Added ${moment(signer.addedOn).calendar().toLowerCase()}`; } } if (signerType === SignerType.MY_KEEPER) { @@ -419,7 +419,7 @@ export const getSDMessage = ({ type }: { type: SignerType }) => { return 'Passport signers from Foundation Devices'; } case SignerType.BITBOX02: { - return 'Swiss Made signer from BitBox'; + return 'Swiss made signer from BitBox'; } case SignerType.SPECTER: { return 'A DIY signer from Spector Solutions'; @@ -428,7 +428,7 @@ export const getSDMessage = ({ type }: { type: SignerType }) => { return 'Open Source signer from keyst.one'; } case SignerType.JADE: { - return 'Great signer from Blockstream'; + return 'Simple and open source bitcoin wallet'; } case SignerType.MY_KEEPER: { return 'Use Mobile Key as signer'; diff --git a/src/hardware/signerSetup.ts b/src/hardware/signerSetup.ts index e2278ca2d..c351cd76f 100644 --- a/src/hardware/signerSetup.ts +++ b/src/hardware/signerSetup.ts @@ -18,6 +18,7 @@ import { extractColdCardExport } from './coldcard'; import { getLedgerDetailsFromChannel } from './ledger'; import { getTrezorDetails } from './trezor'; import { getBitbox02Details } from './bitbox'; +import { RECOVERY_KEY_SIGNER_NAME } from 'src/constants/defaultData'; const setupPassport = (qrData, isMultisig) => { const { xpub, derivationPath, masterFingerprint, forMultiSig, forSingleSig } = @@ -226,6 +227,13 @@ const setupSeedWordsBasedKey = (mnemonic: string, isMultisig: boolean) => { return { signer: softSigner, key }; }; +const setupRecoveryKeySigningKey = (primaryMnemonic: string) => { + const { signer: recoveryKeySigner } = setupSeedWordsBasedKey(primaryMnemonic, true); + recoveryKeySigner.hidden = true; + recoveryKeySigner.signerName = RECOVERY_KEY_SIGNER_NAME; + return recoveryKeySigner; +}; + const setupColdcard = (data, isMultisig) => { const { xpub, derivationPath, masterFingerprint, xpubDetails } = extractColdCardExport( data, @@ -307,4 +315,5 @@ export { setupLedger, setupTrezor, setupBitbox, + setupRecoveryKeySigningKey, }; diff --git a/src/hooks/useSigners.tsx b/src/hooks/useSigners.tsx index 0d934f5d2..bc2e35aa7 100644 --- a/src/hooks/useSigners.tsx +++ b/src/hooks/useSigners.tsx @@ -10,8 +10,8 @@ const useSigners = (vaultId = ''): { vaultSigners: Signer[]; signers: Signer[] } const vaultSigners = []; if (vaultId) { currentVault = vaults.filtered(`id == "${vaultId}"`)[0]; - const vaultKeys = (currentVault as Vault).signers; - vaultKeys.forEach((key) => { + const vaultKeys = (currentVault as Vault)?.signers; + vaultKeys?.forEach((key) => { const signer = signers.filtered(`masterFingerprint == "${key.masterFingerprint}"`)[0]; vaultSigners.push(signer.toJSON()); }); diff --git a/src/models/interfaces/HeathCheckTypes.ts b/src/models/interfaces/HeathCheckTypes.ts index 5fcbb9891..8cdbd7505 100644 --- a/src/models/interfaces/HeathCheckTypes.ts +++ b/src/models/interfaces/HeathCheckTypes.ts @@ -6,4 +6,5 @@ export enum hcStatusType { HEALTH_CHECK_SIGNING = 'HEALTH_CHECK_SIGNING', HEALTH_CHECK_SD_ADDITION = 'HEALTH_CHECK_ADDITION', HEALTH_CHECK_REGISTRATION = 'HEALTH_CHECK_REGISTRATION', + HEALTH_CHECK_VERIFICATION = 'HEALTH_CHECK_VERIFICATION', } diff --git a/src/navigation/Navigator.tsx b/src/navigation/Navigator.tsx index 28cdf65be..8fa7fdd3f 100644 --- a/src/navigation/Navigator.tsx +++ b/src/navigation/Navigator.tsx @@ -123,6 +123,7 @@ import PassportConfigRecovery from 'src/screens/SigningDevices/PassportConfigRec import { useAppSelector } from 'src/store/hooks'; import { AppStackParams } from './types'; import Login from '../screens/LoginScreen/Login'; +import VerifyAddressSelectionScreen from 'src/screens/Recieve/VerifyAddressSelectionScreen'; const defaultTheme = { ...DefaultTheme, @@ -195,6 +196,10 @@ function AppStack() { + diff --git a/src/navigation/themes.js b/src/navigation/themes.js index 7350e1fb5..15a2d0053 100644 --- a/src/navigation/themes.js +++ b/src/navigation/themes.js @@ -72,7 +72,8 @@ export const customTheme = extendTheme({ dashedButtonBackground: Colors.lightGreen, dashedButtonContent: Colors.White, choosePlanHome: Colors.White, - choosePlanCard: Colors.SmokeGreen, + choosePlanCard: Colors.Seashell, + choosePlanInactiveText: Colors.Black, choosePlanIconBackSelected: Colors.DeepOlive, choosePlanIconBack: Colors.PaleKhaki, hexagonIconBackColor: Colors.deepTeal, @@ -114,6 +115,7 @@ export const customTheme = extendTheme({ inActiveMsg: Colors.RichBlack, vaultCardText: Colors.Bisque, walletTypePillBack: Colors.Bisque, + pillPlaceholderBack: Colors.LightKhaki, satsDark: Colors.DeepSpaceGreen, gradientStart: Colors.GenericViridian, // linearGradient gradientEnd: Colors.RichGreen, // linearGradient @@ -157,7 +159,7 @@ export const customTheme = extendTheme({ DarkSage: Colors.DarkSage, // Smoke: Colors.Smoke, // deepTeal: Colors.deepTeal, - // ChampagneBliss: Colors.ChampagneBliss, + ChampagneBliss: Colors.ChampagneBliss, // PearlGrey: Colors.PearlGrey, // Taupe: Colors.Taupe, // Crayola: Colors.Crayola, @@ -197,7 +199,8 @@ export const customTheme = extendTheme({ dashedButtonBackground: Colors.ForestGreenDark, dashedButtonContent: Colors.Black, choosePlanHome: Colors.White, - choosePlanCard: Colors.SmokeGreen, + choosePlanCard: Colors.Seashell, + choosePlanInactiveText: Colors.Black, choosePlanIconBackSelected: Colors.DeepOlive, choosePlanIconBack: Colors.PaleKhaki, hexagonIconBackColor: Colors.deepTeal, @@ -239,6 +242,7 @@ export const customTheme = extendTheme({ inActiveMsg: Colors.SpanishGray, vaultCardText: Colors.Bisque, walletTypePillBack: Colors.BisqueDark, + pillPlaceholderBack: Colors.BisqueDark, satsDark: Colors.DeepSpaceGreen, gradientStart: Colors.GenericViridian, // linearGradient gradientEnd: Colors.DeepAquamarine, // linearGradient @@ -283,7 +287,7 @@ export const customTheme = extendTheme({ // deepTeal: Colors.deepTeal, // Champagne: Colors.Champagne, // Ivory: Colors.Ivory, - // ChampagneBliss: Colors.ChampagneBliss, + ChampagneBliss: Colors.pantoneGreenLight, // PearlGrey: Colors.PearlGrey, // Taupe: Colors.Taupe, // Crayola: Colors.Crayola, diff --git a/src/navigation/types.ts b/src/navigation/types.ts index d8dade76a..d1866d452 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -22,6 +22,7 @@ export type AppStackParams = { Send: undefined; UTXOLabeling: undefined; Receive: undefined; + VerifyAddressSelectionScreen: undefined; ChangeLanguage: undefined; ChoosePlan: undefined; EnterWalletDetail: undefined; diff --git a/src/screens/AddWalletScreen/AddWallet.tsx b/src/screens/AddWalletScreen/AddWallet.tsx index 01fa7fb01..1f5b9e9f9 100644 --- a/src/screens/AddWalletScreen/AddWallet.tsx +++ b/src/screens/AddWalletScreen/AddWallet.tsx @@ -152,12 +152,12 @@ function AddWallet({ navigation }) { Content={AddWalletContent} showCloseIcon={false} learnMore - learnMoreTitle={common.needMoreHelp} + learnMoreTitle={common.needHelp} learnMoreCallback={() => { setVisibleModal(false); dispatch(goToConcierge([ConciergeTag.WALLET], 'add-wallet')); }} - buttonText={common.continue} + buttonText={common.ok} buttonTextColor={`${colorMode}.modalWhiteButtonText`} buttonBackground={`${colorMode}.modalWhiteButton`} buttonCallback={() => setVisibleModal(false)} diff --git a/src/screens/AppSettings/AppBackupSettings.tsx b/src/screens/AppSettings/AppBackupSettings.tsx index d04e686bc..898613ea7 100644 --- a/src/screens/AppSettings/AppBackupSettings.tsx +++ b/src/screens/AppSettings/AppBackupSettings.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import { Box, ScrollView, useColorMode } from 'native-base'; import { StyleSheet } from 'react-native'; import { useQuery } from '@realm/react'; @@ -13,21 +13,117 @@ import { LocalizationContext } from 'src/context/Localization/LocContext'; import { RealmSchema } from 'src/storage/realm/enum'; import { getJSONFromRealmObject } from 'src/storage/realm/utils'; import WalletFingerprint from 'src/components/WalletFingerPrint'; +import usePlan from 'src/hooks/usePlan'; +import ActivityIndicatorView from 'src/components/AppActivityIndicator/ActivityIndicatorView'; +import idx from 'idx'; +import { VaultType, XpubTypes } from 'src/services/wallets/enums'; +import useToastMessage from 'src/hooks/useToastMessage'; +import { VaultSigner } from 'src/services/wallets/interfaces/vault'; +import WalletUtilities from 'src/services/wallets/operations/utils'; +import config from 'src/utils/service-utilities/config'; +import { generateVaultId } from 'src/services/wallets/factories/VaultFactory'; +import useCanaryVault from 'src/hooks/useCanaryWallets'; +import { captureError } from 'src/services/sentry'; +import { useDispatch } from 'react-redux'; +import { addNewVault } from 'src/store/sagaActions/vaults'; +import { useAppSelector } from 'src/store/hooks'; +import { resetRealyVaultState } from 'src/store/reducers/bhr'; +import useSigners from 'src/hooks/useSigners'; +import { NewVaultInfo } from 'src/store/sagas/wallets'; function AppBackupSettings() { const { colorMode } = useColorMode(); const navigation = useNavigation(); + const { translations } = useContext(LocalizationContext); + const { settings, common } = translations; const { primaryMnemonic, publicId } = useQuery(RealmSchema.KeeperApp).map( getJSONFromRealmObject )[0]; - + const { isOnL2Above: isCanaryWalletAllowed } = usePlan(); + let signer = useSigners().signers.find((data) => data.masterFingerprint === publicId); + const CANARY_SCHEME = { m: 1, n: 1 }; + const { allCanaryVaults } = useCanaryVault({ getAll: true }); const [confirmPassVisible, setConfirmPassVisible] = useState(false); + const [canaryVaultLoading, setCanaryVaultLoading] = useState(false); + const [canaryWalletId, setCanaryWalletId] = useState(); + const dispatch = useDispatch(); + const { showToast } = useToastMessage(); - const { translations } = useContext(LocalizationContext); - const { settings, common } = translations; + const { relayVaultUpdate, relayVaultError, realyVaultErrorMessage } = useAppSelector( + (state) => state.bhr + ); + + useEffect(() => { + if (relayVaultUpdate) { + navigation.navigate('VaultDetails', { vaultId: canaryWalletId }); + setCanaryVaultLoading(false); + dispatch(resetRealyVaultState()); + } + if (relayVaultError) { + showToast(`Canary wallet creation failed ${realyVaultErrorMessage}`); + dispatch(resetRealyVaultState()); + setCanaryVaultLoading(false); + } + }, [relayVaultUpdate, relayVaultError]); + + const handleCanaryWallet = () => { + setCanaryVaultLoading(true); + try { + const singleSigSigner = idx(signer, (_) => { + return _.signerXpubs[XpubTypes.P2WPKH][0]; + }); + if (!singleSigSigner) { + showToast('No single Sig found'); + setCanaryVaultLoading(false); + } else { + const ssVaultKey: VaultSigner = { + ...singleSigSigner, + masterFingerprint: publicId, + xfp: WalletUtilities.getFingerprintFromExtendedKey( + singleSigSigner.xpub, + WalletUtilities.getNetworkByType(config.NETWORK_TYPE) + ), + }; + const canaryVaultId = generateVaultId([ssVaultKey], CANARY_SCHEME); + setCanaryWalletId(canaryVaultId); + const canaryVault = allCanaryVaults.find((vault) => vault.id === canaryVaultId); + if (canaryVault) { + navigation.navigate('VaultDetails', { vaultId: canaryVaultId, signer: signer }); + setCanaryVaultLoading(false); + } else { + createCreateCanaryWallet(ssVaultKey); + } + } + } catch (err) { + console.log('🚀 ~ handleCanaryWal ~ err:', err); + } + }; + + const createCreateCanaryWallet = useCallback( + (ssVaultKey) => { + try { + const vaultInfo: NewVaultInfo = { + vaultType: VaultType.CANARY, + vaultScheme: CANARY_SCHEME, + vaultSigners: [ssVaultKey], + vaultDetails: { + name: 'Canary Wallet', + description: `Canary Wallet for Recovery Key`, + }, + }; + dispatch(addNewVault({ newVaultInfo: vaultInfo })); + return vaultInfo; + } catch (err) { + captureError(err); + return false; + } + }, + [signer] + ); return ( + + {isCanaryWalletAllowed && ( + + )} setConfirmPassVisible(false)} title="Confirm Passcode" subTitleWidth={wp(240)} - subTitle="To backup app recovery key" + subTitle="To backup app Recovery Key" modalBackground={`${colorMode}.modalWhiteBackground`} subTitleColor={`${colorMode}.secondaryText`} textColor={`${colorMode}.primaryText`} diff --git a/src/screens/AppSettings/ManageWallets.tsx b/src/screens/AppSettings/ManageWallets.tsx index d4db7f72e..f15d5ae84 100644 --- a/src/screens/AppSettings/ManageWallets.tsx +++ b/src/screens/AppSettings/ManageWallets.tsx @@ -46,8 +46,7 @@ enum PasswordMode { DEFAULT = 'DEFAULT', SHOWALL = 'SHOWALL', } - -function ListItem({ title, subtitle, balance, visibilityToggle, isHidden, onDelete, icon }) { +function ListItem({ title, subtitle, balance, visibilityToggle, isHidden, onDelete, icon, type }) { const { colorMode } = useColorMode(); const { getSatUnit, getBalance, getCurrencyIcon } = useBalance(); @@ -77,7 +76,9 @@ function ListItem({ title, subtitle, balance, visibilityToggle, isHidden, onDele - {isHidden && } />} + {isHidden && (type == 'VAULT' || (type == 'WALLET' && balance === 0)) && ( + } /> + )} - {`To delete this ${ - isWallet ? 'wallet' : 'vault' - }, please transfer your funds to another wallet or vault first.`} + {isWallet ? settings.DeleteWalletModalDesc : settings.DeleteVaultModalDesc} { - updateWalletVisibility(selectedWallet, true, false); - setShowDeleteVaultBalanceAlert(false); + if (isWallet) { + // Cancel action + updateWalletVisibility(selectedWallet, true, false); + setShowDeleteVaultBalanceAlert(false); + } else { + // Delete Vault action + setShowDeleteVaultBalanceAlert(false); + setConfirmPasscodeVisible(true); + } }} activeOpacity={0.5} > - Cancel + {isWallet ? 'Cancel' : 'Continue'} @@ -305,10 +311,12 @@ function ManageWallets() { /> {!showAll && visibleWallets.length === 0 ? ( - + {settings.ManageWalletsEmptyTitle} - {settings.ManageWalletsEmptySubtitle} + + {settings.ManageWalletsEmptySubtitle} + ) : ( @@ -319,6 +327,7 @@ function ManageWallets() { renderItem={({ item }) => ( { if (item.specs.balances.confirmed + item.specs.balances.unconfirmed > 0) { + setSelectedWallet(item); setShowDeleteVaultBalanceAlert(true); } else { setSelectedWallet(item); @@ -393,19 +403,18 @@ function ManageWallets() { dismissible close={() => { setShowDeleteVaultBalanceAlert(false); + !isWallet && updateWalletVisibility(selectedWallet, true, false); }} visible={showDeleteVaultBalanceAlert} - title={`You have funds in your ${isWallet ? 'wallet' : 'vault'}`} - subTitle={`You have sats in your ${ - isWallet ? 'wallet' : 'vault' - }. Are you sure you want to delete it?`} + title={isWallet ? settings.DeleteWalletModalTitle : settings.DeleteVaultModalTitle} + subTitle={isWallet ? settings.DeleteWalletModalSubTitle : settings.DeleteVaultModalSubTitle} textColor={`${colorMode}.primaryText`} modalBackground={`${colorMode}.modalWhiteBackground`} Content={DeleteVaultBalanceAlertModalContent} subTitleColor={`${colorMode}.secondaryText`} buttonTextColor={`${colorMode}.white`} subTitleWidth={wp(240)} - closeOnOverlayClick={false} + closeOnOverlayClick={isWallet ? false : true} showButtons showCloseIcon={false} /> @@ -541,9 +550,13 @@ const styles = StyleSheet.create({ flex: 1, }, emptyText: { + fontSize: 15, + lineHeight: 20, marginBottom: hp(3), }, emptySubText: { + fontSize: 14, + lineHeight: 20, width: wp(250), textAlign: 'center', marginBottom: hp(30), diff --git a/src/screens/AppSettings/PrivacyAndDisplay.tsx b/src/screens/AppSettings/PrivacyAndDisplay.tsx index 59027f0af..b2a63d7ba 100644 --- a/src/screens/AppSettings/PrivacyAndDisplay.tsx +++ b/src/screens/AppSettings/PrivacyAndDisplay.tsx @@ -14,7 +14,7 @@ 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, TouchableOpacity } from 'react-native'; +import { InteractionManager, StyleSheet, TouchableOpacity } from 'react-native'; import { hp, wp } from 'src/constants/responsive'; import Note from 'src/components/Note/Note'; import { sentryConfig } from 'src/services/sentry'; @@ -56,7 +56,7 @@ function ConfirmPasscode({ oldPassword, setConfirmPasscodeModal }) { useEffect(() => { if (credsChanged === 'changed') { setConfirmPasscodeModal(false); - showToast('Password Successfully updated!'); + showToast('Passcode updated successfully'); } }, [credsChanged]); @@ -218,28 +218,32 @@ function PrivacyAndDisplay({ route }) { const { inProgress, start } = useAsync(); const data = useQuery(RealmSchema.BackupHistory); const app: KeeperApp = useQuery(RealmSchema.KeeperApp).map(getJSONFromRealmObject)[0]; - const analyticsEnabled = app.enableAnalytics; + const [isToggling, setIsToggling] = useState(false); + const [analyticsEnabled, setAnalyticsEnabled] = useState(app.enableAnalytics); const toggleSentryReports = async () => { - if (inProgress) { - return; - } + if (inProgress || isToggling) return; + setIsToggling(true); dbManager.updateObjectById(RealmSchema.KeeperApp, app.id, { enableAnalytics: !analyticsEnabled, }); try { - if (!analyticsEnabled) { - await start(() => Sentry.init(sentryConfig)); - } else { - await start(() => Sentry.init({ ...sentryConfig, enabled: false })); - } + InteractionManager.runAfterInteractions(async () => { + if (!analyticsEnabled) { + await start(() => Sentry.init(sentryConfig)); + } else { + await start(() => Sentry.init({ ...sentryConfig, enabled: false })); + } + }); } catch (error) { dbManager.updateObjectById(RealmSchema.KeeperApp, app.id, { enableAnalytics: analyticsEnabled, }); console.error('Failed to toggle Sentry analytics:', error); + } finally { + setIsToggling(false); } }; @@ -360,7 +364,7 @@ function PrivacyAndDisplay({ route }) { visible={visiblePasscode} closeOnOverlayClick={false} close={() => setVisiblePassCode(false)} - title="Change passcode" + title={settings.changePasscode} subTitleWidth={wp(240)} subTitle="Enter your existing passcode" modalBackground={`${colorMode}.modalWhiteBackground`} diff --git a/src/screens/Channel/ConnectChannel.tsx b/src/screens/Channel/ConnectChannel.tsx index 4ef943f30..727041cad 100644 --- a/src/screens/Channel/ConnectChannel.tsx +++ b/src/screens/Channel/ConnectChannel.tsx @@ -10,12 +10,11 @@ import { LocalizationContext } from 'src/context/Localization/LocContext'; import { io } from 'src/services/channel'; import { BITBOX_HEALTHCHECK, - BITBOX_SETUP, - JOIN_CHANNEL, + CHANNEL_MESSAGE, LEDGER_HEALTHCHECK, - LEDGER_SETUP, TREZOR_HEALTHCHECK, - TREZOR_SETUP, + EMIT_MODES, + JOIN_CHANNEL, } from 'src/services/channel/constants'; import { CommonActions, useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; import { getBitbox02Details } from 'src/hardware/bitbox'; @@ -35,13 +34,19 @@ import MockWrapper from 'src/screens/Vault/MockWrapper'; import { setSigningDevices } from 'src/store/reducers/bhr'; import Text from 'src/components/KeeperText'; import crypto from 'crypto'; -import { createDecipheriv } from 'src/utils/service-utilities/utils'; +import { + createCipherGcm, + createDecipherGcm, + generateVaultAddressDescriptors, +} from 'src/utils/service-utilities/utils'; import useUnkownSigners from 'src/hooks/useUnkownSigners'; import { InteracationMode } from '../Vault/HardwareModalMap'; import { setupBitbox, setupLedger, setupTrezor } from 'src/hardware/signerSetup'; import useCanaryWalletSetup from 'src/hooks/UseCanaryWalletSetup'; import { healthCheckStatusUpdate } from 'src/store/sagaActions/bhr'; import { hcStatusType } from 'src/models/interfaces/HeathCheckTypes'; +import useVault from 'src/hooks/useVault'; +import { updateKeyDetails } from 'src/store/sagaActions/wallets'; function ScanAndInstruct({ onBarCodeRead, mode }) { const { colorMode } = useColorMode(); @@ -78,7 +83,7 @@ function ScanAndInstruct({ onBarCodeRead, mode }) { mode === InteracationMode.HEALTH_CHECK ? 'do a health check' : 'share the xPub' } from the Keeper Desktop App`} - + { '\u2022 If the web interface does not update, please make sure to stay on the same internet connection and rescan the QR code.' } @@ -88,6 +93,12 @@ function ScanAndInstruct({ onBarCodeRead, mode }) { ); } +const HEALTH_CHECK_TYPES = { + BITBOX02: BITBOX_HEALTHCHECK, + LEDGER: LEDGER_HEALTHCHECK, + TREZOR: TREZOR_HEALTHCHECK, +}; + function ConnectChannel() { const { colorMode } = useColorMode(); const route = useRoute(); @@ -99,6 +110,7 @@ function ConnectChannel() { mode, isMultisig, addSignerFlow = false, + vaultId, } = route.params as any; const [channel] = useState(io(config.CHANNEL_URL)); @@ -112,19 +124,92 @@ function ConnectChannel() { const navigation = useNavigation(); const { showToast } = useToastMessage(); + let descriptorString; + let receivingAddress; + + if (mode === InteracationMode.ADDRESS_VERIFICATION) { + const { activeVault: vault } = useVault({ vaultId }); + const resp = generateVaultAddressDescriptors(vault); + descriptorString = resp.descriptorString; + receivingAddress = resp.receivingAddress; + } + + const requestBody: RequestBody = { + action: + mode === InteracationMode.ADDRESS_VERIFICATION + ? EMIT_MODES.VERIFY_ADDRESS + : mode == EMIT_MODES.HEALTH_CHECK + ? EMIT_MODES.HEALTH_CHECK + : EMIT_MODES.ADD_DEVICE, + signerType, + }; + if (mode === InteracationMode.ADDRESS_VERIFICATION) { + requestBody.descriptorString = descriptorString ?? null; + requestBody.receivingAddress = receivingAddress ?? null; + } + const onBarCodeRead = ({ data }) => { decryptionKey.current = data; const sha = crypto.createHash('sha256'); sha.update(data); const room = sha.digest().toString('hex'); - channel.emit(JOIN_CHANNEL, { room, network: config.NETWORK_TYPE }); + const requestBody: RequestBody = { + action: + mode === InteracationMode.ADDRESS_VERIFICATION + ? EMIT_MODES.VERIFY_ADDRESS + : mode == EMIT_MODES.HEALTH_CHECK + ? EMIT_MODES.HEALTH_CHECK + : EMIT_MODES.ADD_DEVICE, + signerType, + }; + if (InteracationMode.ADDRESS_VERIFICATION) { + requestBody.descriptorString = descriptorString; + requestBody.receivingAddress = receivingAddress; + } + const requestData = createCipherGcm(JSON.stringify(requestBody), decryptionKey.current); + channel.emit(JOIN_CHANNEL, { room, network: config.NETWORK_TYPE, requestData }); }; useEffect(() => { - channel.on(BITBOX_SETUP, async (data) => { + channel.on(CHANNEL_MESSAGE, async ({ data }) => { + try { + const { data: decrypted } = createDecipherGcm(data, decryptionKey.current); + const responseData = decrypted.responseData.data; + if (mode == EMIT_MODES.HEALTH_CHECK) { + const type = HEALTH_CHECK_TYPES[signerType]; + await handleVerification(responseData, type); + } else if (mode == InteracationMode.ADDRESS_VERIFICATION) { + const resAdd = responseData.address; + if (resAdd != receivingAddress) return; + dispatch( + updateKeyDetails(signer, 'registered', { + registered: true, + vaultId, + }) + ); + dispatch( + healthCheckStatusUpdate([ + { + signerId: signer.masterFingerprint, + status: hcStatusType.HEALTH_CHECK_VERIFICATION, + }, + ]) + ); + navigation.goBack(); + showToast(`Address verified successfully`, ); + } else { + signerType == SignerType.LEDGER && ledgerSetup(responseData); + signerType == SignerType.BITBOX02 && bitBoxSetup(responseData); + signerType == SignerType.TREZOR && trezorSetup(responseData); + } + } catch (error) { + console.log('🚀 ~ channel.on ~ error:', error); + } + }); + + const bitBoxSetup = (signerData) => { try { - const decrypted = createDecipheriv(data, decryptionKey.current); - const { signer: bitbox02 } = setupBitbox(decrypted, isMultisig); + const { signer: bitbox02 } = setupBitbox(signerData, isMultisig); if (mode === InteracationMode.RECOVERY) { dispatch(setSigningDevices(bitbox02)); navigation.dispatch( @@ -154,30 +239,29 @@ function ConnectChannel() { // ignore if user cancels NFC interaction } else captureError(error); } - }); - channel.on(TREZOR_SETUP, async (data) => { + }; + const ledgerSetup = (signerData) => { try { - const decrypted = createDecipheriv(data, decryptionKey.current); - const { signer: trezor } = setupTrezor(decrypted, isMultisig); + const { signer: ledger } = setupLedger(signerData, isMultisig); if (mode === InteracationMode.RECOVERY) { - dispatch(setSigningDevices(trezor)); + dispatch(setSigningDevices(ledger)); navigation.dispatch( CommonActions.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }) ); } else if (mode === InteracationMode.CANARY_ADDITION) { - dispatch(addSigningDevice([trezor])); - createCreateCanaryWallet(trezor); + dispatch(addSigningDevice([ledger])); + createCreateCanaryWallet(ledger); } else { - dispatch(addSigningDevice([trezor])); + dispatch(addSigningDevice([ledger])); const navigationState = addSignerFlow ? { name: 'ManageSigners', - params: { addedSigner: trezor, addSignerFlow, showModal: true }, + params: { addedSigner: ledger, addSignerFlow, showModal: true }, } : { name: 'AddSigningDevice', merge: true, - params: { addedSigner: trezor, addSignerFlow, showModal: true }, + params: { addedSigner: ledger, addSignerFlow, showModal: true }, }; navigation.dispatch(CommonActions.navigate(navigationState)); } @@ -188,30 +272,29 @@ function ConnectChannel() { // ignore if user cancels NFC interaction } else captureError(error); } - }); - channel.on(LEDGER_SETUP, async (data) => { + }; + const trezorSetup = (signerData) => { try { - const decrypted = createDecipheriv(data, decryptionKey.current); - const { signer: ledger } = setupLedger(decrypted, isMultisig); + const { signer: trezor } = setupTrezor(signerData, isMultisig); if (mode === InteracationMode.RECOVERY) { - dispatch(setSigningDevices(ledger)); + dispatch(setSigningDevices(trezor)); navigation.dispatch( CommonActions.navigate('LoginStack', { screen: 'VaultRecoveryAddSigner' }) ); } else if (mode === InteracationMode.CANARY_ADDITION) { - dispatch(addSigningDevice([ledger])); - createCreateCanaryWallet(ledger); + dispatch(addSigningDevice([trezor])); + createCreateCanaryWallet(trezor); } else { - dispatch(addSigningDevice([ledger])); + dispatch(addSigningDevice([trezor])); const navigationState = addSignerFlow ? { name: 'ManageSigners', - params: { addedSigner: ledger, addSignerFlow, showModal: true }, + params: { addedSigner: trezor, addSignerFlow, showModal: true }, } : { name: 'AddSigningDevice', merge: true, - params: { addedSigner: ledger, addSignerFlow, showModal: true }, + params: { addedSigner: trezor, addSignerFlow, showModal: true }, }; navigation.dispatch(CommonActions.navigate(navigationState)); } @@ -222,14 +305,14 @@ function ConnectChannel() { // ignore if user cancels NFC interaction } else captureError(error); } - }); + }; - const handleVerification = async (data, deviceType) => { + const handleVerification = async (signerData, deviceType) => { const handleSuccess = () => { dispatch( healthCheckStatusUpdate([ { - signerId: data.signer.masterFingerprint, + signerId: signerData.mfp, status: hcStatusType.HEALTH_CHECK_SUCCESSFULL, }, ]) @@ -242,27 +325,25 @@ function ConnectChannel() { navigation.dispatch(CommonActions.goBack()); dispatch( healthCheckStatusUpdate([ - { signerId: data.signer.masterFingerprint, status: hcStatusType.HEALTH_CHECK_FAILED }, + { signerId: signerData.mfp, status: hcStatusType.HEALTH_CHECK_FAILED }, ]) ); showToast(`${signer.signerName} verification failed`, ); }; try { - const decrypted = createDecipheriv(data, decryptionKey.current); let masterFingerprint, signerType; - switch (deviceType) { case LEDGER_HEALTHCHECK: - ({ masterFingerprint } = getLedgerDetailsFromChannel(decrypted, isMultisig)); + ({ masterFingerprint } = getLedgerDetailsFromChannel(signerData, isMultisig)); signerType = SignerType.LEDGER; break; case TREZOR_HEALTHCHECK: - ({ masterFingerprint } = getTrezorDetails(decrypted, isMultisig)); + ({ masterFingerprint } = getTrezorDetails(signerData, isMultisig)); signerType = SignerType.TREZOR; break; case BITBOX_HEALTHCHECK: - ({ masterFingerprint } = getTrezorDetails(decrypted, isMultisig)); + ({ masterFingerprint } = getTrezorDetails(signerData, isMultisig)); signerType = SignerType.BITBOX02; break; default: @@ -294,18 +375,6 @@ function ConnectChannel() { } }; - 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) => { - await handleVerification(data, BITBOX_HEALTHCHECK); - }); - return () => { channel.disconnect(); }; @@ -337,6 +406,13 @@ function ConnectChannel() { export default ConnectChannel; +type RequestBody = { + action: string; + signerType: string; + descriptorString?: string; + receivingAddress?: string; +}; + const styles = StyleSheet.create({ container: { marginVertical: 25, diff --git a/src/screens/ChoosePlanScreen/ChoosePlan.tsx b/src/screens/ChoosePlanScreen/ChoosePlan.tsx index 4f53a5785..44a19d03f 100644 --- a/src/screens/ChoosePlanScreen/ChoosePlan.tsx +++ b/src/screens/ChoosePlanScreen/ChoosePlan.tsx @@ -1,6 +1,16 @@ -import { ActivityIndicator, Platform, ScrollView, Alert, Linking, StyleSheet } from 'react-native'; +import { + ActivityIndicator, + Platform, + ScrollView, + Alert, + Linking, + StyleSheet, + TouchableOpacity, + Dimensions, + Pressable, +} from 'react-native'; import Text from 'src/components/KeeperText'; -import { Box, useColorMode, Pressable } from 'native-base'; +import { Box, useColorMode } from 'native-base'; import RNIap, { getSubscriptions, purchaseErrorListener, @@ -14,12 +24,11 @@ import ChoosePlanCarousel from 'src/components/Carousel/ChoosePlanCarousel'; import KeeperHeader from 'src/components/KeeperHeader'; import { KeeperApp } from 'src/models/interfaces/KeeperApp'; import { LocalizationContext } from 'src/context/Localization/LocContext'; -import Note from 'src/components/Note/Note'; import { RealmSchema } from 'src/storage/realm/enum'; import ScreenWrapper from 'src/components/ScreenWrapper'; import SubScription, { SubScriptionPlan } from 'src/models/interfaces/Subscription'; import dbManager from 'src/storage/realm/dbManager'; -import { wp } from 'src/constants/responsive'; +import { wp, hp } from 'src/constants/responsive'; import Relay from 'src/services/backend/Relay'; import moment from 'moment'; import { getBundleId } from 'react-native-device-info'; @@ -36,8 +45,13 @@ import KeeperTextInput from 'src/components/KeeperTextInput'; import CustomGreenButton from 'src/components/CustomButton/CustomGreenButton'; import Colors from 'src/theme/Colors'; import TierUpgradeModal from './TierUpgradeModal'; +import PlanCheckMark from 'src/assets/images/planCheckMark.svg'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import Buttons from 'src/components/Buttons'; +const { width } = Dimensions.get('window'); function ChoosePlan() { + const inset = useSafeAreaInsets(); const route = useRoute(); const navigation = useNavigation(); const initialPosition = route.params?.planPosition || 0; @@ -85,6 +99,11 @@ function ChoosePlan() { init(); }, []); + useEffect(() => { + // To calculate same index as in ChoosePlanCarousel + setCurrentPosition(initialPosition !== 0 ? initialPosition : subscription.level - 1); + }, []); + async function init() { let data = []; try { @@ -294,13 +313,6 @@ function ChoosePlan() { setShowUpgradeModal(false); }; - const getBenifitsTitle = (name) => { - if (name === 'Diamond Hands') { - return `${name}`; - } - return `A ${name}`; - }; - const restorePurchases = async () => { try { setRequesting(true); @@ -346,25 +358,28 @@ function ChoosePlan() { const { colorMode } = useColorMode(); const [code, setcode] = useState(''); const [isInvalidCode, setIsInvalidCode] = useState(false); + const [activeOffer, setActiveOffer] = useState(null); - const onPressRedeem = async () => { + const validateOnFocusLost = async () => { + setActiveOffer(null); const plan = isMonthly ? items[currentPosition].monthlyPlanDetails : items[currentPosition].yearlyPlanDetails; + if (Platform.OS === 'android') { const promoCode = code.trim().toLowerCase(); if (items[currentPosition].promoCodes || items[currentPosition].promoCodes[promoCode]) { const offerId = items[currentPosition].promoCodes[promoCode]; - const offer = plan.offers.find((offer) => offer.offerId === offerId); + const offer = plan.offers.find((offer) => { + return offer.offerId === offerId; + }); if (offer) { let purchaseTokenAndroid = null; if (appSubscription.receipt) { purchaseTokenAndroid = JSON.parse(appSubscription.receipt).purchaseToken; } - setShowPromocodeModal(false); - requestSubscription({ - sku: plan.productId, - subscriptionOffers: [{ sku: plan.productId, offerToken: offer.offerToken }], + setActiveOffer({ + ...offer, purchaseTokenAndroid, }); } else { @@ -374,38 +389,80 @@ function ChoosePlan() { setIsInvalidCode(true); } } else { + // For iOS const offer = await Relay.getOffer(plan.productId, code.trim().toLowerCase()); if (offer && offer.signature) { - setShowPromocodeModal(false); - requestSubscription({ - sku: plan.productId, - subscriptionOffers: [{ sku: plan.productId, offerToken: offer.offerToken }], - withOffer: offer, - }); + setActiveOffer(offer); } else { setIsInvalidCode(true); } } }; + const onSubscribe = () => { + const plan = isMonthly + ? items[currentPosition].monthlyPlanDetails + : items[currentPosition].yearlyPlanDetails; + if (Platform.OS === 'android') { + setShowPromocodeModal(false); + requestSubscription({ + sku: plan.productId, + subscriptionOffers: [{ sku: plan.productId, offerToken: activeOffer.offerToken }], + purchaseTokenAndroid: activeOffer.purchaseTokenAndroid, + }); + } else { + setShowPromocodeModal(false); + requestSubscription({ + sku: plan.productId, + subscriptionOffers: [{ sku: plan.productId, offerToken: activeOffer.offerToken }], + withOffer: activeOffer, + }); + } + }; + return ( Enter Code { setcode(value.trim()); setIsInvalidCode(false); + setActiveOffer(null); }} + onFocus={() => setIsInvalidCode(false)} testID="input_setcode" /> - {isInvalidCode && Invalid promo code} - + + setShowPromocodeModal(false)} + /> + ); } + const getActionBtnTitle = () => { + const isSubscribed = items[currentPosition].productIds.includes( + subscription.productId.toLowerCase() + ); + if (isSubscribed) return 'Subscribed'; + return ( + 'Continue - ' + + (isMonthly + ? items[currentPosition]?.monthlyPlanDetails.price ?? 'Free' + : items[currentPosition]?.yearlyPlanDetails.price ?? 'Free') + ); + }; + return ( setShowPromocodeModal(false)} - title="Subscribe with Promo Code" - subTitle="To redeem discount, please enter the Code" + title="Subscribe with Promo code" + subTitle={`Please enter the code to redeem discount`} modalBackground={`${colorMode}.modalWhiteBackground`} subTitleColor={`${colorMode}.secondaryText`} textColor={`${colorMode}.primaryText`} @@ -445,7 +502,7 @@ function ChoosePlan() { buttonText={null} buttonCallback={() => {}} Content={PromocodeModalContent} - subTitleWidth={wp(210)} + subTitleWidth={wp(250)} /> - {currentPosition !== 0 && ( - setShowPromocodeModal(true)} - style={{ textAlign: 'center', marginTop: 10 }} - fontSize={16} - letterSpacing={0.16} - > - Have an offer code? - Redeem Now - - )} - - - - + - - {getBenifitsTitle(items[currentPosition].name)} + + {`Included in ${items[currentPosition].name}`} + + + + {choosePlan.restorePurchases} + + + {items?.[currentPosition]?.benifits.map( (i) => i !== '*Coming soon' && ( - + {` ${i}`} @@ -524,50 +577,72 @@ function ChoosePlan() { )} - - - - - - - {choosePlan.restorePurchases} - - - + + {/* BTM CTR */} + + {!loading && + items && + !items[currentPosition].productIds.includes(subscription.productId.toLowerCase()) && ( + <> + + + {formatString(choosePlan.noteSubTitle)} + + + + + processSubscription(items[currentPosition], currentPosition)} + value={getActionBtnTitle()} + fullWidth + /> + setShowPromocodeModal(true)} + visible={ + currentPosition != 0 && + !items[currentPosition].productIds.includes( + subscription.productId.toLowerCase() + ) + } + /> + + + + )} ); } + +const TextActionBtn = ({ value, onPress, visible }) => { + return ( + visible && onPress()} + style={{ alignSelf: 'center', opacity: visible ? 1 : 0, marginTop: 20 }} + > + + + {value} + + + + ); +}; + const styles = StyleSheet.create({ noteWrapper: { - bottom: 1, + flexDirection: 'column', + bottom: 0, margin: 1, - alignItems: 'flex-end', - flexDirection: 'row', - justifyContent: 'space-between', - width: '100%', + width, + position: 'absolute', }, restorePurchaseWrapper: { - padding: 3, - marginBottom: 5, - borderRadius: 5, - borderWidth: 0.7, - alignItems: 'center', - justifyContent: 'center', + marginTop: 5, + marginBottom: 15, }, comingSoonText: { fontSize: 10, @@ -586,8 +661,20 @@ const styles = StyleSheet.create({ alignSelf: 'center', }, restorePurchase: { - fontSize: 12, + fontSize: 13, letterSpacing: 0.24, + textDecorationLine: 'underline', + }, + ctaText: { + fontSize: 13, + letterSpacing: 1, + }, + divider: { + height: 1, + width, + backgroundColor: Colors.GrayX11, + alignSelf: 'center', + marginTop: 20, }, }); export default ChoosePlan; diff --git a/src/screens/CloudBackup/CloudBackupScreen.tsx b/src/screens/CloudBackup/CloudBackupScreen.tsx index 56b016e08..3a2392f01 100644 --- a/src/screens/CloudBackup/CloudBackupScreen.tsx +++ b/src/screens/CloudBackup/CloudBackupScreen.tsx @@ -164,6 +164,7 @@ function CloudBackupScreen() { textColor={`${colorMode}.modalGreenContent`} DarkCloseIcon={colorMode === 'dark'} learnMore + learnMoreTitle={common.needHelp} showCloseIcon={true} learnMoreCallback={() => { setShowModal(false); @@ -172,7 +173,7 @@ function CloudBackupScreen() { } dispatch(goToConcierge([ConciergeTag.SETTINGS], 'cloud-backup')); }} - buttonText={common.continue} + buttonText={common.ok} Content={() => modalContent()} buttonTextColor={`${colorMode}.modalWhiteButtonText`} buttonBackground={`${colorMode}.modalWhiteButton`} diff --git a/src/screens/EnterWalletDetailScreen/EnterWalletDetailScreen.tsx b/src/screens/EnterWalletDetailScreen/EnterWalletDetailScreen.tsx index cdc512376..5e94968bf 100644 --- a/src/screens/EnterWalletDetailScreen/EnterWalletDetailScreen.tsx +++ b/src/screens/EnterWalletDetailScreen/EnterWalletDetailScreen.tsx @@ -358,13 +358,17 @@ function EnterWalletDetailScreen({ route }) { Content={TapRootContent} showCloseIcon={true} DarkCloseIcon + buttonText={common.ok} + buttonCallback={() => setVisibleModal(false)} + buttonTextColor={`${colorMode}.modalWhiteButtonText`} + buttonBackground={`${colorMode}.modalWhiteButton`} learnMore learnMoreCallback={() => { setAdvancedSettingsVisible(false); setVisibleModal(false); dispatch(goToConcierge([ConciergeTag.WALLET], 'add-wallet-advanced-settings')); }} - learnMoreTitle={common.needMoreHelp} + learnMoreTitle={common.needHelp} /> ); diff --git a/src/screens/FeeInsights/FeeDataStats.tsx b/src/screens/FeeInsights/FeeDataStats.tsx index 22531b4d5..15297c695 100644 --- a/src/screens/FeeInsights/FeeDataStats.tsx +++ b/src/screens/FeeInsights/FeeDataStats.tsx @@ -64,7 +64,7 @@ const FeeDataStats = () => { return ( - 24 hours statistics + 24 Hours Statistics { suffix={''} stats={feeInsight.btc_data.btc_price_change_percent.toFixed(2) + '%'} /> - {currencyKind === 'BITCOIN' && } - {currencyKind === 'FIAT' && } + {currencyKind === 'BITCOIN' && ( + + )} + {currencyKind === 'FIAT' && ( + + )} ); @@ -120,7 +124,7 @@ const styles = StyleSheet.create({ titleLabel: { fontSize: 18, letterSpacing: 1, - fontFamily:Fonts.FiraSansRegular + fontFamily: Fonts.FiraSansRegular, }, cardWrapper: { flexDirection: 'row', diff --git a/src/screens/FeeInsights/FeeInsightsContent.tsx b/src/screens/FeeInsights/FeeInsightsContent.tsx index c29649066..2bec69f00 100644 --- a/src/screens/FeeInsights/FeeInsightsContent.tsx +++ b/src/screens/FeeInsights/FeeInsightsContent.tsx @@ -53,7 +53,7 @@ const FeeInsightsContent = () => { - BTC transaction fee insights + BTC Transaction Fee Insights {isLoading ? ( diff --git a/src/screens/ImportWalletScreen/ImportWalletScreen.tsx b/src/screens/ImportWalletScreen/ImportWalletScreen.tsx index fc2114d39..e5382a97e 100644 --- a/src/screens/ImportWalletScreen/ImportWalletScreen.tsx +++ b/src/screens/ImportWalletScreen/ImportWalletScreen.tsx @@ -180,11 +180,12 @@ function ImportWalletScreen() { Content={ImportWalletContent} DarkCloseIcon learnMore + learnMoreTitle={common.needHelp} learnMoreCallback={() => { setIntroModal(false); dispatch(goToConcierge([ConciergeTag.WALLET], 'import-wallet')); }} - buttonText="Continue" + buttonText={common.ok} buttonTextColor={`${colorMode}.modalWhiteButtonText`} buttonBackground={`${colorMode}.modalWhiteButton`} buttonCallback={() => setIntroModal(false)} diff --git a/src/screens/InheritanceToolsAndTips/InheritanceToolsAndTips.tsx b/src/screens/InheritanceToolsAndTips/InheritanceToolsAndTips.tsx index 3187bfe8f..969c723e4 100644 --- a/src/screens/InheritanceToolsAndTips/InheritanceToolsAndTips.tsx +++ b/src/screens/InheritanceToolsAndTips/InheritanceToolsAndTips.tsx @@ -113,14 +113,14 @@ function InheritanceToolsAndTips({ navigation }) { Content={InheritanceModalContent} DarkCloseIcon learnMore - learnMoreTitle={common.needMoreHelp} + learnMoreTitle={common.needHelp} learnMoreCallback={() => { setInheritanceModal(false); dispatch(goToConcierge([ConciergeTag.INHERITANCE], 'inheritance-tools-and-tips')); }} buttonTextColor={`${colorMode}.modalWhiteButtonText`} buttonBackground={`${colorMode}.modalWhiteButton`} - buttonText={common.continue} + buttonText={common.ok} buttonCallback={() => setInheritanceModal(false)} /> diff --git a/src/screens/QRScreens/RegisterWithChannel.tsx b/src/screens/QRScreens/RegisterWithChannel.tsx index 77f8343eb..13bf91b71 100644 --- a/src/screens/QRScreens/RegisterWithChannel.tsx +++ b/src/screens/QRScreens/RegisterWithChannel.tsx @@ -4,26 +4,23 @@ import KeeperHeader from 'src/components/KeeperHeader'; import ScreenWrapper from 'src/components/ScreenWrapper'; import { ActivityIndicator, StyleSheet } from 'react-native'; import { VaultSigner } from 'src/services/wallets/interfaces/vault'; -import { getWalletConfigForBitBox02 } from 'src/hardware/bitbox'; import config, { KEEPER_WEBSITE_BASE_URL } from 'src/utils/service-utilities/config'; import { RNCamera } from 'react-native-camera'; import { hp, windowWidth, wp } from 'src/constants/responsive'; import { io } from 'src/services/channel'; -import { - BITBOX_REGISTER, - JOIN_CHANNEL, - LEDGER_REGISTER, - REGISTRATION_SUCCESS, -} from 'src/services/channel/constants'; +import { CHANNEL_MESSAGE, EMIT_MODES, JOIN_CHANNEL } from 'src/services/channel/constants'; import { captureError } from 'src/services/sentry'; import { updateKeyDetails } from 'src/store/sagaActions/wallets'; import { useDispatch } from 'react-redux'; import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; import useVault from 'src/hooks/useVault'; import Text from 'src/components/KeeperText'; -import { SignerType } from 'src/services/wallets/enums'; import crypto from 'crypto'; -import { createCipheriv, createDecipheriv } from 'src/utils/service-utilities/utils'; +import { + createCipherGcm, + createDecipherGcm, + genrateOutputDescriptors, +} from 'src/utils/service-utilities/utils'; import useSignerFromKey from 'src/hooks/useSignerFromKey'; import { hcStatusType } from 'src/models/interfaces/HeathCheckTypes'; import { healthCheckStatusUpdate } from 'src/store/sagaActions/bhr'; @@ -59,7 +56,7 @@ function ScanAndInstruct({ onBarCodeRead }) { {'\u2022 Please resigter the vault from the Keeper Desktop App'} - + { '\u2022 If the web interface does not update, please make sure to stay on the same internet connection and rescan the QR code.' } @@ -72,33 +69,44 @@ function ScanAndInstruct({ onBarCodeRead }) { function RegisterWithChannel() { const { params } = useRoute(); const { colorMode } = useColorMode(); - const { vaultKey, vaultId } = params as { vaultKey: VaultSigner; vaultId: string }; + const navigation = useNavigation(); + const { vaultKey, vaultId, signerType } = params as { + vaultKey: VaultSigner; + vaultId: string; + signerType: string; + }; const { signer } = useSignerFromKey(vaultKey); const [channel] = useState(io(config.CHANNEL_URL)); const decryptionKey = useRef(); const dispatch = useDispatch(); - const navgation = useNavigation(); const { activeVault: vault } = useVault({ vaultId }); + const descriptorString = genrateOutputDescriptors(vault).split('\n')[0]; + const firstExtAdd = vault.specs.addresses.external[0]; // for cross validation from desktop app. const onBarCodeRead = ({ data }) => { decryptionKey.current = data; const sha = crypto.createHash('sha256'); sha.update(data); const room = sha.digest().toString('hex'); - channel.emit(JOIN_CHANNEL, { room, network: config.NETWORK_TYPE }); + const requestBody = { + action: EMIT_MODES.REGISTER_MULTISIG, + signerType, + descriptorString, + firstExtAdd, + }; + const requestData = createCipherGcm(JSON.stringify(requestBody), decryptionKey.current); + channel.emit(JOIN_CHANNEL, { room, network: config.NETWORK_TYPE, requestData }); }; useEffect(() => { - channel.on(BITBOX_REGISTER, async ({ room }) => { + channel.on(CHANNEL_MESSAGE, async ({ data }) => { try { - const walletConfig = getWalletConfigForBitBox02({ vault, signer }); - channel.emit(BITBOX_REGISTER, { - data: createCipheriv(JSON.stringify(walletConfig), decryptionKey.current), - room, - }); + const { data: decrypted } = createDecipherGcm(data, decryptionKey.current); + const resAdd = decrypted.responseData.data.address; + if (resAdd != firstExtAdd) return; dispatch( updateKeyDetails(vaultKey, 'registered', { registered: true, @@ -113,44 +121,11 @@ function RegisterWithChannel() { }, ]) ); - navgation.goBack(); + navigation.goBack(); } catch (error) { captureError(error); } }); - channel.on(LEDGER_REGISTER, async ({ room }) => { - try { - channel.emit(LEDGER_REGISTER, { - data: createCipheriv(JSON.stringify({ vault }), decryptionKey.current), - room, - }); - } catch (error) { - captureError(error); - } - }); - channel.on(REGISTRATION_SUCCESS, async ({ data }) => { - const decrypted = createDecipheriv(data, decryptionKey.current); - const { signerType, policy } = decrypted; - switch (signerType) { - case SignerType.LEDGER: - dispatch( - updateKeyDetails(vaultKey, 'registered', { - registered: true, - vaultId: vault.id, - registrationInfo: JSON.stringify({ registeredWallet: policy.policyHmac }), - }) - ); - dispatch( - healthCheckStatusUpdate([ - { - signerId: signer.masterFingerprint, - status: hcStatusType.HEALTH_CHECK_REGISTRATION, - }, - ]) - ); - navgation.goBack(); - } - }); return () => { channel.disconnect(); }; diff --git a/src/screens/QRScreens/ScanQR.tsx b/src/screens/QRScreens/ScanQR.tsx index b12cc4d9e..2e627f7e1 100644 --- a/src/screens/QRScreens/ScanQR.tsx +++ b/src/screens/QRScreens/ScanQR.tsx @@ -264,8 +264,9 @@ function ScanQR() { setVisibleModal(false); dispatch(goToConcierge([ConciergeTag.COLLABORATIVE_Wallet], 'add-co-signer')); }} - learnMoreTitle={common.needMoreHelp} + learnMoreTitle={common.needHelp} buttonCallback={() => setVisibleModal(false)} + buttonText={common.ok} buttonBackground={`${colorMode}.modalWhiteButton`} /> @@ -292,7 +293,6 @@ const styles = StyleSheet.create({ }, uploadButton: { position: 'absolute', - left: wp(90), zIndex: 999, justifyContent: 'center', }, diff --git a/src/screens/QRScreens/SignWithChannel.tsx b/src/screens/QRScreens/SignWithChannel.tsx index a1420c101..af393744e 100644 --- a/src/screens/QRScreens/SignWithChannel.tsx +++ b/src/screens/QRScreens/SignWithChannel.tsx @@ -8,13 +8,7 @@ import config, { KEEPER_WEBSITE_BASE_URL } from 'src/utils/service-utilities/con import { RNCamera } from 'react-native-camera'; import { hp, windowWidth, wp } from 'src/constants/responsive'; import { io } from 'src/services/channel'; -import { - BITBOX_SIGN, - JOIN_CHANNEL, - LEDGER_SIGN, - SIGNED_TX, - TREZOR_SIGN, -} from 'src/services/channel/constants'; +import { CHANNEL_MESSAGE, EMIT_MODES, JOIN_CHANNEL } from 'src/services/channel/constants'; import { useDispatch } from 'react-redux'; import { CommonActions, useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; import { useAppSelector } from 'src/store/hooks'; @@ -26,7 +20,7 @@ import { SignerType } from 'src/services/wallets/enums'; import { healthCheckStatusUpdate } from 'src/store/sagaActions/bhr'; import Text from 'src/components/KeeperText'; import crypto from 'crypto'; -import { createCipheriv, createDecipheriv } from 'src/utils/service-utilities/utils'; +import { createCipherGcm, createDecipherGcm } from 'src/utils/service-utilities/utils'; import useSignerFromKey from 'src/hooks/useSignerFromKey'; import { getPsbtForHwi } from 'src/hardware'; import { hcStatusType } from 'src/models/interfaces/HeathCheckTypes'; @@ -62,7 +56,7 @@ function ScanAndInstruct({ onBarCodeRead }) { {'\u2022 Please sign the transaction from the Keeper Desktop App'} - + { '\u2022 If the web interface does not update, please make sure to stay on the same internet connection and rescan the QR code.' } @@ -75,9 +69,14 @@ function ScanAndInstruct({ onBarCodeRead }) { function SignWithChannel() { const { colorMode } = useColorMode(); const { params } = useRoute(); - const { vaultKey, vaultId = '' } = params as { + const { + vaultKey, + vaultId = '', + signerType, + } = params as { vaultKey: VaultSigner; vaultId: string; + signerType: string; }; const { signer } = useSignerFromKey(vaultKey); const { activeVault } = useVault({ vaultId }); @@ -94,94 +93,47 @@ function SignWithChannel() { const dispatch = useDispatch(); const navgation = useNavigation(); - const onBarCodeRead = ({ data }) => { + const onBarCodeRead = async ({ data }) => { decryptionKey.current = data; const sha = crypto.createHash('sha256'); sha.update(data); const room = sha.digest().toString('hex'); - channel.emit(JOIN_CHANNEL, { room, network: config.NETWORK_TYPE }); + const psbt = await getPsbtForHwi(serializedPSBT, activeVault); + const requestBody = { + action: EMIT_MODES.SIGN_TX, + signerType, + psbt, + }; + const requestData = createCipherGcm(JSON.stringify(requestBody), decryptionKey.current); + channel.emit(JOIN_CHANNEL, { room, network: config.NETWORK_TYPE, requestData }); }; useEffect(() => { - channel.on(BITBOX_SIGN, async ({ room }) => { - const data = await getPsbtForHwi(serializedPSBT, activeVault); - channel.emit(BITBOX_SIGN, { - data: createCipheriv(JSON.stringify(data), decryptionKey.current), - room, - }); - }); - channel.on(TREZOR_SIGN, async ({ room }) => { - try { - const data = await getPsbtForHwi(serializedPSBT, activeVault); - channel.emit(TREZOR_SIGN, { - data: createCipheriv(JSON.stringify(data), decryptionKey.current), - room, - }); - } catch (err) { - captureError(err); - } - }); - channel.on(LEDGER_SIGN, async ({ room }) => { + channel.on(CHANNEL_MESSAGE, async ({ data }) => { try { - const data = await getPsbtForHwi(serializedPSBT, activeVault); - channel.emit(LEDGER_SIGN, { - data: createCipheriv(JSON.stringify(data), decryptionKey.current), - room, - }); - } catch (err) { - captureError(err); + const { data: decrypted } = createDecipherGcm(data, decryptionKey.current); + onSignedTnx(decrypted.responseData); + } catch (error) { + console.log('🚀 ~ channel.on ~ error:', error); } }); - channel.on(SIGNED_TX, ({ data }) => { + const onSignedTnx = (data) => { try { - const decrypted = createDecipheriv(data, decryptionKey.current); - if (signer.type === SignerType.TREZOR) { - const { signedSerializedPSBT } = decrypted; - dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp: vaultKey.xfp })); - navgation.dispatch( - CommonActions.navigate({ name: 'SignTransactionScreen', merge: true }) - ); - dispatch( - healthCheckStatusUpdate([ - { - signerId: data.signer.masterFingerprint, - status: hcStatusType.HEALTH_CHECK_SIGNING, - }, - ]) - ); - } else if (signer.type === SignerType.BITBOX02) { - const { signedSerializedPSBT } = decrypted; - dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp: vaultKey.xfp })); - navgation.dispatch( - CommonActions.navigate({ name: 'SignTransactionScreen', merge: true }) - ); - dispatch( - healthCheckStatusUpdate([ - { - signerId: data.signer.masterFingerprint, - status: hcStatusType.HEALTH_CHECK_SIGNING, - }, - ]) - ); - } else if (signer.type === SignerType.LEDGER) { - const { signedSerializedPSBT } = decrypted; - dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp: vaultKey.xfp })); - navgation.dispatch( - CommonActions.navigate({ name: 'SignTransactionScreen', merge: true }) - ); - dispatch( - healthCheckStatusUpdate([ - { - signerId: data.signer.masterFingerprint, - status: hcStatusType.HEALTH_CHECK_SIGNING, - }, - ]) - ); - } + const signedSerializedPSBT = data.data.signedSerializedPSBT; + dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp: vaultKey.xfp })); + navgation.dispatch(CommonActions.navigate({ name: 'SignTransactionScreen', merge: true })); + dispatch( + healthCheckStatusUpdate([ + { + signerId: signer.masterFingerprint, + status: hcStatusType.HEALTH_CHECK_SIGNING, + }, + ]) + ); } catch (error) { captureError(error); } - }); + }; return () => { channel.disconnect(); }; diff --git a/src/screens/Recieve/ReceiveScreen.tsx b/src/screens/Recieve/ReceiveScreen.tsx index 9907936f0..13f91f3ec 100644 --- a/src/screens/Recieve/ReceiveScreen.tsx +++ b/src/screens/Recieve/ReceiveScreen.tsx @@ -1,8 +1,8 @@ /* eslint-disable react/no-unstable-nested-components */ import Text from 'src/components/KeeperText'; -import { Box, Input, useColorMode } from 'native-base'; -import { Keyboard, StyleSheet, View } from 'react-native'; +import { Box, Input, useColorMode, Pressable } from 'native-base'; +import { Keyboard, ScrollView, StyleSheet, View } from 'react-native'; import React, { useContext, useEffect, useState } from 'react'; import AppNumPad from 'src/components/AppNumPad'; import Buttons from 'src/components/Buttons'; @@ -23,6 +23,11 @@ import { LocalizationContext } from 'src/context/Localization/LocContext'; import BitcoinInput from 'src/assets/images/btc_input.svg'; import useBalance from 'src/hooks/useBalance'; import ReceiveAddress from './ReceiveAddress'; +import useSigners from 'src/hooks/useSigners'; +import { SignerType } from 'src/services/wallets/enums'; +import { CommonActions, useNavigation } from '@react-navigation/native'; +import ReceiveGreen from 'src/assets/images/receive-green.svg'; +const AddressVerifiableSigners = [SignerType.BITBOX02, SignerType.LEDGER, SignerType.TREZOR]; function ReceiveScreen({ route }: { route }) { const { colorMode } = useColorMode(); @@ -38,6 +43,11 @@ function ReceiveScreen({ route }: { route }) { const { translations } = useContext(LocalizationContext); const { common, home, wallet: walletTranslation } = translations; + const [btmCtrHeight, setBtmCtrHeight] = useState(0); + const navigation = useNavigation(); + const { vaultSigners } = useSigners(wallet.id); + const [addVerifiableSigners, setAddVerifiableSigners] = useState([]); + useEffect(() => { const receivingAddress = WalletOperations.getNextFreeAddress(wallet); setReceivingAddress(receivingAddress); @@ -52,6 +62,13 @@ function ReceiveScreen({ route }: { route }) { } else if (paymentURI) setPaymentURI(null); }, [amount]); + useEffect(() => { + const avSigner = vaultSigners.filter((signer) => + AddressVerifiableSigners.includes(signer?.type) + ); + setAddVerifiableSigners(avSigner); + }, []); + function AddAmountContent() { return ( @@ -102,41 +119,80 @@ function ReceiveScreen({ route }: { route }) { ); } + + const onVerifyAddress = () => { + const signersMFP = addVerifiableSigners.map((signer) => signer.masterFingerprint); + navigation.dispatch( + CommonActions.navigate('VerifyAddressSelectionScreen', { + signersMFP, + vaultId: wallet.id, + }) + ); + }; + + const VerifyAddressBtn = () => { + return ( + + + + Receive + + {'Verify the address'} + + + + ); + }; + return ( + + + + + + + {walletTranslation.receiveAddress} + + + + + + setModalVisible(true)} + icon={} + title={home.AddAmount} + subTitle={walletTranslation.addSpecificInvoiceAmt} + /> + {wallet.entityKind === 'VAULT' && addVerifiableSigners?.length > 0 && ( + + )} + + setBtmCtrHeight(event.nativeEvent.layout.height)} > - - - - {walletTranslation.receiveAddress} - - - - - - setModalVisible(true)} - icon={} - title={home.AddAmount} - subTitle={walletTranslation.addSpecificInvoiceAmt} - /> - - - 600 ? hp(35) : 0, + marginTop: 0, alignItems: 'center', alignSelf: 'center', width: hp(250), @@ -219,6 +276,16 @@ const styles = StyleSheet.create({ addressContainer: { marginHorizontal: wp(20), }, + verifyAddCtr: { + marginTop: hp(15), + width: '100%', + paddingVertical: 16, + paddingHorizontal: 23, + gap: 11, + borderRadius: 10, + flexDirection: 'row', + alignItems: 'center', + }, }); export default ReceiveScreen; diff --git a/src/screens/Recieve/VerifyAddressSelectionScreen.tsx b/src/screens/Recieve/VerifyAddressSelectionScreen.tsx new file mode 100644 index 000000000..c906bc1b3 --- /dev/null +++ b/src/screens/Recieve/VerifyAddressSelectionScreen.tsx @@ -0,0 +1,124 @@ +import { Dimensions, FlatList, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { CommonActions, useNavigation, useRoute } from '@react-navigation/native'; +import React, { useContext, useState } from 'react'; +import { Box, Text, useColorMode } from 'native-base'; +import Next from 'src/assets/images/icon_arrow.svg'; +import KeeperHeader from 'src/components/KeeperHeader'; +import ScreenWrapper from 'src/components/ScreenWrapper'; +import ActivityIndicatorView from 'src/components/AppActivityIndicator/ActivityIndicatorView'; +import * as Sentry from '@sentry/react-native'; +import { errorBourndaryOptions } from 'src/screens/ErrorHandler'; +import { SDIcons } from '../Vault/SigningDeviceIcons'; +import { getSignerNameFromType } from 'src/hardware'; +import moment from 'moment'; +import { InteracationMode } from '../Vault/HardwareModalMap'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import useSigners from 'src/hooks/useSigners'; + +const { width } = Dimensions.get('screen'); + +function VerifyAddressSelectionScreen() { + const { params } = useRoute(); + const { colorMode } = useColorMode(); + const { vaultId, signersMFP } = params as { + vaultId: string; + signersMFP: string[]; + }; + + const { vaultSigners } = useSigners(vaultId); + const [availableSigners] = useState( + vaultSigners.filter((signer) => signersMFP?.includes(signer.masterFingerprint)) + ); + + const navigation = useNavigation(); + const { translations } = useContext(LocalizationContext); + const { vault: vaultText } = translations; + + return ( + + + + item.masterFingerprint} + renderItem={({ item }) => ( + { + navigation.dispatch( + CommonActions.navigate('ConnectChannel', { + signer, + vaultId, + type: signer.type, + mode: InteracationMode.ADDRESS_VERIFICATION, + title: `Setting up ${signerName}`, + subtitle: vaultText.verifyAddDesc, + }) + ); + }} + signer={item} + /> + )} + /> + + ); +} + +export default Sentry.withErrorBoundary(VerifyAddressSelectionScreen, errorBourndaryOptions); + +const styles = StyleSheet.create({ + inheritenceView: { + alignItems: 'center', + justifyContent: 'center', + width: 30, + height: 30, + backgroundColor: '#E3E3E3', + borderRadius: 30, + marginRight: 20, + alignSelf: 'center', + }, +}); + +const SignerCard = ({ onPress, signer }) => { + const { colorMode } = useColorMode(); + const signerName = getSignerNameFromType(signer.type, signer.isMock, false); + return ( + onPress(signer, signerName)}> + + + + + + {SDIcons(signer.type).Icon} + + + + + {`${signerName}`} + + + {`Added on ${moment(signer.addedOn).calendar().toLowerCase()}`} + + + + + + + + + + ); +}; diff --git a/src/screens/Recovery/EnterSeedScreen.tsx b/src/screens/Recovery/EnterSeedScreen.tsx index b0754562f..99fc815d2 100644 --- a/src/screens/Recovery/EnterSeedScreen.tsx +++ b/src/screens/Recovery/EnterSeedScreen.tsx @@ -36,6 +36,7 @@ import Dropdown from 'src/components/Dropdown'; import { SIGNTRANSACTION } from 'src/navigation/contants'; import { hcStatusType } from 'src/models/interfaces/HeathCheckTypes'; import { ConciergeTag, goToConcierge } from 'src/store/sagaActions/concierge'; +import RecoverySuccessModalContent from './RecoverySuccessModalContent'; type seedWordItem = { id: number; @@ -123,7 +124,6 @@ function EnterSeedScreen({ route, navigation }) { if (appId && recoveryLoading) { setRecoveryLoading(false); setRecoverySuccessModal(true); - navigation.replace('App', { screen: 'Home' }); } }, [appId]); @@ -622,14 +622,23 @@ function EnterSeedScreen({ route, navigation }) { /> {}} + title="App Recovered Successfully!" + subTitle="All your wallets and data about your vault has been recovered" + buttonText="Continue" + modalBackground={`${colorMode}.modalWhiteBackground`} + subTitleColor={`${colorMode}.secondaryText`} + textColor={`${colorMode}.primaryText`} + buttonTextColor={`${colorMode}.white`} + buttonBackground={`${colorMode}.greenButtonBackground`} + Content={RecoverySuccessModalContent} + close={() => { + setRecoverySuccessModal(false); + navigation.replace('App', { screen: 'Home' }); + }} showCloseIcon={false} buttonCallback={() => { setRecoverySuccessModal(false); + navigation.replace('App', { screen: 'Home' }); }} /> diff --git a/src/screens/Recovery/RecoverySuccessModalContent.tsx b/src/screens/Recovery/RecoverySuccessModalContent.tsx new file mode 100644 index 000000000..6cce7e8cf --- /dev/null +++ b/src/screens/Recovery/RecoverySuccessModalContent.tsx @@ -0,0 +1,108 @@ +import React, { useContext } from 'react'; +import { Box, HStack } from 'native-base'; +import { useColorMode } from 'native-base'; +import { hp, windowWidth, wp } from '../../constants/responsive'; +import WalletIcon from 'src/assets/images/daily_wallet.svg'; +import AsterisksIcon from 'src/assets/images/asterisks.svg'; +import { StyleSheet } from 'react-native'; +import HexagonIcon from 'src/components/HexagonIcon'; +import Colors from 'src/theme/Colors'; +import Text from 'src/components/KeeperText'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; + +const RecoverySuccessModalContent = () => { + const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { seed } = translations; + + return ( + + + + + + + + + + + + + } + backgroundColor={colorMode == 'dark' ? Colors.ForestGreenDark : Colors.pantoneGreen} + /> + + + + + + + {seed.appRecoveredSuccessfulDesc} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + display: 'flex', + flex: 1, + }, + outerBox: { + alignItems: 'center', + marginBottom: hp(45), + }, + firstLayer: { + position: 'absolute', + top: '16%', + zIndex: 0, + width: '90%', + height: hp(85), + borderRadius: 10, + }, + secondLayer: { + position: 'absolute', + top: '8%', + zIndex: 1, + width: '95%', + height: hp(85), + borderRadius: 10, + }, + thirdLayer: { + position: 'relative', + zIndex: 2, + width: '100%', + height: hp(85), + borderRadius: 10, + padding: 16, + }, + hStack: { + height: '100%', + width: '100%', + alignItems: 'center', + }, + pillsContainer: { + position: 'absolute', + top: 12, + right: 12, + flexDirection: 'row', + alignItems: 'center', + }, + pill: { + width: wp(18), + height: hp(11), + borderRadius: 4, + marginHorizontal: 4, + }, + text: { + width: windowWidth * 0.75, + fontSize: 13, + fontWeight: '400', + marginBottom: hp(20), + }, +}); + +export default RecoverySuccessModalContent; diff --git a/src/screens/Send/SendConfirmation.tsx b/src/screens/Send/SendConfirmation.tsx index 4013e86e7..d575972ce 100644 --- a/src/screens/Send/SendConfirmation.tsx +++ b/src/screens/Send/SendConfirmation.tsx @@ -53,6 +53,7 @@ import * as Sentry from '@sentry/react-native'; import { errorBourndaryOptions } from 'src/screens/ErrorHandler'; import Fonts from 'src/constants/Fonts'; import SendIcon from 'src/assets/images/icon_sent_footer.svg'; +import InvalidUTXO from 'src/assets/images/invalidUTXO.svg'; const customFeeOptionTransfers = [ TransferType.VAULT_TO_ADDRESS, @@ -253,37 +254,33 @@ function SendingCard({ ); } -function TransferCard({ preTitle = '', title, subTitle = '', isVault = false, icon = null }) { +function TransferCard({ + transferFrom = false, + preTitle = '', + title, + subTitle = '', + isVault = false, + currentCurrency, + currencyCode, +}) { + const getCurrencyIcon = () => { + if (currentCurrency === CurrencyKind.BITCOIN) { + return '₿'; + } + return currencyCode; + }; const { colorMode } = useColorMode(); return ( - - - {isVault ? ( - - ) : ( - } - /> - )} - {preTitle} - - - - - {title} - - - {icon} {subTitle} - - + + + {transferFrom ? 'Transfer From' : 'Transfer To'} + + ); } @@ -533,6 +530,9 @@ function TransactionPriorityDetails({ getBalance, getCurrencyIcon, getSatUnit, + isAutoTransfer, + sendMaxFee, + sendMaxFeeEstimatedBlocks, }) { const { colorMode } = useColorMode(); const { translations } = useContext(LocalizationContext); @@ -556,8 +556,10 @@ function TransactionPriorityDetails({ {transactionPriority.toUpperCase()} ~{' '} - {txFeeInfo[transactionPriority?.toLowerCase()]?.estimatedBlocksBeforeConfirmation * - 10}{' '} + {(isAutoTransfer + ? sendMaxFeeEstimatedBlocks + : txFeeInfo[transactionPriority?.toLowerCase()] + ?.estimatedBlocksBeforeConfirmation) * 10}{' '} mins @@ -565,9 +567,11 @@ function TransactionPriorityDetails({ {getCurrencyIcon(BTC, 'dark')}   - {`${getBalance( - txFeeInfo[transactionPriority?.toLowerCase()]?.amount - )} ${getSatUnit()}`} + {isAutoTransfer + ? sendMaxFee + : `${getBalance( + txFeeInfo[transactionPriority?.toLowerCase()]?.amount + )} ${getSatUnit()}`} @@ -788,6 +792,9 @@ function SendConfirmation({ route }) { transferType === TransferType.WALLET_TO_ADDRESS; const txFeeInfo = useAppSelector((state) => state.sendAndReceive.transactionFeeInfo); const sendMaxFee = useAppSelector((state) => state.sendAndReceive.sendMaxFee); + const sendMaxFeeEstimatedBlocks = useAppSelector( + (state) => state.sendAndReceive.setSendMaxFeeEstimatedBlocks + ); const averageTxFees = useAppSelector((state) => state.network.averageTxFees); const { isSuccessful: crossTransferSuccess } = useAppSelector( (state) => state.sendAndReceive.crossTransfer @@ -817,6 +824,7 @@ function SendConfirmation({ route }) { const [highFeeAlertVisible, setHighFeeAlertVisible] = useState(false); const [feeInsightVisible, setFeeInsightVisible] = useState(false); const [visibleCustomPriorityModal, setVisibleCustomPriorityModal] = useState(false); + const [discardUTXOVisible, setDiscardUTXOVisible] = useState(false); const [feePercentage, setFeePercentage] = useState(0); const OneDayHistoricalFee = useOneDayInsight(); const isMoveAllFunds = @@ -1010,25 +1018,7 @@ function SendConfirmation({ route }) { const isValid = validateUTXOsForCachedTxn(); if (!isValid) { // block and show discard alert - Alert.alert( - 'Invalid UTXO set', - 'Please discard this transaction', - [ - { - text: 'Discard', - onPress: discardCachedTransaction, - style: 'destructive', - }, - { - text: 'Cancel', - onPress: () => { - setProgress(false); - }, - style: 'cancel', - }, - ], - { cancelable: true } - ); + setDiscardUTXOVisible(true); return; } } @@ -1146,6 +1136,19 @@ function SendConfirmation({ route }) { setFeeInsightVisible(!feeInsightVisible); } }; + + const discardUTXOModalContent = () => { + return ( + + + + + + {walletTransactions.discardTnxDesc} + + + ); + }; const addNumbers = (str1, str2) => { if (typeof str1 === 'string' && typeof str2 === 'string') { // Convert strings to numbers @@ -1210,51 +1213,44 @@ function SendConfirmation({ route }) { /> ) : ( - - - Transfer From - - - - Transfer To - - - + <> + + + )} {/* Custom priority diabled for auto transfer */} - {!isAutoTransferFlow ? ( - setTransPriorityModalVisible(true)} - > - - - ) : null} + + setTransPriorityModalVisible(true)} + disabled={isAutoTransfer} // disable change priority for AutoTransfers + > + + + {OneDayHistoricalFee.length > 0 && ( Fee stats @@ -1316,7 +1312,13 @@ function SendConfirmation({ route }) { primaryLoading={inProgress} /> ) : ( -
+ navigation.goBack()} + primaryCallback={() => setConfirmPassVisible(true)} + primaryLoading={inProgress} + /> )} } /> + {/* Discard UTXO Modal */} + {}} + dismissible={false} + title={walletTransactions.discardTnxTitle} + subTitle={walletTransactions.discardTnxSubTitle} + subTitleColor={`${colorMode}.secondaryText`} + modalBackground={`${colorMode}.modalWhiteBackground`} + textColor={`${colorMode}.primaryText`} + buttonBackground={`${colorMode}.greenButtonBackground`} + buttonText={'Discard'} + buttonCallback={discardCachedTransaction} + buttonTextColor={`${colorMode}.white`} + showButtons + secondaryButtonText={'Cancel'} + secondaryCallback={() => { + setProgress(false); + setDiscardUTXOVisible(false); + }} + Content={discardUTXOModalContent} + subTitleWidth={wp(280)} + /> {visibleCustomPriorityModal && ( { + const navigateToChannelSigning = (vaultKey: VaultSigner, signerType: string) => { setTrezorModal(false); setBitbox02Modal(false); setLedgerModal(false); @@ -660,6 +660,7 @@ function SignerModals({ signTransaction, vaultKey, vaultId, + signerType, }) ); }; @@ -763,7 +764,7 @@ function SignerModals({ textColor={`${colorMode}.primaryText`} Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToChannelSigning(vaultKey)} + buttonCallback={() => navigateToChannelSigning(vaultKey, signer.type)} /> ); } @@ -958,7 +959,7 @@ function SignerModals({ textColor={`${colorMode}.primaryText`} Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToChannelSigning(vaultKey)} + buttonCallback={() => navigateToChannelSigning(vaultKey, signer.type)} /> ); } @@ -975,7 +976,7 @@ function SignerModals({ textColor={`${colorMode}.primaryText`} Content={() => } buttonText="Proceed" - buttonCallback={() => navigateToChannelSigning(vaultKey)} + buttonCallback={() => navigateToChannelSigning(vaultKey, signer.type)} /> ); } diff --git a/src/screens/SigningDevices/DeleteKeys.tsx b/src/screens/SigningDevices/DeleteKeys.tsx index 65fa75882..a772880e4 100644 --- a/src/screens/SigningDevices/DeleteKeys.tsx +++ b/src/screens/SigningDevices/DeleteKeys.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from 'react'; -import { Box, HStack, VStack, useColorMode } from 'native-base'; +import { Box, useColorMode } from 'native-base'; import { hp, windowWidth, wp } from 'src/constants/responsive'; import SettingsIcon from 'src/assets/images/SignerShow.svg'; import KeeperHeader from 'src/components/KeeperHeader'; @@ -8,16 +8,13 @@ import PasscodeVerifyModal from 'src/components/Modal/PasscodeVerify'; import KeeperModal from 'src/components/KeeperModal'; import CircleIconWrapper from 'src/components/CircleIconWrapper'; import useSigners from 'src/hooks/useSigners'; -import { ActivityIndicator, StyleSheet } from 'react-native'; +import { StyleSheet, ScrollView } from 'react-native'; import { SDIcons } from '../Vault/SigningDeviceIcons'; import Text from 'src/components/KeeperText'; import { getSignerDescription, getSignerNameFromType } from 'src/hardware'; -import ActionChip from 'src/components/ActionChip'; import DeleteIcon from 'src/assets/images/delete_bin.svg'; import EmptyState from 'src/assets/images/empty-state-illustration.svg'; import ShowIcon from 'src/assets/images/show.svg'; -import HexagonIcon from 'src/components/HexagonIcon'; -import Colors from 'src/theme/Colors'; import { useDispatch } from 'react-redux'; import { updateSignerDetails } from 'src/store/sagaActions/wallets'; import { Signer, Vault } from 'src/services/wallets/interfaces/vault'; @@ -29,41 +26,39 @@ import { archiveSigningDevice, deleteSigningDevice } from 'src/store/sagaActions import useArchivedVault from 'src/hooks/useArchivedVaults'; import { LocalizationContext } from 'src/context/Localization/LocContext'; import { SignerType } from 'src/services/wallets/enums'; - -function Content({ colorMode, vaultUsed }: { colorMode: string; vaultUsed: Vault }) { - return ( - - } - callback={() => {}} - /> - - - Please delete the vault to perform this operation. - - - - ); -} +import { RECOVERY_KEY_SIGNER_NAME } from 'src/constants/defaultData'; +import KeyCard from 'src/components/SigningDevices/KeyCard'; +import HexagonIcon from 'src/components/HexagonIcon'; +import Colors from 'src/theme/Colors'; +import { useAppSelector } from 'src/store/hooks'; +import { hideDeletingKeyModal, hideKeyDeletedSuccessModal } from 'src/store/reducers/bhr'; +import BounceLoader from 'src/components/BounceLoader'; +import TorAsset from 'src/components/Loader'; +import moment from 'moment'; function DeleteKeys({ route }) { const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { signer: signerText } = translations; const isUaiFlow: boolean = route.params?.isUaiFlow ?? false; const [confirmPassVisible, setConfirmPassVisible] = useState(isUaiFlow); const { signers } = useSigners(); - const hiddenSigners = signers.filter((signer) => signer.hidden && !signer.archived); + const hiddenSigners = signers.filter( + (signer) => signer.signerName !== RECOVERY_KEY_SIGNER_NAME && signer.hidden && !signer.archived + ); const dispatch = useDispatch(); const navigation = useNavigation(); const [unhidingMfp, setUnhidingMfp] = useState(''); const { allVaults } = useVault({ includeArchived: true }); const { archivedVaults } = useArchivedVault(); - const [warningEnabled, setHideWarning] = React.useState(false); - const [vaultUsed, setVaultUsed] = React.useState(); - const [signerToDelete, setSignerToDelete] = React.useState(); - const { translations } = useContext(LocalizationContext); - const { signer: signerText } = translations; + const [warningEnabled, setHideWarning] = useState(false); + const [vaultUsed, setVaultUsed] = useState(); + const [signerToDelete, setSignerToDelete] = useState(); + const [deletedSigner, setDeletedSigner] = useState(); + const deletingKeyModalVisible = useAppSelector((state) => state.bhr.deletingKeyModalVisible); + const keyDeletedSuccessModalVisible = useAppSelector( + (state) => state.bhr.keyDeletedSuccessModalVisible + ); const onSuccess = () => { if (signerToDelete) { @@ -75,6 +70,7 @@ function DeleteKeys({ route }) { } else { dispatch(deleteSigningDevice([signerToDelete])); } + setDeletedSigner(signerToDelete); setSignerToDelete(null); } }; @@ -84,18 +80,92 @@ function DeleteKeys({ route }) { dispatch(updateSignerDetails(signer, 'hidden', false)); }; + const handleDelete = (signer: Signer) => { + const vaultsInvolved = allVaults.filter( + (vault) => + !vault.archived && + vault.signers.find((s) => s.masterFingerprint === signer.masterFingerprint) + ); + if (vaultsInvolved.length > 0) { + setVaultUsed(vaultsInvolved[0]); + setConfirmPassVisible(false); + return; + } + setConfirmPassVisible(true); + setSignerToDelete(signer); + }; + useEffect(() => { if (unhidingMfp && !hiddenSigners.find((signer) => signer.masterFingerprint === unhidingMfp)) { setUnhidingMfp(''); } }, [hiddenSigners]); + function DeleteLoadingContent() { + return ( + + + + + + {signerText.thisStepTakesTime} + + + + ); + } + + function DeletedSuccessContent() { + return ( + + + + + + {getSignerNameFromType(deletedSigner?.type)} + + + {getSignerDescription( + deletedSigner?.type, + deletedSigner?.extraData?.instanceNumber, + deletedSigner + )} + + + + + ); + } + + function Content({ colorMode, vaultUsed }) { + return ( + + } + callback={() => {}} + /> + + + {signerText.deleteVaultInstruction} + + + + ); + } + return ( } /> - - {hiddenSigners.length === 0 ? ( - - - {signerText.hideSignerTitle} - - {signerText.hideSignerSubtitle} - - - ) : ( - hiddenSigners.map((signer) => { - return ( - - - - - - {getSignerNameFromType(signer.type)} - - {getSignerDescription(signer.type, signer.extraData?.instanceNumber, signer)} - - - - {signer.type !== SignerType.INHERITANCEKEY && - signer.type !== SignerType.POLICY_SERVER && ( - { - // check if signer is a part of any of the vaults - const vaultsInvolved = allVaults.filter( - (vault) => - !vault.archived && - vault.signers.find( - (s) => s.masterFingerprint === signer.masterFingerprint - ) - ); - if (vaultsInvolved.length > 0) { - setVaultUsed(vaultsInvolved[0]); - setHideWarning(true); - return; - } - setConfirmPassVisible(true); - setSignerToDelete(signer); - }} - Icon={} - /> - )} - unhide(signer)} - Icon={ - signer.masterFingerprint === unhidingMfp ? ( - - ) : ( - - ) - } - /> - - setHideWarning(false)} - title="Key is being used for Vault" - subTitle="The Key you are trying to hide is used in one of the visible vaults." - buttonText="View Vault" - secondaryButtonText="Back" - secondaryCallback={() => setHideWarning(false)} - secButtonTextColor={`${colorMode}.greenText`} - modalBackground={`${colorMode}.modalWhiteBackground`} - buttonBackground={`${colorMode}.greenButtonBackground`} - buttonTextColor={`${colorMode}.white`} - DarkCloseIcon={colorMode === 'dark'} - buttonCallback={() => { - setHideWarning(false); - navigation.dispatch( - CommonActions.navigate('VaultDetails', { vaultId: vaultUsed.id }) - ); - }} - textColor={`${colorMode}.primaryText`} - Content={() => } + + {hiddenSigners.length === 0 ? ( + + + {signerText.hideSignerTitle} + + + {signerText.hideSignerSubtitle} + + + + ) : ( + hiddenSigners.map((signer) => { + const showDelete = + signer.type !== SignerType.INHERITANCEKEY && signer.type !== SignerType.POLICY_SERVER; + + return ( + handleDelete(signer) : null} + secondaryAction={() => unhide(signer)} + primaryText={showDelete ? signerText.delete : null} + secondaryText={signerText.unhide} + primaryIcon={showDelete ? : null} + secondaryIcon={} + icon={{ element: SDIcons(signer.type, true).Icon, backgroundColor: 'pantoneGreen' }} + name={getSignerNameFromType(signer.type)} + description={getSignerDescription( + signer.type, + signer.extraData?.instanceNumber, + signer + )} + descriptionTitle={'Description'} + dateAdded={`Added ${moment(signer?.addedOn).calendar()}`} /> - - ); - }) - )} + ); + }) + )} +
+ setHideWarning(false)} + title={signerText.deleteVaultWarning} + subTitle={signerText.vaultWarningSubtitle} + buttonText={signerText.viewVault} + secondaryButtonText={signerText.back} + secondaryCallback={() => setHideWarning(false)} + secButtonTextColor={`${colorMode}.greenText`} + modalBackground={`${colorMode}.modalWhiteBackground`} + buttonBackground={`${colorMode}.greenButtonBackground`} + buttonTextColor={`${colorMode}.white`} + DarkCloseIcon={colorMode === 'dark'} + buttonCallback={() => { + setHideWarning(false); + navigation.dispatch(CommonActions.navigate('VaultDetails', { vaultId: vaultUsed.id })); + }} + textColor={`${colorMode}.primaryText`} + Content={() => } + /> + dispatch(hideDeletingKeyModal())} + showCloseIcon={false} + title={signerText.deletingKey} + subTitle={signerText.keyWillBeDeleted} + subTitleColor={`${colorMode}.secondaryText`} + modalBackground={`${colorMode}.modalWhiteBackground`} + textColor={`${colorMode}.primaryText`} + Content={DeleteLoadingContent} + /> + dispatch(hideKeyDeletedSuccessModal())} + closeOnOverlayClick + showCloseIcon={false} + title={signerText.keyDeletedSuccessfully} + subTitle={signerText.keyDeletedSuccessMessage} + subTitleColor={`${colorMode}.secondaryText`} + modalBackground={`${colorMode}.modalWhiteBackground`} + textColor={`${colorMode}.primaryText`} + buttonBackground={`${colorMode}.greenButtonBackground`} + buttonTextColor={`${colorMode}.white`} + buttonText={signerText.manageKeys} + buttonCallback={() => { + dispatch(hideKeyDeletedSuccessModal()); + navigation.dispatch(CommonActions.navigate('ManageSigners')); + }} + Content={DeletedSuccessContent} + /> setConfirmPassVisible(false)} - title="Enter Passcode" + title={signerText.enterPasscode} subTitleWidth={wp(240)} - subTitle={'Confirm passcode to delete key'} + subTitle={signerText.confirmPasscodeToDeleteKey} modalBackground={`${colorMode}.modalWhiteBackground`} subTitleColor={`${colorMode}.secondaryText`} textColor={`${colorMode}.primaryText`} Content={() => ( - { - setConfirmPassVisible(false); - }} - onSuccess={onSuccess} - /> + + {signerToDelete && ( + + + + + {getSignerNameFromType(signerToDelete.type)} + + + {getSignerDescription( + signerToDelete?.type, + signerToDelete?.extraData?.instanceNumber, + signerToDelete + )} + + + + )} + setConfirmPassVisible(false)} + onSuccess={onSuccess} + /> + )} /> @@ -239,18 +327,48 @@ const styles = StyleSheet.create({ letterSpacing: 0.65, }, emptyWrapper: { + height: '100%', alignItems: 'center', justifyContent: 'center', - flex: 0.8, }, emptyText: { + fontSize: 15, + lineHeight: 20, marginBottom: hp(3), }, emptySubText: { + fontSize: 14, + lineHeight: 20, width: wp(250), textAlign: 'center', marginBottom: hp(30), }, + signerContentContainer: { + flexDirection: 'row', + alignItems: 'flex-start', + gap: wp(12), + }, + loadingModalContainer: { + marginBottom: hp(20), + }, + loadingIcon: { + width: windowWidth * 0.85, + marginTop: hp(30), + marginBottom: hp(50), + }, + loadingText: { + width: windowWidth * 0.8, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + successModalContainer: { + paddingHorizontal: wp(25), + justifyContent: 'center', + height: hp(108), + marginBottom: hp(20), + borderRadius: 10, + }, }); export default DeleteKeys; diff --git a/src/screens/SigningDevices/SetupCollaborativeWallet.tsx b/src/screens/SigningDevices/SetupCollaborativeWallet.tsx index ce900df3b..ce324ea18 100644 --- a/src/screens/SigningDevices/SetupCollaborativeWallet.tsx +++ b/src/screens/SigningDevices/SetupCollaborativeWallet.tsx @@ -337,12 +337,12 @@ function SetupCollaborativeWallet() { dispatch(setCosginerModal(false)); dispatch(goToConcierge([ConciergeTag.COLLABORATIVE_Wallet], 'add-signers')); }} - learnMoreTitle={common.needMoreHelp} + learnMoreTitle={common.needHelp} buttonCallback={() => { dispatch(setCosginerModal(false)); }} buttonBackground={`${colorMode}.modalWhiteButton`} - buttonText="Continue" + buttonText={common.ok} buttonTextColor={`${colorMode}.modalWhiteButtonText`} /> diff --git a/src/screens/Vault/AddSigningDevice.tsx b/src/screens/Vault/AddSigningDevice.tsx index fb3a697ce..2f62e5e3a 100644 --- a/src/screens/Vault/AddSigningDevice.tsx +++ b/src/screens/Vault/AddSigningDevice.tsx @@ -699,8 +699,8 @@ function AddSigningDevice() { const [keyAddedModalVisible, setKeyAddedModalVisible] = useState(false); const { signers } = useSigners(); - // filter out archived signers - const activeSigners = signers.filter((signer) => !signer.archived); + // filter out archived & hidden signers + const activeSigners = signers.filter((signer) => !signer.archived && !signer.hidden); const { signerMap } = useSignerMap(); const [selectedSigners, setSelectedSigners] = useState(new Map()); const [vaultKeys, setVaultKeys] = useState([]); diff --git a/src/screens/Vault/ArchivedVault.tsx b/src/screens/Vault/ArchivedVault.tsx index 76b7d60b2..d0f92515b 100644 --- a/src/screens/Vault/ArchivedVault.tsx +++ b/src/screens/Vault/ArchivedVault.tsx @@ -72,10 +72,12 @@ function ArchivedVault({ navigation, route }) { /> {vaults.length === 0 ? ( - + {vaultText.archivedVaultEmptyTitle} - {vaultText.archivedVaultEmptySubtitle} + + {vaultText.archivedVaultEmptySubtitle} + ) : ( @@ -118,11 +120,15 @@ const styles = StyleSheet.create({ flex: 1, }, emptyText: { + fontSize: 15, + lineHeight: 20, marginBottom: hp(3), }, emptySubText: { - textAlign: 'center', + fontSize: 14, + lineHeight: 20, width: wp(250), + textAlign: 'center', marginBottom: hp(30), }, cardContainer: { diff --git a/src/screens/Vault/HardwareModalMap.tsx b/src/screens/Vault/HardwareModalMap.tsx index 4774c8f7c..3b2b409f8 100644 --- a/src/screens/Vault/HardwareModalMap.tsx +++ b/src/screens/Vault/HardwareModalMap.tsx @@ -4,7 +4,14 @@ 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 { + ActivityIndicator, + Alert, + Clipboard, + Linking, + StyleSheet, + TouchableOpacity, +} from 'react-native'; import { Box, useColorMode, View } from 'native-base'; import { CommonActions, useNavigation } from '@react-navigation/native'; import { @@ -32,7 +39,7 @@ import LedgerImage from 'src/assets/images/ledger_image.svg'; 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 SeedSignerSetupImage from 'src/assets/images/seedsigner-setup-horizontal.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'; @@ -119,6 +126,7 @@ export const enum InteracationMode { IDENTIFICATION = 'IDENTIFICATION', APP_ADDITION = 'APP_ADDITION', CANARY_ADDITION = 'CANARY_ADDITION', + ADDRESS_VERIFICATION = 'ADDRESS_VERIFICATION', } const getSignerContent = ( @@ -370,6 +378,22 @@ const getSignerContent = ( const seedSignerInstructions = `Make sure the seed is loaded and export the xPub by going to Seeds > Select your master fingerprint > Export Xpub > ${ isMultisig ? 'Multisig' : 'Singlesig' } > Native Segwit > Keeper.\n`; + + const setupGuideLink = ( + + Linking.openURL( + 'https://bitcoinmagazine.com/guides/how-to-use-seedsigner-for-secure-bitcoin' + ) + } + > + Setting Up a SeedSigner - + https://bitcoinmagazine.com/guides/how-to-use-seedsigner-for-secure-bitcoin + + ); + return { type: SignerType.SEEDSIGNER, Illustration: , @@ -377,8 +401,9 @@ const getSignerContent = ( ? [ 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.', + setupGuideLink, ] - : [seedSignerInstructions], + : [seedSignerInstructions, setupGuideLink], title: isHealthcheck ? 'Verify SeedSigner' : isCanaryAddition @@ -1517,7 +1542,7 @@ function HardwareModalMap({ function SigningServerOTPModal() { const { translations } = useContext(LocalizationContext); - const { vault: vaultTranslation, common } = translations; + const { vault: vaultTranslation, common, signer: signerTranslation } = translations; const onPressNumber = (text) => { let tmpPasscode = otp; @@ -1556,8 +1581,8 @@ function HardwareModalMap({
- { + { if (mode === InteracationMode.HEALTH_CHECK) { checkSigningServerHealth(); setSigningServerHealthCheckOTPModal(false); @@ -1575,7 +1600,14 @@ function HardwareModalMap({ } } }} - value={common.confirm} + primaryText={common.confirm} + secondaryText={ + mode === InteracationMode.HEALTH_CHECK && signerTranslation.forgot2FA + } + secondaryCallback={() => { + setSigningServerHealthCheckOTPModal(false); + showToast(signerTranslation.forgot2FANote, null, IToastCategory.DEFAULT, 5000); + }} /> @@ -2092,5 +2124,12 @@ const styles = StyleSheet.create({ width: '100%', marginTop: 2, }, + infoText: { + letterSpacing: 0.65, + padding: 3, + fontSize: 13, + fontWeight: '400', + width: wp(285), + }, }); export default HardwareModalMap; diff --git a/src/screens/Vault/SignerAdvanceSettings.tsx b/src/screens/Vault/SignerAdvanceSettings.tsx index 9ea38a998..45c0e1cd2 100644 --- a/src/screens/Vault/SignerAdvanceSettings.tsx +++ b/src/screens/Vault/SignerAdvanceSettings.tsx @@ -330,7 +330,13 @@ function SignerAdvanceSettings({ route }: any) { return; case SignerType.LEDGER: case SignerType.BITBOX02: - navigation.dispatch(CommonActions.navigate('RegisterWithChannel', { vaultKey, vaultId })); + navigation.dispatch( + CommonActions.navigate('RegisterWithChannel', { + vaultKey, + vaultId, + signerType: signer.type, + }) + ); break; case SignerType.KEYSTONE: case SignerType.JADE: @@ -837,20 +843,6 @@ function SignerAdvanceSettings({ route }: any) { callback={navigateToPolicyChange} /> )} - {isPolicyServer && vaultId && ( - { - showToast( - 'If you have lost your 2FA app, it is recommended that you remove SS and add a different key or SS again', - null, - IToastCategory.DEFAULT, - 7000 - ); - }} - /> - )} {showOneTimeBackup && ( backupModalContent({ title: signer.signerName, - subTitle: `${common.added} ${moment(signer.addedOn).calendar()}`, + subTitle: `${common.added} ${moment(signer.addedOn).calendar().toLowerCase()}`, icon: signer.type === SignerType.INHERITANCEKEY ? ( diff --git a/src/screens/Vault/SigningDeviceChecklist.tsx b/src/screens/Vault/SigningDeviceChecklist.tsx index 2883f74c3..08dc0367b 100644 --- a/src/screens/Vault/SigningDeviceChecklist.tsx +++ b/src/screens/Vault/SigningDeviceChecklist.tsx @@ -22,6 +22,8 @@ const getHealthCheckStatusText = (status) => { return 'Key used for signing'; case hcStatusType.HEALTH_CHECK_REGISTRATION: return 'Signer used for vault registration'; + case hcStatusType.HEALTH_CHECK_VERIFICATION: + return 'Signer used for vault address verification'; } }; diff --git a/src/screens/Vault/SigningDeviceDetails.tsx b/src/screens/Vault/SigningDeviceDetails.tsx index b1dbb4d42..be436e482 100644 --- a/src/screens/Vault/SigningDeviceDetails.tsx +++ b/src/screens/Vault/SigningDeviceDetails.tsx @@ -10,7 +10,7 @@ import { hp, wp } from 'src/constants/responsive'; import KeeperHeader from 'src/components/KeeperHeader'; import useToastMessage from 'src/hooks/useToastMessage'; import KeeperModal from 'src/components/KeeperModal'; -import SeedSigner from 'src/assets/images/seedsigner_setup.svg'; +import SeedSigner from 'src/assets/images/seedsigner-setup-horizontal.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'; @@ -29,7 +29,8 @@ 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 SpecterSetupImage from 'src/assets/images/illustration_spectre.svg'; -import InhertanceKeyIcon from 'src/assets/images/illustration_inheritanceKey.svg'; +import InhertanceKeyIcon from 'src/assets/images/illustration-inheritance-key.svg'; +import EmptyState from 'src/assets/images/key-empty-state-illustration.svg'; import { SignerType } from 'src/services/wallets/enums'; import { healthCheckStatusUpdate } from 'src/store/sagaActions/bhr'; import useVault from 'src/hooks/useVault'; @@ -63,6 +64,7 @@ import PasscodeVerifyModal from 'src/components/Modal/PasscodeVerify'; import BackupModalContent from 'src/screens/AppSettings/BackupModal'; import DotView from 'src/components/DotView'; import Note from 'src/components/Note/Note'; +import ActivityIndicatorView from 'src/components/AppActivityIndicator/ActivityIndicatorView'; const getSignerContent = (type: SignerType) => { switch (type) { @@ -73,17 +75,17 @@ 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 signer (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.', FAQ: 'https://coldcard.com/docs/faq', }; case SignerType.TAPSIGNER: return { title: 'TAPSIGNER', subTitle: - 'TAPSIGNER is a Bitcoin private key on a card! You can sign mobile wallet transaction by tapping the phone', + 'TAPSIGNER is a Bitcoin private key on a card! You can sign mobile wallet transaction by tapping the phone.', assert: , description: - '\u2022 TAPSIGNER’s lower cost makes hardware wallet features and security available to a wider market around the world.\n\u2022 An NFC card provides fast and easy user experiences.\n\u2022 TAPSIGNER is a great way to keep your keys separate from your wallet(s) \n\u2022 The card form factor makes it easy to carry and easy to conceal', + '\u2022 TAPSIGNER’s lower cost makes hardware wallet features and security available to a wider market around the world.\n\u2022 An NFC card provides fast and easy user experiences.\n\u2022 TAPSIGNER is a great way to keep your keys separate from your wallet(s). \n\u2022 The card form factor makes it easy to carry and easy to conceal.', FAQ: 'https://tapsigner.com/faq', }; case SignerType.LEDGER: @@ -122,7 +124,7 @@ const getSignerContent = (type: SignerType) => { 'Foundation products empower individuals to reclaim their digital sovereignty by taking control of your money and data. Foundation offers best-in-class security and privacy via openness. No walled gardens; no closed source engineering', assert: , description: - '\u2022Foundation products are beautiful, and intuitive, and remove the steep learning curve typically associated with Bitcoin and decentralized tech.\n\u2022 Foundation reflects our optimism about the future. Our products feel positive, aspirational, and a bit sci-fi.', + '\u2022 Passport has no direct connection with the outside world – meaning your keys are never directly exposed online. It uses a camera and QR codes for communication. This provides hardcore, air-gapped security while offering a seamless user experience.\n\u2022 Passport’s software and hardware are both fully open source. No walled gardens, no closed source engineering. Connect Passport to their Envoy mobile app for a seamless experience.', FAQ: 'https://docs.foundationdevices.com', }; case SignerType.MOBILE_KEY: @@ -140,7 +142,7 @@ const getSignerContent = (type: SignerType) => { subTitle: 'You could use a newly generated seed (12 words) as one of the signing keys', assert: , description: - '\u2022Keep these safe by writing them down on a piece of paper or on a metal plate.\n\u2022 When you use them to sign a transaction, you will have to provide these in the same order.\n\u2022 These keys are considered warm because you may have to get them online when signing a transaction.', + '\u2022 Keep these safe by writing them down on a piece of paper or on a metal plate.\n\u2022 When you use them to sign a transaction, you will have to provide these in the same order.\n\u2022 These keys are considered warm because you may have to get them online when signing a transaction.', FAQ: '', }; case SignerType.MY_KEEPER: @@ -150,7 +152,7 @@ const getSignerContent = (type: SignerType) => { subTitle: 'You can use a specific BIP-85 wallet on Keeper 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', + '\u2022 Make 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.', FAQ: KEEPER_KNOWLEDGEBASE, }; case SignerType.POLICY_SERVER: @@ -160,7 +162,7 @@ const getSignerContent = (type: SignerType) => { '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 signers, you will need to provide this code. \n\u2022 Considered a hot key as it is on a connected online server', + '\u2022 An 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: @@ -169,7 +171,7 @@ const getSignerContent = (type: SignerType) => { subTitle: 'Easy backup and restore with a microSD card', assert: , description: - 'Minimalist and discreet design. The BitBox02 features a dual-chip design with a secure chip Limited firmware that only supports Bitcoin', + '\u2022 BitBox02 is known for its ease of use, open-source firmware, and security features like backup recovery via microSD card, USB-C connectivity, and integration with the BitBoxApp.\n\u2022 The wallet prioritizes privacy and security with advanced encryption and verification protocols, making it ideal for users who value high security in managing their bitcoin.', FAQ: 'https://shiftcrypto.ch/support/', }; case SignerType.TREZOR: @@ -195,10 +197,11 @@ const getSignerContent = (type: SignerType) => { case SignerType.INHERITANCEKEY: return { title: 'Inheritance Key', - subTitle: 'Secure your legacy with the Inheritance Key feature in Keeper.', + subTitle: + 'An additional key setup with special conditions to help transfer bitcoin to the beneficiary.', assert: , description: - '\u2022Prepare for the future by using a 3-of-6 multisig setup with one key being an Inheritance Key.\n\u2022 Ensure a seamless transfer of assets while maintaining control over your financial legacy.', + '\u2022 Prepare for the future by using a 3-of-6 multisig setup with one key being an Inheritance Key.\n\u2022 Ensure a seamless transfer of assets while maintaining control over your financial legacy.', FAQ: `${KEEPER_KNOWLEDGEBASE}sections/17238611956253-Inheritance`, }; case SignerType.SPECTER: @@ -248,6 +251,7 @@ function SigningDeviceDetails({ route }) { const [showMobileKeyModal, setShowMobileKeyModal] = useState(false); const [confirmPassVisible, setConfirmPassVisible] = useState(false); const [backupModalVisible, setBackupModalVisible] = useState(false); + const [showLoader, setShowLoader] = useState(true); const data = useQuery(RealmSchema.BackupHistory); const history = useMemo(() => data.sorted('date', true), [data]); @@ -269,6 +273,7 @@ function SigningDeviceDetails({ route }) { useEffect(() => { if (signer) { setHealthCheckArray(signer.healthCheckDetails); + setShowLoader(false); } }, [signer.healthCheckDetails.length]); @@ -414,42 +419,58 @@ function SigningDeviceDetails({ route }) {
- {healthCheckArray.map((item, index) => { - return currentSigner.type !== SignerType.MY_KEEPER ? ( - - ) : ( - - ( - + {showLoader ? ( + + ) : healthCheckArray.length === 0 ? ( + + + {'Key History'} + + + {'The history of your key health checks would be visible here.'} + + + + + + ) : ( + healthCheckArray.map((item, index) => { + return currentSigner.type !== SignerType.MY_KEEPER ? ( + + ) : ( + + ( - + + + + + {strings[item?.title]} + + + {moment.unix(item.date).format('DD MMM YYYY, HH:mmA')} + - - {strings[item?.title]} - - - {moment.unix(item.date).format('DD MMM YYYY, HH:mmA')} - - - )} - /> - - ); - })} + )} + /> + + ); + }) + )} {currentSigner.type === SignerType.MY_KEEPER && ( @@ -483,36 +504,37 @@ function SigningDeviceDetails({ route }) { close={() => 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" + buttonText="Confirm Access" + secondaryButtonText="Confirm Later" buttonTextColor={`${colorMode}.white`} buttonCallback={() => { dispatch( healthCheckStatusUpdate([ { signerId: signer.masterFingerprint, - status: hcStatusType.HEALTH_CHECK_SKIPPED, + status: hcStatusType.HEALTH_CHECK_MANAUAL, }, ]) ); + showToast('Device verified manually!'); setSkipHealthCheckModalVisible(false); - showToast('Device healhcheck skipped!'); }} secondaryCallback={() => { dispatch( healthCheckStatusUpdate([ { signerId: signer.masterFingerprint, - status: hcStatusType.HEALTH_CHECK_MANAUAL, + status: hcStatusType.HEALTH_CHECK_SKIPPED, }, ]) ); - showToast('Device verified manually!'); + showToast('Device health check skipped!'); setSkipHealthCheckModalVisible(false); }} textColor={`${colorMode}.primaryText`} Content={HealthCheckSkipContent} /> + setDetailModal(false)} @@ -526,12 +548,13 @@ function SigningDeviceDetails({ route }) { }} Content={SignerContent} subTitleWidth={wp(280)} - buttonText={common.proceed} + buttonText={common.ok} buttonTextColor={`${colorMode}.modalWhiteButtonText`} buttonBackground={`${colorMode}.modalWhiteButton`} buttonCallback={() => setDetailModal(false)} DarkCloseIcon learnMore + learnMoreTitle={common.needHelp} /> + ); } @@ -718,6 +742,26 @@ const styles = StyleSheet.create({ flexGrow: 1, paddingBottom: hp(220), }, + emptyWrapper: { + alignItems: 'center', + justifyContent: 'center', + height: '90%', + }, + emptyStateContainer: { + marginLeft: wp(20), + }, + emptyText: { + fontSize: 15, + lineHeight: 20, + marginBottom: hp(3), + }, + emptySubText: { + fontSize: 14, + lineHeight: 20, + width: wp(250), + textAlign: 'center', + marginBottom: hp(30), + }, }); export default SigningDeviceDetails; diff --git a/src/screens/Vault/SigningDeviceList.tsx b/src/screens/Vault/SigningDeviceList.tsx index 0fc36a98c..4b8c39347 100644 --- a/src/screens/Vault/SigningDeviceList.tsx +++ b/src/screens/Vault/SigningDeviceList.tsx @@ -71,7 +71,7 @@ function SigningDeviceList() { const [isNfcSupported, setNfcSupport] = useState(true); const [signersLoaded, setSignersLoaded] = useState(false); - const { vault } = translations; + const { vault, common } = translations; const getNfcSupport = async () => { const isSupported = await NFC.isNFCSupported(); @@ -264,7 +264,7 @@ function SigningDeviceList() { modalBackground={`${colorMode}.modalGreenBackground`} buttonTextColor={`${colorMode}.modalWhiteButtonText`} buttonBackground={`${colorMode}.modalWhiteButton`} - buttonText="Add Now" + buttonText={common.ok} buttonCallback={() => { dispatch(setSdIntroModal(false)); }} @@ -272,6 +272,7 @@ function SigningDeviceList() { Content={VaultSetupContent} DarkCloseIcon learnMore + learnMoreTitle={common.needHelp} learnMoreCallback={() => { dispatch(setSdIntroModal(false)); reduxDispatch(goToConcierge([ConciergeTag.KEYS], 'signing-device-list')); diff --git a/src/screens/Vault/VaultConfigurationRecreation.tsx b/src/screens/Vault/VaultConfigurationRecreation.tsx index 7582e9cc3..7d43aa893 100644 --- a/src/screens/Vault/VaultConfigurationRecreation.tsx +++ b/src/screens/Vault/VaultConfigurationRecreation.tsx @@ -26,6 +26,7 @@ import { useDispatch } from 'react-redux'; import { goToConcierge } from 'src/store/sagaActions/concierge'; import { ConciergeTag } from 'src/models/enums/ConciergeTag'; import { useFocusEffect } from '@react-navigation/native'; +import CameraUnauthorized from 'src/components/CameraUnauthorized'; function WrappedImportIcon() { return ( @@ -187,6 +188,7 @@ function VaultConfigurationCreation() { captureAudio={false} onBarCodeRead={onBarCodeRead} useNativeZoom + notAuthorizedView={} /> )}
@@ -244,7 +246,8 @@ function VaultConfigurationCreation() { Content={ImportVaultContent} DarkCloseIcon learnMore - buttonText="Continue" + learnMoreTitle={common.needHelp} + buttonText={common.ok} buttonTextColor={`${colorMode}.modalWhiteButtonText`} buttonBackground={`${colorMode}.modalWhiteButton`} buttonCallback={() => setShowModal(false)} @@ -295,8 +298,6 @@ const styles = StyleSheet.create({ overflow: 'hidden', marginVertical: 15, alignItems: 'center', - height: hp(285), - width: wp(330), alignSelf: 'center', }, cameraView: { @@ -327,11 +328,11 @@ const styles = StyleSheet.create({ }, qrStatus: { position: 'absolute', - top: hp(255), - left: wp(90), zIndex: 999, justifyContent: 'center', alignItems: 'center', + alignSelf: 'center', + transform: [{ translateY: hp(235) }], }, scrollViewWrapper: { flex: 1, diff --git a/src/screens/Vault/VaultDetails.tsx b/src/screens/Vault/VaultDetails.tsx index cbc647bf2..487c01de0 100644 --- a/src/screens/Vault/VaultDetails.tsx +++ b/src/screens/Vault/VaultDetails.tsx @@ -441,12 +441,13 @@ function VaultDetails({ navigation, route }: ScreenProps) { Content={VaultContent} buttonTextColor={`${colorMode}.modalWhiteButtonText`} buttonBackground={`${colorMode}.modalWhiteButton`} - buttonText={common.continue} + buttonText={common.ok} buttonCallback={() => { dispatch(setIntroModal(false)); }} DarkCloseIcon learnMore + learnMoreTitle={common.needHelp} learnMoreCallback={ isCollaborativeWallet ? () => { diff --git a/src/screens/WalletDetails/components/LearnMoreModal.tsx b/src/screens/WalletDetails/components/LearnMoreModal.tsx index 8f91297cc..f2c193f59 100644 --- a/src/screens/WalletDetails/components/LearnMoreModal.tsx +++ b/src/screens/WalletDetails/components/LearnMoreModal.tsx @@ -1,5 +1,5 @@ import { StyleSheet, View } from 'react-native'; -import React from 'react'; +import React, { useContext } from 'react'; import KeeperModal from 'src/components/KeeperModal'; import { useDispatch } from 'react-redux'; import { Box, useColorMode } from 'native-base'; @@ -8,6 +8,7 @@ import { hp } from 'src/constants/responsive'; import Text from 'src/components/KeeperText'; import { goToConcierge } from 'src/store/sagaActions/concierge'; import { ConciergeTag } from 'src/models/enums/ConciergeTag'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; function LinkedWalletContent() { return ( @@ -26,6 +27,8 @@ function LinkedWalletContent() { function LearnMoreModal({ introModal, setIntroModal }) { const dispatch = useDispatch(); const { colorMode } = useColorMode(); + const { translations } = useContext(LocalizationContext); + const { common } = translations; return ( { dispatch(setIntroModal(false)); dispatch(goToConcierge([ConciergeTag.WALLET], 'wallet-details')); }} - buttonText="Back to Wallet" + buttonText={common.ok} buttonTextColor={`${colorMode}.modalWhiteButtonText`} buttonBackground={`${colorMode}.modalWhiteButton`} buttonCallback={() => dispatch(setIntroModal(false))} diff --git a/src/services/channel/constants.ts b/src/services/channel/constants.ts index f95dd09fc..39c5106d2 100644 --- a/src/services/channel/constants.ts +++ b/src/services/channel/constants.ts @@ -15,6 +15,16 @@ export const TREZOR_HEALTHCHECK = 'TREZOR_HEALTHCHECK'; export const SIGNED_TX = 'SIGNED_TX'; export const REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS'; export const SET_NETWORK = 'SET_NETWORK'; +export const CHANNEL_MESSAGE = 'CHANNEL_MESSAGE'; + +// EMIT MODES +export const EMIT_MODES = { + ADD_DEVICE: 'ADD_DEVICE', + REGISTER_MULTISIG: 'REGISTER_MULTISIG', + SIGN_TX: 'SIGN_TX', + HEALTH_CHECK: 'HEALTH_CHECK', + VERIFY_ADDRESS: 'VERIFY_ADDRESS', +}; // whirlpool events export const WHIRLPOOL_LISTEN = 'WHIRLPOOL_LISTEN'; diff --git a/src/services/wallets/operations/index.ts b/src/services/wallets/operations/index.ts index 9bd64b85a..28338354e 100644 --- a/src/services/wallets/operations/index.ts +++ b/src/services/wallets/operations/index.ts @@ -17,6 +17,7 @@ import ElectrumClient from 'src/services/electrum/client'; import { isSignerAMF } from 'src/hardware'; import idx from 'idx'; import RestClient, { TorStatus } from 'src/services/rest/RestClient'; +import { hash256 } from 'src/utils/service-utilities/encryption'; import ecc from './taproot-utils/noble_ecc'; import { AverageTxFees, @@ -43,10 +44,10 @@ import { import { Signer, Vault, VaultSigner, VaultSpecs } from '../interfaces/vault'; import { AddressCache, AddressPubs, Wallet, WalletSpecs } from '../interfaces/wallet'; import WalletUtilities from './utils'; -import { hash256 } from 'src/utils/service-utilities/encryption'; bitcoinJS.initEccLib(ecc); const ECPair = ECPairFactory(ecc); +const TESTNET_FEE_CUTOFF = 10; const validator = (pubkey: Buffer, msghash: Buffer, signature: Buffer): boolean => ECPair.fromPublicKey(pubkey).verify(msghash, signature); @@ -435,21 +436,21 @@ export default class WalletOperations { // high fee: 10 minutes const highFeeBlockEstimate = 1; const high = { - feePerByte: 50, + feePerByte: 5, estimatedBlocks: highFeeBlockEstimate, }; // medium fee: 30 mins const mediumFeeBlockEstimate = 3; const medium = { - feePerByte: 25, + feePerByte: 3, estimatedBlocks: mediumFeeBlockEstimate, }; // low fee: 60 mins const lowFeeBlockEstimate = 6; const low = { - feePerByte: 12, + feePerByte: 1, estimatedBlocks: lowFeeBlockEstimate, }; const feeRatesByPriority = { high, medium, low }; @@ -479,6 +480,11 @@ export default class WalletOperations { estimatedBlocks: lowFeeBlockEstimate, }; + if (config.NETWORK_TYPE === NetworkType.TESTNET && low.feePerByte > TESTNET_FEE_CUTOFF) { + // working around testnet fee spikes + return WalletOperations.mockFeeRates(); + } + const feeRatesByPriority = { high, medium, low }; return feeRatesByPriority; } catch (err) { @@ -530,6 +536,11 @@ export default class WalletOperations { estimatedBlocks: lowFeeBlockEstimate, }; + if (config.NETWORK_TYPE === NetworkType.TESTNET && low.feePerByte > TESTNET_FEE_CUTOFF) { + // working around testnet fee spikes + return WalletOperations.mockFeeRates(); + } + const feeRatesByPriority = { high, medium, low }; return feeRatesByPriority; } catch (err) { diff --git a/src/store/reducers/bhr.ts b/src/store/reducers/bhr.ts index dea9c2e32..b48248cbf 100644 --- a/src/store/reducers/bhr.ts +++ b/src/store/reducers/bhr.ts @@ -41,6 +41,9 @@ const initialState: { isCloudBsmsBackupRequired: boolean; lastBsmsBackup?: number; encPassword?: string; + + deletingKeyModalVisible: boolean; + keyDeletedSuccessModalVisible: boolean; } = { backupMethod: null, isBackupError: false, @@ -76,6 +79,8 @@ const initialState: { isCloudBsmsBackupRequired: false, lastBsmsBackup: null, encPassword: '', + deletingKeyModalVisible: false, + keyDeletedSuccessModalVisible: false, }; const bhrSlice = createSlice({ @@ -208,6 +213,18 @@ const bhrSlice = createSlice({ setEncPassword: (state, action: PayloadAction) => { state.encPassword = action.payload; }, + showDeletingKeyModal: (state) => { + state.deletingKeyModalVisible = true; + }, + hideDeletingKeyModal: (state) => { + state.deletingKeyModalVisible = false; + }, + showKeyDeletedSuccessModal: (state) => { + state.keyDeletedSuccessModalVisible = true; + }, + hideKeyDeletedSuccessModal: (state) => { + state.keyDeletedSuccessModalVisible = false; + }, }, }); @@ -249,6 +266,11 @@ export const { setIsCloudBsmsBackupRequired, setLastBsmsBackup, setEncPassword, + + showDeletingKeyModal, + hideDeletingKeyModal, + showKeyDeletedSuccessModal, + hideKeyDeletedSuccessModal, } = bhrSlice.actions; const bhrPersistConfig = { diff --git a/src/store/reducers/send_and_receive.ts b/src/store/reducers/send_and_receive.ts index 3c87df115..8e7cd2ca8 100644 --- a/src/store/reducers/send_and_receive.ts +++ b/src/store/reducers/send_and_receive.ts @@ -103,6 +103,7 @@ export interface SendAndReceiveState { isSuccessful: boolean; }; sendMaxFee: number; + setSendMaxFeeEstimatedBlocks: number; feeIntelMissing: boolean; transactionFeeInfo: TransactionFeeInfo; inheritanceSigningRequestId: string; @@ -145,6 +146,7 @@ const initialState: SendAndReceiveState = { isSuccessful: false, }, sendMaxFee: 0, + setSendMaxFeeEstimatedBlocks: 0, feeIntelMissing: false, transactionFeeInfo: { [TxPriority.LOW]: { @@ -174,6 +176,9 @@ const sendAndReceiveSlice = createSlice({ setSendMaxFee: (state, action: PayloadAction) => { state.sendMaxFee = action.payload; }, + setSendMaxFeeEstimatedBlocks: (state, action: PayloadAction) => { + state.setSendMaxFeeEstimatedBlocks = action.payload; + }, sendPhaseOneExecuted: (state, action: PayloadAction) => { const { transactionFeeInfo } = state; @@ -333,6 +338,7 @@ const sendAndReceiveSlice = createSlice({ export const { setSendMaxFee, + setSendMaxFeeEstimatedBlocks, sendPhaseOneExecuted, customFeeCalculated, sendPhaseTwoExecuted, diff --git a/src/store/reducers/settings.ts b/src/store/reducers/settings.ts index 213d7e9ae..ff17f9934 100644 --- a/src/store/reducers/settings.ts +++ b/src/store/reducers/settings.ts @@ -30,7 +30,7 @@ const initialState: { language: 'en', torEnbled: false, inheritanceModal: true, - satsEnabled: false, + satsEnabled: true, whirlpoolSwiperModal: true, keySecurityTips: '', letterToAttorny: '', diff --git a/src/store/sagas/bhr.ts b/src/store/sagas/bhr.ts index bea668b4f..3367b051c 100644 --- a/src/store/sagas/bhr.ts +++ b/src/store/sagas/bhr.ts @@ -77,6 +77,8 @@ import { setAppId } from '../reducers/storage'; import { applyRestoreSequence } from './restoreUpgrade'; import { KEY_MANAGEMENT_VERSION } from './upgrade'; import { RootState } from '../store'; +import { setupRecoveryKeySigningKey } from 'src/hardware/signerSetup'; +import { addSigningDeviceWorker } from './wallets'; export function* updateAppImageWorker({ payload, @@ -390,6 +392,9 @@ function* getAppImageWorker({ payload }) { ); } } + + const recoveryKeySigner = setupRecoveryKeySigningKey(primaryMnemonic); + yield call(addSigningDeviceWorker, { payload: { signers: [recoveryKeySigner] } }); } catch (err) { console.log(err); yield put(setAppImageError(true)); diff --git a/src/store/sagas/send_and_receive.ts b/src/store/sagas/send_and_receive.ts index 21e704d65..ee62622e1 100644 --- a/src/store/sagas/send_and_receive.ts +++ b/src/store/sagas/send_and_receive.ts @@ -23,6 +23,7 @@ import { sendPhaseThreeExecuted, sendPhaseTwoExecuted, setSendMaxFee, + setSendMaxFeeEstimatedBlocks, crossTransferExecuted, crossTransferFailed, sendPhaseTwoStarted, @@ -408,7 +409,7 @@ function* calculateSendMaxFee({ payload }: CalculateSendMaxFeeAction) { (state) => state.network.averageTxFees ); const averageTxFeeByNetwork = averageTxFees[wallet.networkType]; - const { feePerByte } = averageTxFeeByNetwork[TxPriority.LOW]; + const { feePerByte, estimatedBlocks } = averageTxFeeByNetwork[TxPriority.LOW]; const network = WalletUtilities.getNetworkByType(wallet.networkType); const { fee } = WalletOperations.calculateSendMaxFee( @@ -420,6 +421,7 @@ function* calculateSendMaxFee({ payload }: CalculateSendMaxFeeAction) { ); yield put(setSendMaxFee(fee)); + yield put(setSendMaxFeeEstimatedBlocks(estimatedBlocks)); } export const calculateSendMaxFeeWatcher = createWatcher( diff --git a/src/store/sagas/storage.ts b/src/store/sagas/storage.ts index 8764a762d..1bf4db774 100644 --- a/src/store/sagas/storage.ts +++ b/src/store/sagas/storage.ts @@ -15,10 +15,11 @@ import Relay from 'src/services/backend/Relay'; import config from 'src/utils/service-utilities/config'; import { createWatcher } from '../utilities'; import { SETUP_KEEPER_APP, SETUP_KEEPER_APP_VAULT_RECOVERY } from '../sagaActions/storage'; -import { addNewWalletsWorker, NewWalletInfo } from './wallets'; +import { addNewWalletsWorker, NewWalletInfo, addSigningDeviceWorker } from './wallets'; import { setAppId } from '../reducers/storage'; import { setAppCreationError } from '../reducers/login'; import { resetRealyWalletState } from '../reducers/bhr'; +import { setupRecoveryKeySigningKey } from 'src/hardware/signerSetup'; export const defaultTransferPolicyThreshold = null; export const maxTransferPolicyThreshold = 1e11; @@ -83,7 +84,10 @@ export function* setupKeeperAppWorker({ payload }) { }, }, }; + + const recoveryKeySigner = setupRecoveryKeySigningKey(primaryMnemonic); yield call(addNewWalletsWorker, { payload: [defaultWallet] }); + yield call(addSigningDeviceWorker, { payload: { signers: [recoveryKeySigner] } }); yield put(setAppId(appID)); yield put(resetRealyWalletState()); } else { diff --git a/src/store/sagas/uai.ts b/src/store/sagas/uai.ts index b39f81c66..19f540d58 100644 --- a/src/store/sagas/uai.ts +++ b/src/store/sagas/uai.ts @@ -178,7 +178,9 @@ function* uaiChecksWorker({ payload }) { } if (checkForTypes.includes(uaiType.SIGNING_DEVICES_HEALTH_CHECK)) { // check for each signer if health check uai is needed - const signers: Signer[] = dbManager.getCollection(RealmSchema.Signer); + const signers: Signer[] = dbManager + .getCollection(RealmSchema.Signer) + .filter((signer) => !signer.hidden); if (signers.length > 0) { for (const signer of signers) { const lastHealthCheck = isTestnet() diff --git a/src/store/sagas/wallets.ts b/src/store/sagas/wallets.ts index c74b3fcc4..48b2a8149 100644 --- a/src/store/sagas/wallets.ts +++ b/src/store/sagas/wallets.ts @@ -137,6 +137,9 @@ import { setRelaySignersUpdateLoading, setRelayVaultUpdateLoading, setRelayWalletUpdateLoading, + showDeletingKeyModal, + hideDeletingKeyModal, + showKeyDeletedSuccessModal, } from '../reducers/bhr'; import { setElectrumNotConnectedErr } from '../reducers/login'; import { connectToNodeWorker } from './network'; @@ -629,7 +632,11 @@ export function* addNewVaultWorker({ export const addNewVaultWatcher = createWatcher(addNewVaultWorker, ADD_NEW_VAULT); -function* addSigningDeviceWorker({ payload: { signers } }: { payload: { signers: Signer[] } }) { +export function* addSigningDeviceWorker({ + payload: { signers }, +}: { + payload: { signers: Signer[] }; +}) { if (!signers.length) return; for (let i = 0; i < signers.length; i++) { const signer = signers[i]; @@ -748,6 +755,7 @@ export const addSigningDeviceWatcher = createWatcher(addSigningDeviceWorker, ADD function* deleteSigningDeviceWorker({ payload: { signers } }: { payload: { signers: Signer[] } }) { try { if (signers.length) { + yield put(showDeletingKeyModal()); let signersToDeleteIds = []; for (const signer of signers) { signersToDeleteIds.push(signer.masterFingerprint); @@ -760,9 +768,12 @@ function* deleteSigningDeviceWorker({ payload: { signers } }: { payload: { signe }); } yield put(uaiChecks([uaiType.SIGNING_DEVICES_HEALTH_CHECK])); + yield put(hideDeletingKeyModal()); + yield put(showKeyDeletedSuccessModal()); } } catch (error) { captureError(error); + yield put(hideDeletingKeyModal()); yield put(relaySignersUpdateFail('An error occurred while deleting signers.')); } } diff --git a/src/theme/Colors.ts b/src/theme/Colors.ts index 53ad8f700..2ac60f918 100644 --- a/src/theme/Colors.ts +++ b/src/theme/Colors.ts @@ -70,12 +70,12 @@ const Colors = { OffWhite: 'rgba(230,230,223,1)', SageGreen: 'rgba(141,157,150,1)', SlateGrey: 'rgba(36,49,46,1)', - // LightKhaki: 'rgba(217,209,169,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)', + PaleKhaki: 'rgba(95,106,103,0.5)', PaleTurquoise: 'rgba(184,214,207,1)', Turquoise: 'rgba(177, 208, 201, 1)', darkGreen: 'rgb(46, 103, 89)', @@ -102,6 +102,6 @@ const Colors = { CoffeeDark: 'rgba(150,130,111, 1)', Periwinkle: 'rgba(184, 182, 208, 1)', Purple: 'rgba(204, 184, 214, 1)', - // ChampagneBliss: 'rgba(253, 247, 240, 1)', + ChampagneBliss: 'rgba(253, 247, 240, 1)', }; export default Colors; diff --git a/src/utils/service-utilities/config.ts b/src/utils/service-utilities/config.ts index 8d7f03251..253b14ce3 100644 --- a/src/utils/service-utilities/config.ts +++ b/src/utils/service-utilities/config.ts @@ -32,7 +32,7 @@ const DEFAULT_CONFIG = { HEXA_ID: 'b01623f1065ba45d68b516efe2873f59bfc9b9b2d8b194f94f989d87d711830a', SENTRY_DNS: 'https://25289533edf7432994f58edeaf6541dc@o1388909.ingest.sentry.io/6711631', ENVIRONMENT: APP_STAGE.DEVELOPMENT, - CHANNEL_URL: 'https://keeper-channel.herokuapp.com/', + CHANNEL_URL: 'https://keeper-channel-dev-8d01fa5233d0.herokuapp.com/', RAMP_BASE_URL: 'https://app.ramp.network/', RAMP_REFERRAL_CODE: 'ku67r7oh5juc27bmb3h5pek8y5heyb5bdtfa66pr', SIGNING_SERVER_RSA_PUBKEY: diff --git a/src/utils/service-utilities/utils.ts b/src/utils/service-utilities/utils.ts index 250140383..50b5a5932 100644 --- a/src/utils/service-utilities/utils.ts +++ b/src/utils/service-utilities/utils.ts @@ -8,9 +8,15 @@ const crypto = require('crypto'); export const getDerivationPath = (derivationPath: string) => derivationPath.substring(2).split("'").join('h'); -export const getMultiKeyExpressions = (signers: VaultSigner[]) => { +export const getMultiKeyExpressions = (signers: VaultSigner[], nextFreeAddressIndex?: number) => { const keyExpressions = signers.map((signer: VaultSigner) => - getKeyExpression(signer.masterFingerprint, signer.derivationPath, signer.xpub) + getKeyExpression( + signer.masterFingerprint, + signer.derivationPath, + signer.xpub, + true, + nextFreeAddressIndex + ) ); return keyExpressions.join(); }; @@ -19,11 +25,18 @@ export const getKeyExpression = ( masterFingerprint: string, derivationPath: string, xpub: string, - withPathRestrictions: boolean = true -) => - `[${masterFingerprint}/${getDerivationPath(derivationPath)}]${xpub}${ - withPathRestrictions ? '/**' : '' - }`; + withPathRestrictions: boolean = true, + nextFreeAddressIndex?: number +) => { + if (nextFreeAddressIndex != undefined) + return `[${masterFingerprint}/${getDerivationPath( + derivationPath + )}]${xpub}/0/${nextFreeAddressIndex}`; + else + return `[${masterFingerprint}/${getDerivationPath(derivationPath)}]${xpub}${ + withPathRestrictions ? '/**' : '' + }`; +}; export const genrateOutputDescriptors = ( wallet: Vault | Wallet, @@ -56,6 +69,44 @@ export const genrateOutputDescriptors = ( }`; }; +export const generateVaultAddressDescriptors = (wallet: Vault | Wallet) => { + const receivingAddress = WalletOperations.getNextFreeAddress(wallet); + const { nextFreeAddressIndex } = wallet.specs; + + if (wallet.entityKind === EntityKind.WALLET) { + const { + derivationDetails: { xDerivationPath }, + specs: { xpub }, + } = wallet as Wallet; + const des = `wpkh(${getKeyExpression(wallet.id, xDerivationPath, xpub)})`; + return { + descriptorString: des, + receivingAddress, + }; + } + const { signers, scheme, isMultiSig } = wallet as Vault; + if (!isMultiSig) { + const signer: VaultSigner = signers[0]; + const des = `wpkh(${getKeyExpression( + signer.masterFingerprint, + signer.derivationPath, + signer.xpub + )})`; + return { + descriptorString: des, + receivingAddress, + }; + } + + return { + descriptorString: `wsh(sortedmulti(${scheme.m},${getMultiKeyExpressions( + signers, + nextFreeAddressIndex + )}))`, + receivingAddress, + }; +}; + // PASRER export interface ParsedSignersDetails { xpub: String; @@ -275,3 +326,42 @@ export const createDecipheriv = (data: { iv: string; encryptedData: string }, pa // Returning iv and encrypted data return JSON.parse(decrypted.toString()); }; + + + +export const createCipherGcm = (data: string, password: string) => { + const algorithm = 'aes-256-gcm'; + const key = Buffer.from(password, 'hex'); + const iv = crypto.randomBytes(12); // 12 bytes for GCM + const cipher = crypto.createCipheriv(algorithm, key, iv); + const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]); + const authTag = cipher.getAuthTag(); + return { + iv: iv.toString('hex'), + encryptedData: encrypted.toString('hex'), + authTag: authTag.toString('hex'), + }; +}; + +interface DecryptData { + iv: string; + encryptedData: string; + authTag: string; +} +export const createDecipherGcm = (data: DecryptData, password: string) => { + const algorithm = 'aes-256-gcm'; + const key = Buffer.from(password, 'hex'); + const iv = Buffer.from(data.iv, 'hex'); + const encryptedText = Buffer.from(data.encryptedData, 'hex'); + const authTag = Buffer.from(data.authTag, 'hex'); + const decipher = crypto.createDecipheriv(algorithm, key, iv); + decipher.setAuthTag(authTag); + let decrypted: Buffer; + + try { + decrypted = Buffer.concat([decipher.update(encryptedText), decipher.final()]); + } catch (err) { + throw new Error('Failed to decrypt data: ' + err.message); + } + return JSON.parse(decrypted.toString('utf-8')); +};