Skip to content

Commit

Permalink
[Issue-281]: Implement Wallet Connect
Browse files Browse the repository at this point in the history
  • Loading branch information
dominhquang committed Jul 25, 2023
1 parent 3d59f01 commit 134034e
Show file tree
Hide file tree
Showing 16 changed files with 177 additions and 76 deletions.
8 changes: 7 additions & 1 deletion src/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 0 additions & 2 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
52 changes: 44 additions & 8 deletions src/components/Input/InputConnectUrl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,49 @@ 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<AddressScannerProps, 'onChangeAddress' | 'onPressCancel' | 'qrModalVisible'>;
isShowQrModalVisible: boolean;
setQrModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
}

const Component = (
{ isValidValue, scannerProps = {}, value = '', ...inputProps }: Props,
{
isValidValue,
scannerProps = {},
value = '',
isShowQrModalVisible,
setQrModalVisible,
setLoading,
...inputProps
}: Props,
ref: ForwardedRef<TextInput>,
) => {
const theme = useSubWalletTheme().swThemes;
const [isShowQrModalVisible, setIsShowQrModalVisible] = useState<boolean>(false);
const isAddressValid = isValidValue !== undefined ? isValidValue : true;
const [error, setError] = useState<string | undefined>(undefined);
const navigation = useNavigation<RootNavigationProps>();
const toast = useToast();

useEffect(() => setAdjustResize(), []);

const onPressQrButton = useCallback(async () => {
const result = await requestCameraPermission();

if (result === RESULTS.GRANTED) {
setIsShowQrModalVisible(true);
setQrModalVisible(true);
}
}, []);
}, [setQrModalVisible]);

