Skip to content

Commit

Permalink
feat: support intent URL to start a new WalletConnect session (#271)
Browse files Browse the repository at this point in the history
## Description

Closes: DPM-190



This PR adds support for a new intent URI to start a new WalletConnect session from an external application.
The new intent URI has the following schema: `dpm://wcV2?uri=${WALLET_CONNECT_URI}&returnToApp=${true | false}`.
Here are the details about the query parameters:
* `uri` - The URL-encoded WalletConnect URI to start the new session;
* `returnToApp` - Indicates whether the application should return to the application that has triggered the session start after the session is established.

---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] provided a link to the relevant issue or specification
- [x] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed all author checklist items have been addressed




## Summary by CodeRabbit

- **New Features**
	- Added support for custom URL schemes to enhance app interoperability.
	- Enhanced URI action handling with new parsing and caching capabilities.
	- Improved WalletConnect functionality with new hooks for session management and action handling.
	- Introduced a more flexible GraphQL client setup to support child components.
	- Updated theme provider to support child components for better theme management.

- **Enhancements**
	- Refined navigation logic to better handle cached URI actions and return to the current screen accurately.
	- Streamlined the handling of received actions and WalletConnect session proposals with updated logic and hooks.
	- Enhanced loading modal styling for better user experience.

- **Bug Fixes**
	- Fixed issues related to WalletConnect session establishment tracking and proposal handling.

- **Refactor**
	- Simplified WalletConnect hooks and removed unnecessary dependencies to streamline the codebase.
	- Updated the `UnlockApplication` and `WalletConnectRequest` components for improved logic and user flow.

- **Documentation**
	- Added comments to clarify the logic in WalletConnect session request handling.
  • Loading branch information
manu0466 authored Jan 31, 2024
1 parent f1bf0c0 commit f0d9f35
Show file tree
Hide file tree
Showing 26 changed files with 419 additions and 111 deletions.
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],
);
};

/**
* 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

0 comments on commit f0d9f35

Please sign in to comment.