Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support intent URL to start a new WalletConnect session #271

Merged
merged 21 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d18b3f9
feat(android): add wallet connect deep link
manu0466 Jan 24, 2024
e29d1f8
feat: prepared wallet connect pair action logic
manu0466 Jan 24, 2024
c8f6c69
fix: props type
manu0466 Jan 24, 2024
94881cf
feat: centered text in loading modal
manu0466 Jan 24, 2024
7913aa7
feat: init wallet connect session from uri action
manu0466 Jan 24, 2024
c4408d9
feat(ios): add dpm url schema
manu0466 Jan 24, 2024
9383b98
feat: update walletconnect event parsing
manu0466 Jan 30, 2024
f54829b
refactor: remove session acknowledged
manu0466 Jan 30, 2024
8b2c952
refactor: renamed the function to parse the uri action received troug…
manu0466 Jan 30, 2024
e35d4fd
feat: use a recoil to store the UriAction to handle
manu0466 Jan 30, 2024
dee8dbc
refactor: use useRestToHomeScreen in UnlockApplication screen
manu0466 Jan 30, 2024
6b37272
feat: handle the returnToApp UriAction param
manu0466 Jan 30, 2024
8a60b9b
chore: fix typos
manu0466 Jan 30, 2024
7a910c5
feat: reject all the requests on WalletConnectRequest screen close
manu0466 Jan 31, 2024
651f54e
feat: improve handling of actions received while the app was in backg…
manu0466 Jan 31, 2024
6fd0709
feat: remove WalletConnect requests on error
manu0466 Jan 31, 2024
a892612
chore: removed wc intent uri
manu0466 Jan 31, 2024
3006a8d
chore(android): re added Web3Auth intent uri
manu0466 Jan 31, 2024
0a2538a
docs: fix grammar
manu0466 Jan 31, 2024
54cf25b
refactor: removed unsued url filed in WalletConnectSession
manu0466 Jan 31, 2024
6b8225e
chore: removed unused function
manu0466 Jan 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@
<data android:scheme="https" android:host="desmos-alternate.test-app.link" />
</intent-filter>

<!-- Web3Auth URI Scheme -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="dpmweb3auth" />
</intent-filter>
</activity>
Expand Down
6 changes: 6 additions & 0 deletions ios/DesmosProfileManager/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
<string>dpm</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>dpm</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
Expand Down
32 changes: 24 additions & 8 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import SnackBarProvider from 'lib/SnackBarProvider';
import DesmosPostHogProvider from 'components/DesmosPostHogProvider';
import useCheckKeyChainIntegrity from 'hooks/dataintegrity/useCheckKeyChainIntegrity';
import useInitNotifications from 'hooks/notifications/useInitNotifications';
import { getCachedUriAction, isUriActionPending, onCachedUriActionChange } from 'lib/UriActions';
import { useSetUriAction } from '@recoil/uriaction';

const AppLockLogic = () => {
useLockApplicationOnBlur();
Expand All @@ -20,14 +22,28 @@ const AppLockLogic = () => {
return null;
};

const Navigation = () => (
<NavigationContainer onReady={() => RNBootSplash.hide({ fade: true, duration: 500 })}>
<AppLockLogic />
<DesmosPostHogProvider>
<RootNavigator />
</DesmosPostHogProvider>
</NavigationContainer>
);
const Navigation = () => {
const setUriAction = useSetUriAction();

React.useEffect(() => {
if (isUriActionPending()) {
const action = getCachedUriAction();
if (action) {
setUriAction(action);
}
}
return onCachedUriActionChange(setUriAction);
}, [setUriAction]);

return (
<NavigationContainer onReady={() => RNBootSplash.hide({ fade: true, duration: 500 })}>
<AppLockLogic />
<DesmosPostHogProvider>
<RootNavigator />
</DesmosPostHogProvider>
</NavigationContainer>
);
};

const App = () => (
<RecoilRoot>
Expand Down
4 changes: 3 additions & 1 deletion src/assets/locales/en/walletConnect.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"reject": "Reject",
"authorization": "Authorization",
"app authorized": "{{app}} authorized",
"app authorized, you can return to the app": "{{app}} authorized, you can return to the app",
"go to authorization": "Go to authorization",
"error while closing session": "Ooops an error occurred while terminating the session:\n{{error}}"
"error while closing session": "Ooops an error occurred while terminating the session:\n{{error}}",
"initializing new session": "Initializing new WalletConnect session..."
}
4 changes: 2 additions & 2 deletions src/contexts/GraphQLClientProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ApolloProvider } from '@apollo/client';
import React from 'react';
import React, { PropsWithChildren } from 'react';
import useGraphQLClient from 'services/graphql/client';