const RightPart = useMemo(() => {
return (
Expand Down Expand Up @@ -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<TextInputFocusEventData>) => {
Expand All @@ -94,8 +130,8 @@ const Component = (

const closeAddressScanner = useCallback(() => {
setError(undefined);
setIsShowQrModalVisible(false);
}, []);
setQrModalVisible(false);
}, [setQrModalVisible]);

return (
<>
Expand Down
6 changes: 5 additions & 1 deletion src/components/WalletConnect/Account/WCAccountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ 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[];
selected: string[];
}

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],
Expand All @@ -22,10 +25,11 @@ export const WCAccountInput = ({ accounts, selected }: Props) => {

return (
<AccountItemBase
customStyle={{ left: { paddingRight: countSelected ? 8 : 0 }, right: { marginRight: -2 } }}
address={''}
leftItem={<AvatarGroup addresses={selectedAccounts.map(acc => acc.address)} />}
middleItem={
<Typography.Text>
<Typography.Text style={{ color: theme.colorWhite, ...FontMedium }}>
{countSelected ? i18n.message.connectedAccounts(countSelected) : i18n.inputLabel.selectAcc}
</Typography.Text>
}
Expand Down
35 changes: 21 additions & 14 deletions src/components/WalletConnect/Account/WCAccountSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -23,6 +24,8 @@ interface Props {
onCancel: () => void;
}

const renderButtonIcon = (color: string) => <Icon phosphorIcon={CheckCircle} weight={'fill'} iconColor={color} />;

export const WCAccountSelect = ({
appliedAccounts,
availableAccounts,
Expand All @@ -32,6 +35,7 @@ export const WCAccountSelect = ({
selectedAccounts,
useModal,
}: Props) => {
const theme = useSubWalletTheme().swThemes;
const modalRef = useRef<ModalRef>();

const onCloseModal = useCallback(() => {
Expand All @@ -49,23 +53,25 @@ export const WCAccountSelect = ({
const selected = !!selectedAccounts.find(address => isSameAddress(address, item.address));

return (
<AccountItemWithName
accountName={item.name}
address={item.address}
avatarSize={24}
direction="horizontal"
isSelected={selected}
key={item.address}
onPress={onSelectAccount(item.address, false)}
showUnselectIcon={true}
/>
<>
<AccountItemWithName
accountName={item.name}
address={item.address}
avatarSize={24}
direction="horizontal"
isSelected={selected}
key={item.address}
onPress={onSelectAccount(item.address, false)}
showUnselectIcon={true}
/>
</>
);
},
[onSelectAccount, selectedAccounts],
);

return (
<View>
<View style={{ width: '100%' }}>
{!availableAccounts.length ? (
<AlertBox
title={i18n.common.noAvailableAccount}
Expand All @@ -79,18 +85,19 @@ export const WCAccountSelect = ({
title={i18n.inputLabel.selectAcc}
items={availableAccounts}
selectedValueMap={{}}
isShowInput
renderSelected={() => <WCAccountInput accounts={availableAccounts} selected={appliedAccounts} />}
renderCustomItem={renderItem}>
<Button
block
style={{ marginTop: theme.margin }}
disabled={!selectedAccounts.length}
icon={<Icon phosphorIcon={CheckCircle} weight={'fill'} />}
icon={renderButtonIcon}
onPress={_onApply}>
{i18n.buttonTitles.applyAccounts(selectedAccounts.length)}
</Button>
</BasicSelectModal>
) : (
<View>
<View style={{ gap: 8 }}>
{availableAccounts.length > 1 && (
<AccountItemWithName
accountName={i18n.common.allAccounts}
Expand Down
10 changes: 7 additions & 3 deletions src/components/WalletConnect/Network/WCNetworkAvatarGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ export const WCNetworkAvatarGroup = ({ networks }: Props) => {
}, [networks]);

const getAvatarStyle = useCallback(
(index: number) => {
(index: number, arrLength: number) => {
let avatarStyles: StyleProp<ViewStyle> = [_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) {
Expand All @@ -56,7 +60,7 @@ export const WCNetworkAvatarGroup = ({ networks }: Props) => {
<View style={[_style.container, countMore > 0 && _style.mlStrong]}>
{networks.slice(0, 3).map((network, index) => {
return (
<View key={network.slug} style={getAvatarStyle(index)}>
<View key={network.slug} style={getAvatarStyle(index, networks.length)}>
<Logo
size={showCount === 3 ? sizeLogo.default : sizeLogo.large}
shape={'squircle'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ import React, { useMemo } from 'react';
import { Text, View } from 'react-native';
import createStyle from './styles';
import { ImageLogosMap } from 'assets/logo';
import { isWalletConnectRequest } from '@subwallet/extension-base/services/wallet-connect-service/helpers';
import { SVGImages } from 'assets/index';

interface Props {
request: ConfirmationRequestBase;
gap?: number;
linkIcon?: React.ReactNode;
linkIconBg?: string;
}

const ConfirmationGeneralInfo: React.FC<Props> = (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]);
Expand All @@ -28,8 +28,7 @@ const ConfirmationGeneralInfo: React.FC<Props> = (props: Props) => {
<View style={styles.container}>
<DualLogo
leftLogo={<Image shape={'squircle'} src={ImageLogosMap.subwallet} squircleSize={56} />}
linkIcon={linkIcon}
linkIconBg={linkIconBg}
linkIcon={isWCRequest && <SVGImages.WalletConnect width={24} height={24} color={theme.colorWhite} />}
rightLogo={<Image shape="squircle" src={{ uri: leftLogoUrl }} squircleSize={56} />}
/>
<Text style={styles.text}>{domain}</Text>
Expand Down
11 changes: 9 additions & 2 deletions src/components/common/SelectModal/BasicSelectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -130,7 +130,14 @@ function _BasicSelectModal<T>(selectModalProps: Props<T>, ref: ForwardedRef<any>
}}>
<View style={{ width: '100%' }}>
{beforeListItem}
{items.map(item => (renderCustomItem ? renderCustomItem(item) : renderItem(item)))}
<ScrollView
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps={'handled'}
style={{ width: '100%', maxHeight: 400 }}
contentContainerStyle={{ gap: 8 }}>
{items.map(item => (renderCustomItem ? renderCustomItem(item) : renderItem(item)))}
</ScrollView>

{selectModalType === 'multi' && renderFooter()}
{children}
</View>
Expand Down
1 change: 1 addition & 0 deletions src/routes/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';

export type WrapperParamList = {
Main: NavigatorScreenParams<HomeStackParamList>;
FirstScreen: undefined;
TransactionAction: NavigatorScreenParams<TransactionActionStackParamList>;
BuyToken: { slug?: string; symbol?: string };
LoadingScreen: undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -48,6 +49,7 @@ export const ConnectWalletConnectConfirmation = ({ request }: Props) => {
const navigation = useNavigation<RootNavigationProps>();
const { params } = request.request;
const toast = useToast();
const { hasMasterPassword } = useSelector((state: RootState) => state.accountState);
const nameSpaceNameMap = useMemo(
(): Record<string, string> => ({
[WALLET_CONNECT_EIP155_NAMESPACE]: 'EVM networks',
Expand Down Expand Up @@ -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) => {
Expand All @@ -137,11 +143,7 @@ export const ConnectWalletConnectConfirmation = ({ request }: Props) => {
return (
<React.Fragment>
<ConfirmationContent>
<ConfirmationGeneralInfo
request={request}
gap={0}
linkIcon={<SVGImages.WalletConnect width={24} height={24} color={theme.colorWhite} />}
/>
<ConfirmationGeneralInfo request={request} gap={0} />
{isUnSupportCase && (
<View>
<View style={{ paddingBottom: 8 }}>
Expand Down
Loading

0 comments on commit 134034e

Please sign in to comment.