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 21, 2023
1 parent d441e42 commit 85515d0
Show file tree
Hide file tree
Showing 52 changed files with 2,152 additions and 71 deletions.
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -920,4 +920,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 4a5643dbfb4bccf03b1a886003653271df733a29

COCOAPODS: 1.11.3
COCOAPODS: 1.12.1
40 changes: 38 additions & 2 deletions src/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { LoadingScreen } from 'screens/LoadingScreen';
import { RootRouteProps, RootStackParamList } from './routes';
import { THEME_PRESET } from 'styles/themes';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { deeplinks, getValidURL } from 'utils/browser';
import { deeplinks, getProtocol, getValidURL } from 'utils/browser';
import ErrorBoundary from 'react-native-error-boundary';
import ApplyMasterPassword from 'screens/MasterPassword/ApplyMasterPassword';
import { NetworkSettingDetail } from 'screens/NetworkSettingDetail';
Expand All @@ -55,7 +55,7 @@ import { AddProvider } from 'screens/AddProvider';
import TransactionScreen from 'screens/Transaction/TransactionScreen';
import SendNFT from 'screens/Transaction/NFT';
import changeNavigationBarColor from 'react-native-navigation-bar-color';
import { Platform } from 'react-native';
import { Linking, Platform } from 'react-native';
import { useSubWalletTheme } from 'hooks/useSubWalletTheme';
import { Home } from 'screens/Home';
import { deviceWidth } from 'constants/index';
Expand All @@ -65,6 +65,13 @@ import { WrapperParamList } from 'routes/wrapper';
import { ManageAddressBook } from 'screens/Settings/AddressBook';
import { BuyToken } from 'screens/Home/Crypto/BuyToken';
import useCheckEmptyAccounts from 'hooks/useCheckEmptyAccounts';
import { ConnectionList } from 'screens/Settings/WalletConnect/ConnectionList';
import { ConnectWalletConnect } from 'screens/Settings/WalletConnect/ConnectWalletConnect';
import { ConnectionDetail } from 'screens/Settings/WalletConnect/ConnectionDetail';
import urlParse from 'url-parse';
import queryString from 'querystring';
import { connectWalletConnect } from 'utils/walletConnect';
import { useToast } from 'react-native-toast-notifications';

