From 134034ebee8e357b59db9fb8882f31d683c6c42f Mon Sep 17 00:00:00 2001 From: dominhquang Date: Tue, 25 Jul 2023 18:41:10 +0700 Subject: [PATCH] [Issue-281]: Implement Wallet Connect --- src/AppNavigator.tsx | 8 ++- src/components/Header.tsx | 2 - src/components/Input/InputConnectUrl.tsx | 52 ++++++++++++++++--- .../WalletConnect/Account/WCAccountInput.tsx | 6 ++- .../WalletConnect/Account/WCAccountSelect.tsx | 35 ++++++++----- .../Network/WCNetworkAvatarGroup.tsx | 10 ++-- .../ConfirmationGeneralInfo/index.tsx | 11 ++-- .../common/SelectModal/BasicSelectModal.tsx | 11 +++- src/routes/wrapper.ts | 1 + .../index.tsx | 18 ++++--- src/screens/Home/index.tsx | 14 +++-- .../WalletConnect/ConnectWalletConnect.tsx | 12 ++++- .../WalletConnect/ConnectionDetail.tsx | 8 +-- .../Settings/WalletConnect/ConnectionList.tsx | 39 ++++++++------ src/utils/deeplink/index.ts | 7 +++ src/utils/walletConnect/index.ts | 19 ++++--- 16 files changed, 177 insertions(+), 76 deletions(-) diff --git a/src/AppNavigator.tsx b/src/AppNavigator.tsx index 924b61594..2be272e45 100644 --- a/src/AppNavigator.tsx +++ b/src/AppNavigator.tsx @@ -168,7 +168,7 @@ const AppNavigator = ({ isAppReady }: Props) => { let amount = true; if (hasConfirmations && currentRoute && amount) { if (currentRoute.name !== 'Confirmations' && amount) { - if (currentRoute.name !== 'CreateAccount' && amount) { + if (!['CreateAccount', 'CreatePassword'].includes(currentRoute.name) && amount) { navigationRef.current?.navigate('Confirmations'); } } @@ -205,12 +205,18 @@ const AppNavigator = ({ isAppReady }: Props) => { const urlParsed = new urlParse(url); if (getProtocol(url) === 'subwallet') { if (urlParsed.hostname === 'wc') { + if (urlParsed.query.startsWith('?requestId')) { + return; + } const decodedWcUrl = queryString.decode(urlParsed.query.slice(5)); const finalWcUrl = Object.keys(decodedWcUrl)[0]; connectWalletConnect(finalWcUrl, toast); } } else if (getProtocol(url) === 'https') { if (urlParsed.pathname.split('/')[1] === 'wc') { + if (urlParsed.query.startsWith('?requestId')) { + return; + } const decodedWcUrl = queryString.decode(urlParsed.query.slice(5)); const finalWcUrl = Object.keys(decodedWcUrl)[0]; connectWalletConnect(finalWcUrl, toast); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 905cd2815..7206eaf6e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -49,8 +49,6 @@ export const Header = ({ rightComponent, disabled }: HeaderProps) => { const onScanAddress = useCallback( (data: string) => { - const _error = validWalletConnectUri(data); - console.log('error', _error); if (isAddress(data)) { setError(undefined); setIsScanning(false); diff --git a/src/components/Input/InputConnectUrl.tsx b/src/components/Input/InputConnectUrl.tsx index 1b634d6c9..d1c0397db 100644 --- a/src/components/Input/InputConnectUrl.tsx +++ b/src/components/Input/InputConnectUrl.tsx @@ -10,21 +10,39 @@ import { AddressScanner, AddressScannerProps } from 'components/Scanner/AddressS import { requestCameraPermission } from 'utils/permission/camera'; import { RESULTS } from 'react-native-permissions'; import { setAdjustResize } from 'rn-android-keyboard-adjust'; +import { addConnection } from 'messaging/index'; +import i18n from 'utils/i18n/i18n'; +import { validWalletConnectUri } from 'utils/scanner/walletConnect'; +import { useToast } from 'react-native-toast-notifications'; +import { useNavigation } from '@react-navigation/native'; +import { RootNavigationProps } from 'routes/index'; interface Props extends InputProps { isValidValue?: boolean; showAvatar?: boolean; scannerProps?: Omit; + isShowQrModalVisible: boolean; + setQrModalVisible: React.Dispatch>; + setLoading: React.Dispatch>; } const Component = ( - { isValidValue, scannerProps = {}, value = '', ...inputProps }: Props, + { + isValidValue, + scannerProps = {}, + value = '', + isShowQrModalVisible, + setQrModalVisible, + setLoading, + ...inputProps + }: Props, ref: ForwardedRef, ) => { const theme = useSubWalletTheme().swThemes; - const [isShowQrModalVisible, setIsShowQrModalVisible] = useState(false); const isAddressValid = isValidValue !== undefined ? isValidValue : true; const [error, setError] = useState(undefined); + const navigation = useNavigation(); + const toast = useToast(); useEffect(() => setAdjustResize(), []); @@ -32,9 +50,9 @@ const Component = ( const result = await requestCameraPermission(); if (result === RESULTS.GRANTED) { - setIsShowQrModalVisible(true); + setQrModalVisible(true); } - }, []); + }, [setQrModalVisible]); const RightPart = useMemo(() => { return ( @@ -78,10 +96,28 @@ const Component = ( const onScanInputText = useCallback( (data: string) => { setError(undefined); - setIsShowQrModalVisible(false); + setQrModalVisible(false); onChangeInputText(data); + setLoading(true); + if (!validWalletConnectUri(data)) { + addConnection({ uri: data }) + .then(() => { + setLoading(false); + navigation.goBack(); + }) + .catch(e => { + const errMessage = (e as Error).message; + const message = errMessage.includes('Pairing already exists') + ? i18n.errorMessage.connectionAlreadyExist + : i18n.errorMessage.failToAddConnection; + + toast.hideAll(); + toast.show(message, { type: 'danger' }); + setLoading(false); + }); + } }, - [onChangeInputText], + [navigation, onChangeInputText, setLoading, setQrModalVisible, toast], ); const onInputFocus = (e: NativeSyntheticEvent) => { @@ -94,8 +130,8 @@ const Component = ( const closeAddressScanner = useCallback(() => { setError(undefined); - setIsShowQrModalVisible(false); - }, []); + setQrModalVisible(false); + }, [setQrModalVisible]); return ( <> diff --git a/src/components/WalletConnect/Account/WCAccountInput.tsx b/src/components/WalletConnect/Account/WCAccountInput.tsx index a29a10032..a31f39a86 100644 --- a/src/components/WalletConnect/Account/WCAccountInput.tsx +++ b/src/components/WalletConnect/Account/WCAccountInput.tsx @@ -6,6 +6,8 @@ import { AccountJson } from '@subwallet/extension-base/background/types'; import { isSameAddress } from '@subwallet/extension-base/utils'; import { DotsThree } from 'phosphor-react-native'; import i18n from 'utils/i18n/i18n'; +import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; +import { FontMedium } from 'styles/sharedStyles'; interface Props { accounts: AccountJson[]; @@ -13,6 +15,7 @@ interface Props { } export const WCAccountInput = ({ accounts, selected }: Props) => { + const theme = useSubWalletTheme().swThemes; const selectedAccounts = useMemo( () => accounts.filter(account => selected.some(address => isSameAddress(address, account.address))), [accounts, selected], @@ -22,10 +25,11 @@ export const WCAccountInput = ({ accounts, selected }: Props) => { return ( acc.address)} />} middleItem={ - + {countSelected ? i18n.message.connectedAccounts(countSelected) : i18n.inputLabel.selectAcc} } diff --git a/src/components/WalletConnect/Account/WCAccountSelect.tsx b/src/components/WalletConnect/Account/WCAccountSelect.tsx index 6da54a995..a1fb79574 100644 --- a/src/components/WalletConnect/Account/WCAccountSelect.tsx +++ b/src/components/WalletConnect/Account/WCAccountSelect.tsx @@ -12,6 +12,7 @@ import AccountItemWithName from 'components/common/Account/Item/AccountItemWithN import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; import { CheckCircle } from 'phosphor-react-native'; import { ModalRef } from 'types/modalRef'; +import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; interface Props { selectedAccounts: string[]; @@ -23,6 +24,8 @@ interface Props { onCancel: () => void; } +const renderButtonIcon = (color: string) => ; + export const WCAccountSelect = ({ appliedAccounts, availableAccounts, @@ -32,6 +35,7 @@ export const WCAccountSelect = ({ selectedAccounts, useModal, }: Props) => { + const theme = useSubWalletTheme().swThemes; const modalRef = useRef(); const onCloseModal = useCallback(() => { @@ -49,23 +53,25 @@ export const WCAccountSelect = ({ const selected = !!selectedAccounts.find(address => isSameAddress(address, item.address)); return ( - + <> + + ); }, [onSelectAccount, selectedAccounts], ); return ( - + {!availableAccounts.length ? ( } renderCustomItem={renderItem}> ) : ( - + {availableAccounts.length > 1 && ( { }, [networks]); const getAvatarStyle = useCallback( - (index: number) => { + (index: number, arrLength: number) => { let avatarStyles: StyleProp = [_style.avatarContent]; if (index === 0) { - avatarStyles.push({ marginLeft: 0, opacity: 0.5 }); + if (index === arrLength - 1) { + avatarStyles.push({ marginLeft: 0, opacity: 1 }); + } else { + avatarStyles.push({ marginLeft: 0, opacity: 0.5 }); + } } if (index === 2) { @@ -56,7 +60,7 @@ export const WCNetworkAvatarGroup = ({ networks }: Props) => { 0 && _style.mlStrong]}> {networks.slice(0, 3).map((network, index) => { return ( - + = (props: Props) => { - const { request, gap = 0, linkIcon, linkIconBg } = props; + const { request, gap = 0 } = props; const domain = getDomainFromUrl(request.url); const leftLogoUrl = `https://icons.duckduckgo.com/ip2/${domain}.ico`; - + const isWCRequest = useMemo(() => isWalletConnectRequest(request.id), [request.id]); const theme = useSubWalletTheme().swThemes; const styles = useMemo(() => createStyle(theme, gap), [theme, gap]); @@ -28,8 +28,7 @@ const ConfirmationGeneralInfo: React.FC = (props: Props) => { } - linkIcon={linkIcon} - linkIconBg={linkIconBg} + linkIcon={isWCRequest && } rightLogo={} /> {domain} diff --git a/src/components/common/SelectModal/BasicSelectModal.tsx b/src/components/common/SelectModal/BasicSelectModal.tsx index 699e1f241..f48cd24e3 100644 --- a/src/components/common/SelectModal/BasicSelectModal.tsx +++ b/src/components/common/SelectModal/BasicSelectModal.tsx @@ -2,7 +2,7 @@ import React, { ForwardedRef, forwardRef, useImperativeHandle, useState } from ' import { Button, Divider, Icon, SwModal } from 'components/design-system-ui'; import { IconProps } from 'phosphor-react-native'; import { SelectModalField } from 'components/common/SelectModal/parts/SelectModalField'; -import { View } from 'react-native'; +import { ScrollView, View } from 'react-native'; import { ActionSelectItem } from 'components/common/SelectModal/parts/ActionSelectItem'; import { FilterSelectItem } from 'components/common/SelectModal/parts/FilterSelectItem'; import { ActionItemType } from 'components/Modal/AccountActionSelectModal'; @@ -130,7 +130,14 @@ function _BasicSelectModal(selectModalProps: Props, ref: ForwardedRef }}> {beforeListItem} - {items.map(item => (renderCustomItem ? renderCustomItem(item) : renderItem(item)))} + + {items.map(item => (renderCustomItem ? renderCustomItem(item) : renderItem(item)))} + + {selectModalType === 'multi' && renderFooter()} {children} diff --git a/src/routes/wrapper.ts b/src/routes/wrapper.ts index 56bc27191..5cbcf0c2a 100644 --- a/src/routes/wrapper.ts +++ b/src/routes/wrapper.ts @@ -6,6 +6,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack'; export type WrapperParamList = { Main: NavigatorScreenParams; + FirstScreen: undefined; TransactionAction: NavigatorScreenParams; BuyToken: { slug?: string; symbol?: string }; LoadingScreen: undefined; diff --git a/src/screens/Confirmations/variants/ConnectWalletConnectConfirmation/index.tsx b/src/screens/Confirmations/variants/ConnectWalletConnectConfirmation/index.tsx index 12b28487b..20216b596 100644 --- a/src/screens/Confirmations/variants/ConnectWalletConnectConfirmation/index.tsx +++ b/src/screens/Confirmations/variants/ConnectWalletConnectConfirmation/index.tsx @@ -25,7 +25,8 @@ import { WCAccountSelect } from 'components/WalletConnect/Account/WCAccountSelec import createStyle from './styles'; import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; import { WCNetworkSupported } from 'components/WalletConnect/Network/WCNetworkSupported'; -import { SVGImages } from 'assets/index'; +import { useSelector } from 'react-redux'; +import { RootState } from 'stores/index'; interface Props { request: WalletConnectSessionRequest; @@ -48,6 +49,7 @@ export const ConnectWalletConnectConfirmation = ({ request }: Props) => { const navigation = useNavigation(); const { params } = request.request; const toast = useToast(); + const { hasMasterPassword } = useSelector((state: RootState) => state.accountState); const nameSpaceNameMap = useMemo( (): Record => ({ [WALLET_CONNECT_EIP155_NAMESPACE]: 'EVM networks', @@ -111,8 +113,12 @@ export const ConnectWalletConnectConfirmation = ({ request }: Props) => { }, [namespaceAccounts, request, toast]); const onAddAccount = useCallback(() => { - navigation.replace('CreateAccount', { keyTypes: convertKeyTypes(missingType), isBack: true }); - }, [missingType, navigation]); + if (hasMasterPassword) { + navigation.replace('CreateAccount', { keyTypes: convertKeyTypes(missingType), isBack: true }); + } else { + navigation.replace('CreatePassword', { pathName: 'CreateAccount', state: convertKeyTypes(missingType) }); + } + }, [hasMasterPassword, missingType, navigation]); const onApplyModal = useCallback( (namespace: string) => { @@ -137,11 +143,7 @@ export const ConnectWalletConnectConfirmation = ({ request }: Props) => { return ( - } - /> + {isUnSupportCase && ( diff --git a/src/screens/Home/index.tsx b/src/screens/Home/index.tsx index 76e8e0e13..742ba9eb1 100644 --- a/src/screens/Home/index.tsx +++ b/src/screens/Home/index.tsx @@ -151,6 +151,7 @@ const MainScreen = () => { }; const Wrapper = () => { + const isEmptyAccounts = useCheckEmptyAccounts(); const Drawer = createDrawerNavigator(); return ( { drawerType: 'front', swipeEnabled: false, }}> + {isEmptyAccounts && } ); @@ -176,15 +178,19 @@ export const Home = ({ navigation }: Props) => { const [isLoading, setLoading] = useState(true); useEffect(() => { - if (isReady) { - handleDeeplinkOnFirstOpen(navigation); - } if (isReady && isLoading) { setTimeout(() => setLoading(false), 500); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isReady, isLoading]); + useEffect(() => { + if (isReady) { + handleDeeplinkOnFirstOpen(navigation); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isReady]); + if (isLoading) { return ( @@ -195,7 +201,7 @@ export const Home = ({ navigation }: Props) => { return ( <> - {isEmptyAccounts ? : } + {!isLocked && } ); diff --git a/src/screens/Settings/WalletConnect/ConnectWalletConnect.tsx b/src/screens/Settings/WalletConnect/ConnectWalletConnect.tsx index 99199466c..c4f7f5071 100644 --- a/src/screens/Settings/WalletConnect/ConnectWalletConnect.tsx +++ b/src/screens/Settings/WalletConnect/ConnectWalletConnect.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { ContainerWithSubHeader } from 'components/ContainerWithSubHeader'; import { useNavigation } from '@react-navigation/native'; import { RootNavigationProps } from 'routes/index'; @@ -29,6 +29,7 @@ export const ConnectWalletConnect = () => { const [loading, setLoading] = useState(false); const theme = useSubWalletTheme().swThemes; const toast = useToast(); + const [isShowQrModalVisible, setQrModalVisible] = useState(false); const formConfig = useMemo((): FormControlConfig => { return { @@ -59,7 +60,9 @@ export const ConnectWalletConnect = () => { ? i18n.errorMessage.connectionAlreadyExist : i18n.errorMessage.failToAddConnection; + toast.hideAll(); toast.show(message, { type: 'danger' }); + setLoading(false); }); }; @@ -67,6 +70,10 @@ export const ConnectWalletConnect = () => { onSubmitForm: onSubmit, }); + useEffect(() => { + setQrModalVisible(true); + }, []); + return ( navigation.goBack()} title={i18n.header.walletConnect}> @@ -92,6 +99,9 @@ export const ConnectWalletConnect = () => { placeholder={i18n.placeholder.connectWalletPlaceholder} disabled={loading} onSubmitEditing={formState.errors.uri.length > 0 ? () => Keyboard.dismiss() : onSubmitField('uri')} + isShowQrModalVisible={isShowQrModalVisible} + setQrModalVisible={setQrModalVisible} + setLoading={setLoading} /> {formState.errors.uri.length > 0 && diff --git a/src/screens/Settings/WalletConnect/ConnectionDetail.tsx b/src/screens/Settings/WalletConnect/ConnectionDetail.tsx index 82564a1d7..88e3dd984 100644 --- a/src/screens/Settings/WalletConnect/ConnectionDetail.tsx +++ b/src/screens/Settings/WalletConnect/ConnectionDetail.tsx @@ -139,9 +139,11 @@ export const ConnectionDetail = ({ {i18n.message.connectedAccounts(accountItems.length)} - {accountItems.map(item => ( - - ))} + + {accountItems.map(item => ( + + ))} + - } - /> + <> + {items?.length ? ( + navigation.goBack()} + renderItem={renderItem} + renderListEmptyComponent={renderEmptyList} + searchFunction={searchFunc} + afterListItem={ + + } + /> + ) : ( + + )} + ); }; diff --git a/src/utils/deeplink/index.ts b/src/utils/deeplink/index.ts index d22ae9d03..dcd984537 100644 --- a/src/utils/deeplink/index.ts +++ b/src/utils/deeplink/index.ts @@ -18,10 +18,14 @@ export function handleDeeplinkOnFirstOpen( if (!url || prevDeeplinkUrl === url) { return; } + prevDeeplinkUrl = url; if (getProtocol(url) === 'subwallet') { const urlParsed = new urlParse(url); if (urlParsed.hostname === 'wc') { + if (urlParsed.query.startsWith('?requestId')) { + return; + } const decodedWcUrl = queryString.decode(urlParsed.query.slice(5)); const finalWcUrl = Object.keys(decodedWcUrl)[0]; connectWalletConnect(finalWcUrl, toast); @@ -41,6 +45,9 @@ export function handleDeeplinkOnFirstOpen( } if (urlParsed.pathname.split('/')[1] === 'wc') { + if (urlParsed.query.startsWith('?requestId')) { + return; + } const decodedWcUrl = queryString.decode(urlParsed.query.slice(5)); const finalWcUrl = Object.keys(decodedWcUrl)[0]; connectWalletConnect(finalWcUrl, toast); diff --git a/src/utils/walletConnect/index.ts b/src/utils/walletConnect/index.ts index 6e28ff6f2..4af3d0704 100644 --- a/src/utils/walletConnect/index.ts +++ b/src/utils/walletConnect/index.ts @@ -91,15 +91,20 @@ export const isValidUri = (uri: string) => { return !validWalletConnectUri(uri); }; +const runned: Record = {}; + export const connectWalletConnect = (wcUrl: string, toast?: ToastType) => { if (isValidUri(wcUrl)) { - addConnection({ uri: wcUrl }).catch(e => { - const errMessage = (e as Error).message; - const message = errMessage.includes('Pairing already exists') - ? 'Connection already exists' - : 'Fail to add connection'; - toast?.show(message, { type: 'danger' }); - }); + if (!runned[wcUrl]) { + runned[wcUrl] = true; + addConnection({ uri: wcUrl }).catch(e => { + const errMessage = (e as Error).message; + const message = errMessage.includes('Pairing already exists') + ? 'Connection already exists' + : 'Fail to add connection'; + toast?.show(message, { type: 'danger' }); + }); + } } else { toast?.show('Invalid uri'); }