const GraphQLClientProvider: React.FC = ({ children }) => {
const GraphQLClientProvider: React.FC<PropsWithChildren> = ({ children }) => {
const client = useGraphQLClient();
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
Expand Down
4 changes: 2 additions & 2 deletions src/contexts/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react';
import { Appearance, NativeEventSubscription } from 'react-native';
import { Provider as PaperProvider } from 'react-native-paper';
import { Settings } from 'react-native-paper/lib/typescript/core/settings';
Expand All @@ -12,7 +12,7 @@ const PaperProviderSettings: Settings = {
icon: (props) => <DesmosIcon {...props} />,
};

const ThemeProvider: React.FC = ({ children }) => {
const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
const settings = useSettings();
const [appTheme, setAppTheme] = useState(LightTheme);
const colorScheme = useDebouncingColorScheme();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { SessionTypes } from '@walletconnect/types';
import useTrackEvent from 'hooks/analytics/useTrackEvent';
import { Events } from 'types/analytics';

Expand All @@ -11,11 +10,11 @@ const useTrackWalletConnectSessionEstablished = () => {
const trackEvent = useTrackEvent();

return React.useCallback(
(session: SessionTypes.Struct) => {
(appName: string, namespaces: any) => {
trackEvent(Events.WalletConnectSessionEstablished, {
CreationTime: new Date().toISOString(),
ApplicationName: session.peer.metadata.name,
Namespaces: session.requiredNamespaces,
ApplicationName: appName,
Namespaces: namespaces,
});
},
[trackEvent],
Expand Down
77 changes: 69 additions & 8 deletions src/hooks/uriactions/useHandleUriAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@ import {
UriActionType,
UserAddressActionUri,
ViewProfileActionUri,
WalletConnectPairActionUri,
} from 'types/uri';
import useShowModal from 'hooks/useShowModal';
import GenericUriActionModal from 'modals/GenericUriActionModal';
import { getCachedUriAction } from 'lib/UriActions';
import { useNavigation } from '@react-navigation/native';
import { RootNavigatorParamList } from 'navigation/RootNavigator';
import { StackNavigationProp } from '@react-navigation/stack';
import ROUTES from 'navigation/routes';
import useRequestChainChange from 'hooks/chainselect/useRequestChainChange';
import { Trans } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import { chainTypeToChainName } from 'lib/FormatUtils';
import Typography from 'components/Typography';
import { useActiveAccountAddress } from '@recoil/activeAccount';
import useWalletConnectPair from 'hooks/walletconnect/useWalletConnectPair';
import ErrorModal from 'modals/ErrorModal';
import LoadingModal from 'modals/LoadingModal';
import useModal from 'hooks/useModal';
import { useGetUriAction, useSetUriAction } from '@recoil/uriaction';

const useHandleGenericAction = () => {
const showModal = useShowModal();
Expand Down Expand Up @@ -97,6 +103,40 @@ const useHandleSendTokensAction = () => {
);
};

const useHandleWalletConnectPairAction = () => {
const { t } = useTranslation('walletConnect');
const activeAccountAddress = useActiveAccountAddress();
const pair = useWalletConnectPair();
const navigation = useNavigation<StackNavigationProp<RootNavigatorParamList>>();
const { showModal, hideModal } = useModal();

return React.useCallback(
async (action: WalletConnectPairActionUri) => {
if (activeAccountAddress === undefined) {
// Ignore it if we don't have an account.
return;
}

showModal(LoadingModal, {
text: t('initializing new session'),
});
const pairResult = await pair(action.uri);
hideModal();
if (pairResult.isErr()) {
showModal(ErrorModal, {
text: pairResult.error.message,
});
} else {
navigation.navigate(ROUTES.WALLET_CONNECT_SESSION_PROPOSAL, {
proposal: pairResult.value,
returnToApp: action.returnToApp,
});
}
},
[activeAccountAddress, hideModal, navigation, pair, showModal, t],
);
};
Comment on lines +106 to +138
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation of useHandleWalletConnectPairAction correctly handles the WalletConnect pairing process, including showing loading and error modals as appropriate. However, consider adding error logging for the case when pairResult.isErr() is true, to aid in debugging and monitoring.

if (pairResult.isErr()) {
  showModal(ErrorModal, {
    text: pairResult.error.message,
  });
+ // Consider logging this error for better monitoring and debugging
+ console.error(`WalletConnect pairing failed: ${pairResult.error.message}`);
}

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
const useHandleWalletConnectPairAction = () => {
const { t } = useTranslation('walletConnect');
const activeAccountAddress = useActiveAccountAddress();
const pair = useWalletConnectPair();
const navigation = useNavigation<StackNavigationProp<RootNavigatorParamList>>();
const { showModal, hideModal } = useModal();
return React.useCallback(
async (action: WalletConnectPairActionUri) => {
if (activeAccountAddress === undefined) {
// Ignore it if we don't have an account.
return;
}
showModal(LoadingModal, {
text: t('initializing new session'),
});
const pairResult = await pair(action.uri);
hideModal();
if (pairResult.isErr()) {
showModal(ErrorModal, {
text: pairResult.error.message,
});
} else {
navigation.navigate(ROUTES.WALLET_CONNECT_SESSION_PROPOSAL, {
proposal: pairResult.value,
returnToApp: action.returnToApp,
});
}
},
[activeAccountAddress, hideModal, navigation, pair, showModal, t],
);
};
const useHandleWalletConnectPairAction = () => {
const { t } = useTranslation('walletConnect');
const activeAccountAddress = useActiveAccountAddress();
const pair = useWalletConnectPair();
const navigation = useNavigation<StackNavigationProp<RootNavigatorParamList>>();
const { showModal, hideModal } = useModal();
return React.useCallback(
async (action: WalletConnectPairActionUri) => {
if (activeAccountAddress === undefined) {
// Ignore it if we don't have an account.
return;
}
showModal(LoadingModal, {
text: t('initializing new session'),
});
const pairResult = await pair(action.uri);
hideModal();
if (pairResult.isErr()) {
showModal(ErrorModal, {
text: pairResult.error.message,
});
// Consider logging this error for better monitoring and debugging
console.error(`WalletConnect pairing failed: ${pairResult.error.message}`);
} else {
navigation.navigate(ROUTES.WALLET_CONNECT_SESSION_PROPOSAL, {
proposal: pairResult.value,
returnToApp: action.returnToApp,
});
}
},
[activeAccountAddress, hideModal, navigation, pair, showModal, t],
);
};


/**
* Hook that provides a function that will handle the uri action
* if present.
Expand All @@ -105,18 +145,29 @@ const useHandleUriAction = () => {
const handleGenericAction = useHandleGenericAction();
const handleViewProfileAction = useHandleViewProfileAction();
const handleSendTokensAction = useHandleSendTokensAction();
const handleWalletConnectPairAction = useHandleWalletConnectPairAction();
const getUriAction = useGetUriAction();
const setUriAction = useSetUriAction();

return React.useCallback(
(uriAction?: UriAction, genericActionOverride?: GenericActionsTypes) => {
const action = uriAction ?? getCachedUriAction();
async (uriAction?: UriAction, genericActionOverride?: GenericActionsTypes) => {
const globalAction = await getUriAction();
const action = uriAction ?? globalAction;

// Clear the global action if is the one that
// we are handling.
if (action === globalAction) {
// Clear the global action.
setUriAction(undefined);
}

if (action !== undefined) {
let toHandleAction = action.type;
if (toHandleAction === UriActionType.Generic && genericActionOverride !== undefined) {
toHandleAction = genericActionOverride;
}

// In the following switch-cases we need to berform some casting
// becouse ts is not able to infer the type of action
// In the following switch-cases we need to perform some casting
// because ts is not able to infer the type of action
// since we are performing the switch on another variable instead
// of action.type.
switch (toHandleAction) {
Expand All @@ -130,12 +181,22 @@ const useHandleUriAction = () => {
case UriActionType.SendTokens:
handleSendTokensAction(action as SendTokensActionUri | GenericActionUri);
break;
case UriActionType.WalletConnectPair:
handleWalletConnectPairAction(action as WalletConnectPairActionUri);
break;
default:
break;
}
}
},
[handleGenericAction, handleSendTokensAction, handleViewProfileAction],
[
getUriAction,
handleGenericAction,
handleSendTokensAction,
handleViewProfileAction,
handleWalletConnectPairAction,
setUriAction,
],
);
};

Expand Down
33 changes: 33 additions & 0 deletions src/hooks/uriactions/useInitLinkingUriActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { setCachedUriAction, parseNativeActionUri } from 'lib/UriActions';
import React from 'react';
import { Linking } from 'react-native';

/**
* Hook that initialize the logic to handle the
* actions received from the Linking library.
*/
const useInitLinkingUriActions = () => {
const handleLinkingUrl = React.useCallback((url: string | null) => {
if (url) {
const action = parseNativeActionUri(url);
if (action) {
setCachedUriAction(action);
}
}
}, []);

React.useEffect(() => {
// Handle the uri that has triggered the app open.
Linking.getInitialURL().then(handleLinkingUrl);

// Handle the uri received while the app was
// in the background.
const listener = Linking.addEventListener('url', ({ url }) => {
handleLinkingUrl(url);
});

return () => listener.remove();
}, [handleLinkingUrl]);
};

export default useInitLinkingUriActions;
72 changes: 72 additions & 0 deletions src/hooks/useHandleReceivedActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useAppState } from '@recoil/appState';
import { useUriAction } from '@recoil/uriaction';
import { useAllWalletConnectSessionsRequests } from '@recoil/walletConnectRequests';
import React from 'react';
import { useNavigation } from '@react-navigation/native';
import { RootNavigatorParamList } from 'navigation/RootNavigator';
import { StackNavigationProp } from '@react-navigation/stack';
import ROUTES from 'navigation/routes';
import useHandleUriAction from './uriactions/useHandleUriAction';

/**
* Hook that provides the logic to handle the requests received
* while the application was in background.
* The actions that the app can receive while in background are:
* - URIActions triggered by a deep link or from a system intent;
* - WalletConnect session requests.
*/
export default function useHandleReceivedActions() {
const appState = useAppState();

const uriAction = useUriAction();
const handlingUriAction = React.useRef(false);
const handleUriAction = useHandleUriAction();

const walletConnectRequests = useAllWalletConnectSessionsRequests();
const handlingWalletConnectRequests = React.useRef(false);
const navigation = useNavigation<StackNavigationProp<RootNavigatorParamList>>();

React.useEffect(() => {
// Prevent the execution if we are already handling an action.
if (handlingUriAction.current) {
if (uriAction === undefined) {
handlingUriAction.current = false;
} else {
return;
}
}

// Prevent the execution if we are handling the WalletConnect requests.
if (handlingWalletConnectRequests.current) {
if (walletConnectRequests.length === 0) {
handlingWalletConnectRequests.current = false;
} else {
return;
}
}

// Ensure that the app is active and unlocked before performing any operation.
if (appState.locked === false && appState.lastObBlur === undefined) {
// We give priority to uri actions received from a deep link
// or a system intent.
if (uriAction !== undefined) {
handleUriAction(uriAction);
handlingUriAction.current = true;
return;
}

if (walletConnectRequests.length > 0) {
// Navigate to the screen that handle the WalletConnect requests.
navigation.navigate(ROUTES.WALLET_CONNECT_REQUEST);
handlingWalletConnectRequests.current = true;
}
}
}, [
appState.lastObBlur,
appState.locked,
handleUriAction,
navigation,
uriAction,
walletConnectRequests,
]);
}
11 changes: 9 additions & 2 deletions src/hooks/useReturnToCurrentScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ const useReturnToCurrentScreen = () => {
const navigator = useNavigation<StackNavigationProp<RootNavigatorParamList>>();

const startingScreenNavigateParams = useMemo(() => {
const { routes } = navigator.getState();
const { routes } = navigator.getState() ?? { routes: [] };
// Handle the case where we default to the empty routes in case
// this hook is called while the navigator instance is not ready.
if (routes.length === 0) {
return undefined;
}

let currentRoute = routes[routes.length - 1];

// Check if the screen that we should return to is one of the modal that we
Expand All @@ -32,8 +38,9 @@ const useReturnToCurrentScreen = () => {

return useCallback(() => {
const canNavigate =
startingScreenNavigateParams !== undefined &&
navigator.getState().routes.find((r) => r.key === startingScreenNavigateParams.key) !==
undefined;
undefined;
if (canNavigate) {
navigator.navigate(startingScreenNavigateParams);
}
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/walletconnect/useInitWalletConnectLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ const useInitWalletConnectLogic = () => {
.filter((s) => !activeSessions.find((as) => as.topic === s.topic))
.forEach(({ topic }) => deleteSessionByTopic(topic));

state.client.pendingRequest.values.forEach((pendingRequest, index, array) => {
onSessionRequest(state.client, pendingRequest, array.length - 1 === index);
state.client.pendingRequest.values.forEach((pendingRequest) => {
onSessionRequest(state.client, pendingRequest);
});
}
}, [deleteSessionByTopic, onSessionRequest, savedSessions, state]);
Expand Down
Loading
Loading