Skip to content

Commit

Permalink
[Wallet] Fix DAppKit (#4504)
Browse files Browse the repository at this point in the history
### Description

This PR fixes various regressions with DAppKit support in the wallet:
- DAppKit actions not working because our HOC handling `dispatchAfterNavigate` was removed when we migrated to `react-navigation` v5.
- DAppKit not working on iOS (`Linking.canOpenURL` can't be used without having the queried scheme defined in `LSApplicationQueriesSchemes`)
- Deep links not handled when app is launched from a deep link. We'll probably need to revisit the launch flow with our sagas though.

### Other changes

- Upgraded react-navigation to fix edge case with type safe params, see react-navigation/react-navigation#8389
- Removed unnecessary state in DAppKit screens

### Tested

- Tested with [Savings Circle DApp](https://github.com/celo-org/savings-circle-demo) on iOS.

### Related issues

- Part of #4415
- Fixes #3729

### Backwards compatibility

Yes
  • Loading branch information
jeanregisser committed Jul 22, 2020
1 parent 68c80c2 commit 803f89d
Show file tree
Hide file tree
Showing 13 changed files with 131 additions and 130 deletions.
6 changes: 3 additions & 3 deletions packages/dappkit/README.md
Expand Up @@ -14,15 +14,15 @@ DAppKit is currently built with the excellent [Expo framework](https://expo.io)

This section walks you through the main functionalities of DAppKit. You can also find the result of this walkthrough on the [expo base template](https://github.com/celo-org/dappkit-base) on branch [`dappkit-usage`](https://github.com/celo-org/dappkit-base/tree/dappkit-usage).

DAppKit uses deeplinks to communicate between your DApp and the Celo Wallt. All "requests" that your DApp makes to the Wallet needs to contain the follwing meta payload:
DAppKit uses deeplinks to communicate between your DApp and the Celo Wallet. All "requests" that your DApp makes to the Wallet needs to contain the following meta payload:

- `requestId` A string you can pass to DAppKit, that you can use to listen to the resopnse for that request
- `requestId` A string you can pass to DAppKit, that you can use to listen to the response for that request
- `dappName` A string that will be displayed to the user, indicating the DApp requesting access/signature.
- `callback` The deeplink that the Celo Wallet will use to redirect the user back to the DApp with the appropriate payload. If you want the user to be directed to a particular page in your DApp. With Expo, it's as simple as `Linking.makeUrl('/my/path')`

## Requesting Account Address

One of the first actions you will want to do as a DApp Developer is to get the address of your user's account, to display relevant informtion to them. It can be done as simply as:
One of the first actions you will want to do as a DApp Developer is to get the address of your user's account, to display relevant information to them. It can be done as simply as:

([expo base template commit](https://github.com/celo-org/dappkit-base/commit/9ef5d8916018a1f7b09d062fdd601b851fb4bf79))

Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/locales/en-US/dappkit.json
@@ -1,5 +1,5 @@
{
"connectToWallet": "{{dappname}} would like to connect to {{appName}}",
"connectToWallet": "{{dappName}} would like to connect to {{appName}}",
"connect": "Connect",
"cancel": "Cancel",
"allow": "Allow",
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/locales/es-419/dappkit.json
@@ -1,5 +1,5 @@
{
"connectToWallet": "Círculo de Ahorro le gustaría conectarse a {{appName}}",
"connectToWallet": "{{dappName}} desea conectarse a {{appName}}",
"connect": "Conectar",
"cancel": "Cancelar",
"allow": "Permitir",
Expand Down
10 changes: 5 additions & 5 deletions packages/mobile/package.json
Expand Up @@ -53,11 +53,11 @@
"@react-native-firebase/functions": "^6.7.1",
"@react-native-firebase/messaging": "^6.7.1",
"@react-native-firebase/storage": "^6.7.1",
"@react-navigation/compat": "^5.1.18",
"@react-navigation/drawer": "^5.7.5",
"@react-navigation/material-top-tabs": "^5.2.9",
"@react-navigation/native": "^5.3.0",
"@react-navigation/stack": "^5.3.3",
"@react-navigation/compat": "^5.2.1",
"@react-navigation/drawer": "^5.8.6",
"@react-navigation/material-top-tabs": "^5.2.14",
"@react-navigation/native": "^5.7.1",
"@react-navigation/stack": "^5.7.1",
"@segment/analytics-react-native": "^1.1.1-beta.2",
"@segment/analytics-react-native-firebase": "^1.1.1-beta.2",
"@sentry/react-native": "^1.6.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/src/app/actions.ts
Expand Up @@ -128,7 +128,7 @@ export const setLanguage = (language: string) => {
}
}

export const openDeepLink = (deepLink: string) => {
export const openDeepLink = (deepLink: string): OpenDeepLink => {
return {
type: Actions.OPEN_DEEP_LINK,
deepLink,
Expand Down
8 changes: 6 additions & 2 deletions packages/mobile/src/app/saga.ts
Expand Up @@ -7,6 +7,7 @@ import {
Actions,
appLock,
OpenDeepLink,
openDeepLink,
SetAppState,
setAppState,
setLanguage,
Expand Down Expand Up @@ -50,15 +51,18 @@ export function* appInit() {
yield put(setLanguage(language))
}

const deepLink = yield call(Linking.getInitialURL)
const deepLink: string | null = yield call(Linking.getInitialURL)
const inSync = yield call(clockInSync)
if (!inSync) {
navigate(Screens.SetClock)
return
}

if (deepLink) {
handleDeepLink(deepLink)
// TODO: this should dispatch (put) but since this appInit
// is called before the listener is set, we do it this way.
// This is fragile, change me :D
yield call(handleDeepLink, openDeepLink(deepLink))
return
}
}
Expand Down
47 changes: 19 additions & 28 deletions packages/mobile/src/dappkit/DappKitAccountScreen.tsx
Expand Up @@ -11,6 +11,7 @@ import { e164NumberSelector } from 'src/account/selectors'
import { approveAccountAuth } from 'src/dappkit/dappkit'
import { Namespaces, withTranslation } from 'src/i18n'
import DappkitExchangeIcon from 'src/icons/DappkitExchange'
import { noHeader } from 'src/navigator/Headers.v2'
import { navigateBack, navigateHome } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { StackParamList } from 'src/navigator/types'
Expand All @@ -20,16 +21,17 @@ import { currentAccountSelector } from 'src/web3/selectors'

const TAG = 'dappkit/DappKitAccountScreen'

interface State {
dappName: string | null
}

interface StateProps {
account: string | null
phoneNumber: string | null
}

interface DispatchProps {
approveAccountAuth: typeof approveAccountAuth
}

type Props = StateProps &
DispatchProps &
WithTranslation &
StackScreenProps<StackParamList, Screens.DappKitAccountAuth>

Expand All @@ -38,22 +40,12 @@ const mapStateToProps = (state: RootState): StateProps => ({
phoneNumber: e164NumberSelector(state),
})

class DappKitAccountAuthScreen extends React.Component<Props, State> {
static navigationOptions = { header: null }
state = {
dappName: null,
}

componentDidMount() {
const request = this.props.route.params.dappKitRequest

if (!request) {
Logger.error(TAG, 'No request found in navigation props')
return
}
const mapDispatchToProps = {
approveAccountAuth,
}

this.setState({ dappName: request.dappName })
}
class DappKitAccountAuthScreen extends React.Component<Props> {
static navigationOptions = noHeader

linkBack = () => {
const { account, route, phoneNumber } = this.props
Expand All @@ -72,25 +64,23 @@ class DappKitAccountAuthScreen extends React.Component<Props, State> {
Logger.error(TAG, 'No phone number set up for this wallet')
return
}
navigateHome({ dispatchAfterNavigate: approveAccountAuth(request) })
navigateHome({ onAfterNavigate: () => this.props.approveAccountAuth(request) })
}

cancel = () => {
navigateBack()
}

render() {
const { t, account } = this.props
const { dappName } = this.state
const { t, account, route } = this.props
const { dappName } = route.params.dappKitRequest
return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={styles.logo}>
<DappkitExchangeIcon />
</View>
{dappName && (
<Text style={styles.header}>{t('connectToWallet', { dappname: dappName })}</Text>
)}
{!!dappName && <Text style={styles.header}>{t('connectToWallet', { dappName })}</Text>}

<Text style={styles.share}>{t('shareInfo')}</Text>

Expand Down Expand Up @@ -169,6 +159,7 @@ const styles = StyleSheet.create({
},
})

export default connect<StateProps, null, {}, RootState>(mapStateToProps)(
withTranslation<Props>(Namespaces.dappkit)(DappKitAccountAuthScreen)
)
export default connect<StateProps, DispatchProps, {}, RootState>(
mapStateToProps,
mapDispatchToProps
)(withTranslation<Props>(Namespaces.dappkit)(DappKitAccountAuthScreen))
37 changes: 15 additions & 22 deletions packages/mobile/src/dappkit/DappKitSignTxScreen.tsx
@@ -1,7 +1,6 @@
import Button, { BtnTypes } from '@celo/react-components/components/Button'
import colors from '@celo/react-components/styles/colors'
import fontStyles from '@celo/react-components/styles/fonts'
import { SignTxRequest } from '@celo/utils/src/dappkit'
import { StackScreenProps } from '@react-navigation/stack'
import * as React from 'react'
import { WithTranslation } from 'react-i18next'
Expand All @@ -11,16 +10,14 @@ import { connect } from 'react-redux'
import { requestTxSignature } from 'src/dappkit/dappkit'
import { Namespaces, withTranslation } from 'src/i18n'
import DappkitExchangeIcon from 'src/icons/DappkitExchange'
import { noHeader } from 'src/navigator/Headers.v2'
import { navigate, navigateBack, navigateHome } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { StackParamList } from 'src/navigator/types'
import Logger from 'src/utils/Logger'

const TAG = 'dappkit/DappKitSignTxScreen'

interface State {
request: SignTxRequest | null
}
interface DispatchProps {
requestTxSignature: typeof requestTxSignature
}
Expand All @@ -33,11 +30,8 @@ const mapDispatchToProps = {
requestTxSignature,
}

class DappKitSignTxScreen extends React.Component<Props, State> {
static navigationOptions = { header: null }
state = {
request: null,
}
class DappKitSignTxScreen extends React.Component<Props> {
static navigationOptions = noHeader

componentDidMount() {
const request = this.props.route.params.dappKitRequest
Expand All @@ -50,22 +44,22 @@ class DappKitSignTxScreen extends React.Component<Props, State> {
this.setState({ request })
}

getRequest = () => {
return this.props.route.params.dappKitRequest
}

linkBack = () => {
if (!this.state.request) {
return
}
const request = this.getRequest()

navigateHome({ dispatchAfterNavigate: requestTxSignature(this.state.request!) })
navigateHome({ onAfterNavigate: () => this.props.requestTxSignature(request) })
}

showDetails = () => {
if (!this.state.request) {
return
}
const request = this.getRequest()

// TODO(sallyjyl): figure out which data to pass in for multitx
navigate(Screens.DappKitTxDataScreen, {
dappKitData: (this.state.request! as SignTxRequest).txs[0].txData,
dappKitData: request.txs[0].txData,
})
}

Expand All @@ -75,17 +69,16 @@ class DappKitSignTxScreen extends React.Component<Props, State> {

render() {
const { t } = this.props
const request = this.getRequest()
const { dappName } = request

return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={styles.logo}>
<DappkitExchangeIcon />
</View>
<Text style={styles.header}>
{t('connectToWallet', {
dappname: this.state.request && (this.state.request! as SignTxRequest).dappName,
})}
</Text>
{!!dappName && <Text style={styles.header}>{t('connectToWallet', { dappName })}</Text>}

<Text style={styles.share}> {t('shareInfo')} </Text>

Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/src/dappkit/DappKitTxDataScreen.tsx
Expand Up @@ -6,7 +6,7 @@ import { WithTranslation } from 'react-i18next'
import { ScrollView, StyleSheet, Text } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Namespaces, withTranslation } from 'src/i18n'
import { headerWithBackButton } from 'src/navigator/Headers'
import { headerWithBackButton } from 'src/navigator/Headers.v2'
import { Screens } from 'src/navigator/Screens'
import { StackParamList } from 'src/navigator/types'

Expand Down
12 changes: 11 additions & 1 deletion packages/mobile/src/navigator/NavigationService.ts
Expand Up @@ -131,11 +131,21 @@ export function navigateBack(params?: object) {
})
}

export function navigateHome(params?: object) {
interface NavigateHomeOptions {
onAfterNavigate?: () => void
params?: StackParamList[Screens.DrawerNavigator]
}

export function navigateHome(options?: NavigateHomeOptions) {
const { onAfterNavigate, params } = options ?? {}
navigationRef.current?.reset({
index: 0,
routes: [{ name: Screens.DrawerNavigator, params }],
})

if (onAfterNavigate) {
requestAnimationFrame(onAfterNavigate)
}
}

export function navigateToError(errorMessage: string, error?: Error) {
Expand Down
18 changes: 15 additions & 3 deletions packages/mobile/src/navigator/Navigator.tsx
Expand Up @@ -116,9 +116,21 @@ const commonScreens = (Navigator: typeof Stack) => {
/>
<Navigator.Screen name={Screens.ErrorScreen} component={ErrorScreen} options={noHeader} />
<Navigator.Screen name={Screens.UpgradeScreen} component={UpgradeScreen} />
<Navigator.Screen name={Screens.DappKitAccountAuth} component={DappKitAccountScreen} />
<Navigator.Screen name={Screens.DappKitSignTxScreen} component={DappKitSignTxScreen} />
<Navigator.Screen name={Screens.DappKitTxDataScreen} component={DappKitTxDataScreen} />
<Navigator.Screen
name={Screens.DappKitAccountAuth}
component={DappKitAccountScreen}
options={DappKitAccountScreen.navigationOptions}
/>
<Navigator.Screen
name={Screens.DappKitSignTxScreen}
component={DappKitSignTxScreen}
options={DappKitSignTxScreen.navigationOptions}
/>
<Navigator.Screen
name={Screens.DappKitTxDataScreen}
component={DappKitTxDataScreen}
options={DappKitTxDataScreen.navigationOptions}
/>
<Navigator.Screen name={Screens.Debug} component={Debug} options={Debug.navigationOptions} />
<Navigator.Screen
name={Screens.PhoneNumberLookupQuota}
Expand Down
29 changes: 14 additions & 15 deletions packages/mobile/src/utils/linking.ts
Expand Up @@ -14,21 +14,20 @@ export function navigateToWalletPlayStorePage() {
}

export function navigateToURI(uri: string, backupUri?: string) {
Logger.debug(TAG, 'Navigating to uri', uri)
const onError = (reason: string) => Logger.error(TAG, `Error navigating to URI: ${reason}`)
Linking.canOpenURL(uri)
.then((canOpenUrl: boolean) => {
if (canOpenUrl) {
Linking.openURL(uri).catch(onError)
} else {
Logger.debug(TAG, 'Uri not supported', uri)
if (backupUri) {
Logger.debug(TAG, 'Trying backup uri', uri)
Linking.openURL(backupUri).catch(onError)
}
}
})
.catch(onError)
Logger.debug(TAG, 'Navigating to URI', uri)

// We're NOT using `Linking.canOpenURL` here because we would need
// the scheme to be added to LSApplicationQueriesSchemes on iOS
// which is not possible for DappKit callbacks
Linking.openURL(uri).catch((reason: string) => {
Logger.debug(TAG, 'URI not supported', uri)
if (backupUri) {
Logger.debug(TAG, 'Trying backup URI', backupUri)
navigateToURI(backupUri)
} else {
Logger.error(TAG, `Error navigating to URI: ${reason}`)
}
})
}

export function navigateToPhoneSettings() {
Expand Down

0 comments on commit 803f89d

Please sign in to comment.