interface Props {
isAppReady: boolean;
Expand Down Expand Up @@ -121,6 +128,10 @@ const HistoryScreen = (props: JSX.IntrinsicAttributes) => {
return withPageWrapper(History as ComponentType, ['transactionHistory'])(props);
};

const ConnectionListScreen = (props: JSX.IntrinsicAttributes) => {
return withPageWrapper(ConnectionList as ComponentType, ['walletConnect'])(props);
};

const AppNavigator = ({ isAppReady }: Props) => {
const isDarkMode = true;
const theme = isDarkMode ? THEME_PRESET.dark : THEME_PRESET.light;
Expand All @@ -131,6 +142,7 @@ const AppNavigator = ({ isAppReady }: Props) => {
const isEmptyAccounts = useCheckEmptyAccounts();
const { hasConfirmations } = useSelector((state: RootState) => state.requestState);
const { accounts, hasMasterPassword } = useSelector((state: RootState) => state.accountState);
const toast = useToast();

const needMigrate = useMemo(
() =>
Expand Down Expand Up @@ -188,6 +200,27 @@ const AppNavigator = ({ isAppReady }: Props) => {
}
}, [isEmptyAccounts, navigationRef]);

useEffect(() => {
Linking.addEventListener('url', ({ url }) => {
const urlParsed = new urlParse(url);
if (getProtocol(url) === 'subwallet') {
if (urlParsed.hostname === 'wc') {
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') {
const decodedWcUrl = queryString.decode(urlParsed.query.slice(5));
const finalWcUrl = Object.keys(decodedWcUrl)[0];
connectWalletConnect(finalWcUrl, toast);
}
}
});

return () => Linking.removeAllListeners('url');
}, [toast]);

return (
<NavigationContainer linking={linking} ref={navigationRef} theme={theme} onStateChange={onUpdateRoute}>
<ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
Expand Down Expand Up @@ -222,6 +255,9 @@ const AppNavigator = ({ isAppReady }: Props) => {
<Stack.Group screenOptions={{ headerShown: false, animation: 'slide_from_right' }}>
<Stack.Screen name="History" component={HistoryScreen} />
<Stack.Screen name="NetworksSetting" component={NetworksSetting} />
<Stack.Screen name="ConnectList" component={ConnectionListScreen} />
<Stack.Screen name="ConnectDetail" component={ConnectionDetail} />
<Stack.Screen name="ConnectWalletConnect" component={ConnectWalletConnect} />
<Stack.Screen
name="CreatePassword"
component={CreateMasterPassword}
Expand Down
2 changes: 2 additions & 0 deletions src/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const Logo = React.lazy(() => import('./subwallet-logo.svg'));
const LogoGradient = React.lazy(() => import('./subwallet-logo-gradient.svg'));
const MenuBarLogo = React.lazy(() => import('./menu-bar.svg'));
const IcHalfSquare = React.lazy(() => import('./ic-half-square.svg'));
const WalletConnect = React.lazy(() => import('./wallet-connect.svg'));

export const SVGImages = {
Logo,
Expand All @@ -21,6 +22,7 @@ export const SVGImages = {
SignalSplashIcon,
MenuBarLogo,
IcHalfSquare,
WalletConnect,
};

export const Images = {
Expand Down
3 changes: 3 additions & 0 deletions src/assets/wallet-connect.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 18 additions & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { isAddress } from '@polkadot/util-crypto';
import i18n from 'utils/i18n/i18n';
import { DrawerNavigationProp } from '@react-navigation/drawer';
import { DisabledStyle } from 'styles/sharedStyles';
import { validWalletConnectUri } from 'utils/scanner/walletConnect';
import { addConnection } from 'messaging/index';

export interface HeaderProps {
rightComponent?: JSX.Element;
Expand Down Expand Up @@ -47,6 +49,8 @@ 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 All @@ -55,8 +59,21 @@ export const Header = ({ rightComponent, disabled }: HeaderProps) => {
screen: 'TransactionAction',
params: { screen: 'SendFund', params: { recipient: data } },
});
} else if (!validWalletConnectUri(data)) {
addConnection({ uri: data })
.then(() => {
setError(undefined);
setIsScanning(false);
})
.catch(e => {
const errMessage = (e as Error).message;
const message = errMessage.includes('Pairing already exists')
? i18n.errorMessage.connectionAlreadyExist
: i18n.errorMessage.failToAddConnection;
setError(message);
});
} else {
setError(i18n.errorMessage.isNotAnAddress);
setError(i18n.errorMessage.unreadableQrCode);
}
},
[navigation],
Expand Down
126 changes: 126 additions & 0 deletions src/components/Input/InputConnectUrl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import Input, { InputProps } from 'components/design-system-ui/input';
import React, { ForwardedRef, forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import { TextInput } from 'react-native';
import { useSubWalletTheme } from 'hooks/useSubWalletTheme';
import { Button, Icon } from 'components/design-system-ui';
import { Scan } from 'phosphor-react-native';
import { NativeSyntheticEvent } from 'react-native/Libraries/Types/CoreEventTypes';
import { TextInputFocusEventData } from 'react-native/Libraries/Components/TextInput/TextInput';
import { AddressScanner, AddressScannerProps } from 'components/Scanner/AddressScanner';
import { requestCameraPermission } from 'utils/permission/camera';
import { RESULTS } from 'react-native-permissions';
import { setAdjustResize } from 'rn-android-keyboard-adjust';

interface Props extends InputProps {
isValidValue?: boolean;
showAvatar?: boolean;
scannerProps?: Omit<AddressScannerProps, 'onChangeAddress' | 'onPressCancel' | 'qrModalVisible'>;
}

const Component = (
{ isValidValue, scannerProps = {}, value = '', ...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);

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

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

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

const RightPart = useMemo(() => {
return (
<>
<Button
style={{ marginRight: theme.marginXXS }}
disabled={inputProps.disabled || inputProps.readonly}
size={'xs'}
type={'ghost'}
onPress={onPressQrButton}
icon={
<Icon
phosphorIcon={Scan}
size={'sm'}
iconColor={inputProps.readonly ? theme.colorTextLight5 : theme.colorTextLight3}
/>
}
/>
</>
);
}, [
inputProps.disabled,
inputProps.readonly,
onPressQrButton,
theme.colorTextLight3,
theme.colorTextLight5,
theme.marginXXS,
]);

const onChangeInputText = useCallback(
(rawText: string) => {
const text = rawText.trim();

if (inputProps.onChangeText) {
inputProps.onChangeText(text);
}
},
[inputProps],
);

const onScanInputText = useCallback(
(data: string) => {
setError(undefined);
setIsShowQrModalVisible(false);
onChangeInputText(data);
},
[onChangeInputText],
);

const onInputFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
inputProps.onFocus && inputProps.onFocus(e);
};

const onInputBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
inputProps.onBlur && inputProps.onBlur(e);
};

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

return (
<>
<Input
ref={ref}
{...inputProps}
rightPart={RightPart}
isError={!isAddressValid}
onChangeText={onChangeInputText}
onFocus={onInputFocus}
onBlur={onInputBlur}
value={value}
inputStyle={{ paddingRight: 44 }}
/>

<AddressScanner
{...scannerProps}
qrModalVisible={isShowQrModalVisible}
onPressCancel={closeAddressScanner}
onChangeAddress={onScanInputText}
isShowError
error={error}
/>
</>
);
};

export const InputConnectUrl = forwardRef(Component);
8 changes: 5 additions & 3 deletions src/components/Scanner/AddressScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import { BarcodeFinder } from 'screens/Shared/BarcodeFinder';
import { BarCodeReadEvent } from 'react-native-camera';
import i18n from 'utils/i18n/i18n';
import ModalBase from 'components/Modal/Base/ModalBase';
import { overlayColor, rectDimensions } from 'constants/scanner';
import { rectDimensions } from 'constants/scanner';
import { IconButton } from 'components/IconButton';
import { Warning } from 'components/Warning';
import { launchImageLibrary } from 'react-native-image-picker';
import RNQRGenerator from 'rn-qr-generator';
import { Icon } from 'components/design-system-ui';
import { useSubWalletTheme } from 'hooks/useSubWalletTheme';

export interface AddressScannerProps {
onPressCancel: () => void;
Expand Down Expand Up @@ -59,6 +60,7 @@ export const AddressScanner = ({
error,
isShowError = false,
}: AddressScannerProps) => {
const theme = useSubWalletTheme().swThemes;
const onSuccess = (e: BarCodeReadEvent) => {
try {
onChangeAddress(e.data);
Expand All @@ -83,7 +85,7 @@ export const AddressScanner = ({
return (
<ModalBase isVisible={qrModalVisible} style={{ flex: 1, width: '100%', margin: 0 }}>
<SafeAreaView style={ScannerStyles.SafeAreaStyle} />
<StatusBar barStyle={STATUS_BAR_LIGHT_CONTENT} backgroundColor={overlayColor} translucent={true} />
<StatusBar barStyle={STATUS_BAR_LIGHT_CONTENT} backgroundColor={theme.colorBgSecondary} translucent={true} />
<QRCodeScanner
reactivate={true}
reactivateTimeout={5000}
Expand All @@ -97,7 +99,7 @@ export const AddressScanner = ({
customMarker={
<View style={ScannerStyles.RectangleContainerStyle}>
<View style={ScannerStyles.TopOverlayStyle}>
<View style={ScannerStyles.HeaderStyle}>
<View style={[ScannerStyles.HeaderStyle, { backgroundColor: theme.colorBgSecondary }]}>
<Text style={ScannerStyles.HeaderTitleTextStyle}>{i18n.title.scanQrCode}</Text>
<IconButton icon={CaretLeft} style={CancelButtonStyle} onPress={onPressCancel} />
<IconButton
Expand Down
10 changes: 7 additions & 3 deletions src/components/Scanner/QrAddressScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ScannerStyles } from 'styles/scanner';
import { STATUS_BAR_LIGHT_CONTENT } from 'styles/sharedStyles';
import { QrAccount } from 'types/qr/attach';
import i18n from 'utils/i18n/i18n';
import { overlayColor, rectDimensions } from 'constants/scanner';
import { rectDimensions } from 'constants/scanner';
import { BarCodeReadEvent } from 'react-native-camera';
import { getFunctionScan } from 'utils/scanner/attach';
import ModalBase from 'components/Modal/Base/ModalBase';
Expand All @@ -20,6 +20,7 @@ import { launchImageLibrary } from 'react-native-image-picker';
import RNQRGenerator from 'rn-qr-generator';
import { updatePreventLock } from 'stores/MobileSettings';
import { useDispatch } from 'react-redux';
import { useSubWalletTheme } from 'hooks/useSubWalletTheme';

interface Props {
visible: boolean;
Expand Down Expand Up @@ -48,6 +49,7 @@ const BottomSubContentStyle: StyleProp<ViewStyle> = {
};

const QrAddressScanner = ({ visible, onHideModal, onSuccess, type }: Props) => {
const theme = useSubWalletTheme().swThemes;
const [error, setError] = useState<string>('');
const dispatch = useDispatch();
const handleRead = useCallback(
Expand Down Expand Up @@ -105,7 +107,7 @@ const QrAddressScanner = ({ visible, onHideModal, onSuccess, type }: Props) => {
return (
<ModalBase isVisible={visible} style={{ flex: 1, width: '100%', margin: 0 }}>
<SafeAreaView style={ScannerStyles.SafeAreaStyle} />
<StatusBar barStyle={STATUS_BAR_LIGHT_CONTENT} backgroundColor={overlayColor} translucent={true} />
<StatusBar barStyle={STATUS_BAR_LIGHT_CONTENT} backgroundColor={theme.colorBgSecondary} translucent={true} />
<QRCodeScanner
reactivate={true}
reactivateTimeout={5000}
Expand All @@ -118,7 +120,9 @@ const QrAddressScanner = ({ visible, onHideModal, onSuccess, type }: Props) => {
<View style={ScannerStyles.RectangleContainerStyle}>
<View style={ScannerStyles.TopOverlayStyle}>
<View style={ScannerStyles.HeaderStyle}>
<Text style={ScannerStyles.HeaderTitleTextStyle}>{i18n.header.scanQR}</Text>
<Text style={[ScannerStyles.HeaderStyle, { backgroundColor: theme.colorBgSecondary }]}>
{i18n.header.scanQR}
</Text>
<IconButton icon={CaretLeft} style={CancelButtonStyle} onPress={onHideModal} />
<IconButton icon={ImageSquare} style={LibraryButtonStyle} onPress={onPressLibraryBtn} />
</View>
Expand Down
Loading

0 comments on commit 85515d0

Please sign in to comment.