Skip to content

Commit

Permalink
[Wallet] Prevent exiting force update screen (#5430)
Browse files Browse the repository at this point in the history
### Description

The Update screen could be dismissed before this PR. It shouldn't be dismissable, the intention is to force the update.

I had to add the min required version in Redux to force the screen when backgrounding and reopening the app since if the app was reloaded the screen could still be skipped. This happened specially on Android.

### Tested

Locally changing the app version on both platforms.

### Related issues

- Fixes #5429
  • Loading branch information
gnardini authored and Gonzalo Nardini committed Oct 21, 2020
1 parent 8b5ce89 commit 4ea4a0a
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 34 deletions.
4 changes: 2 additions & 2 deletions packages/mobile/src/app/UpgradeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import FullscreenCTA from '@celo/react-components/components/FullscreenCTA'
import * as React from 'react'
import { WithTranslation } from 'react-i18next'
import { Namespaces, withTranslation } from 'src/i18n'
import { headerWithCloseButton } from 'src/navigator/Headers'
import { emptyHeader } from 'src/navigator/Headers'
import { navigateToWalletStorePage } from 'src/utils/linking'

type Props = WithTranslation

class UpgradeScreen extends React.Component<Props> {
static navigationOptions = {
...headerWithCloseButton,
...emptyHeader,
}

render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ exports[`ErrorScreen with errorMessage renders correctly 1`] = `
<RNCSafeAreaView
style={
Object {
"backgroundColor": "#FFFFFF",
"flex": 1,
"height": 1292,
"justifyContent": "space-between",
Expand Down Expand Up @@ -149,6 +150,7 @@ exports[`ErrorScreen without errorMessage renders correctly 1`] = `
<RNCSafeAreaView
style={
Object {
"backgroundColor": "#FFFFFF",
"flex": 1,
"height": 1292,
"justifyContent": "space-between",
Expand Down
14 changes: 14 additions & 0 deletions packages/mobile/src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum Actions {
UNLOCK = 'APP/UNLOCK',
SET_SESSION_ID = 'SET_SESSION_ID',
OPEN_URL = 'APP/OPEN_URL',
MIN_APP_VERSION_DETERMINED = 'APP/MIN_APP_VERSION_DETERMINED',
}

export interface SetAppState {
Expand Down Expand Up @@ -93,6 +94,11 @@ export interface OpenUrlAction {
url: string
}

interface MinAppVersionDeterminedAction {
type: Actions.MIN_APP_VERSION_DETERMINED
minVersion: string | null
}

export type ActionTypes =
| SetAppState
| SetLoggedIn
Expand All @@ -108,6 +114,7 @@ export type ActionTypes =
| Unlock
| SetSessionId
| OpenUrlAction
| MinAppVersionDeterminedAction

export const setAppState = (state: string) => ({
type: Actions.SET_APP_STATE,
Expand Down Expand Up @@ -181,3 +188,10 @@ export const openUrl = (url: string): OpenUrlAction => ({
type: Actions.OPEN_URL,
url,
})

export const minAppVersionDetermined = (
minVersion: string | null
): MinAppVersionDeterminedAction => ({
type: Actions.MIN_APP_VERSION_DETERMINED,
minVersion,
})
7 changes: 7 additions & 0 deletions packages/mobile/src/app/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface State {
locked: boolean
lastTimeBackgrounded: number
sessionId: string
minVersion: string | null
}

const initialState = {
Expand All @@ -29,6 +30,7 @@ const initialState = {
locked: false,
lastTimeBackgrounded: 0,
sessionId: '',
minVersion: null,
}

export const currentLanguageSelector = (state: RootState) => state.app.language || i18n.language
Expand Down Expand Up @@ -128,6 +130,11 @@ export const appReducer = (
...state,
sessionId: action.sessionId,
}
case Actions.MIN_APP_VERSION_DETERMINED:
return {
...state,
minVersion: action.minVersion,
}
default:
return state
}
Expand Down
35 changes: 24 additions & 11 deletions packages/mobile/src/app/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import {
Actions,
appLock,
minAppVersionDetermined,
OpenDeepLink,
openDeepLink,
OpenUrlAction,
Expand All @@ -25,7 +26,7 @@ import {
import { currentLanguageSelector } from 'src/app/reducers'
import { getLastTimeBackgrounded, getRequirePinOnAppOpen } from 'src/app/selectors'
import { handleDappkitDeepLink } from 'src/dappkit/dappkit'
import { isAppVersionDeprecated } from 'src/firebase/firebase'
import { appVersionDeprecationChannel } from 'src/firebase/firebase'
import { receiveAttestationMessage } from 'src/identity/actions'
import { CodeInputType } from 'src/identity/verification'
import { navigate } from 'src/navigator/NavigationService'
Expand All @@ -48,16 +49,6 @@ const DO_NOT_LOCK_PERIOD = 30000 // 30 sec
// Work that's done before other sagas are initalized
// Be mindful to not put long blocking tasks here
export function* appInit() {
const isDeprecated: boolean = yield call(isAppVersionDeprecated)

if (isDeprecated) {
Logger.warn(TAG, 'App version is deprecated')
navigate(Screens.UpgradeScreen)
return
} else {
Logger.debug(TAG, 'App version is valid')
}

const language = yield select(currentLanguageSelector)
if (language) {
yield put(setLanguage(language))
Expand All @@ -79,6 +70,28 @@ export function* appInit() {
}
}

export function* appVersionSaga() {
const appVersionChannel = yield call(appVersionDeprecationChannel)
if (!appVersionChannel) {
return
}
try {
while (true) {
const minRequiredVersion = yield take(appVersionChannel)
Logger.info(TAG, `Required min version: ${minRequiredVersion}`)
// Note: The NavigatorWrapper will read this value from the store and
// show the UpdateScreen if necessary.
yield put(minAppVersionDetermined(minRequiredVersion))
}
} catch (error) {
Logger.error(`${TAG}@appVersionSaga`, error)
} finally {
if (yield cancelled()) {
appVersionChannel.close()
}
}
}

export function* handleDeepLink(action: OpenDeepLink) {
const { deepLink } = action
Logger.debug(TAG, 'Handling deep link', deepLink)
Expand Down
39 changes: 24 additions & 15 deletions packages/mobile/src/firebase/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import firebase, { ReactNativeFirebase } from '@react-native-firebase/app'
import '@react-native-firebase/auth'
import '@react-native-firebase/database'
import { FirebaseDatabaseTypes } from '@react-native-firebase/database'
import '@react-native-firebase/messaging'
// We can't combine the 2 imports otherwise it only imports the type and fails at runtime
// tslint:disable-next-line: no-duplicate-imports
import { FirebaseMessagingTypes } from '@react-native-firebase/messaging'
import DeviceInfo from 'react-native-device-info'
import { eventChannel, EventChannel } from 'redux-saga'
import { call, select, spawn, take } from 'redux-saga/effects'
import { currentLanguageSelector } from 'src/app/reducers'
Expand Down Expand Up @@ -188,30 +187,40 @@ export function isVersionBelowMinimum(version: string, minVersion: string): bool
return false
}

const VALUE_CHANGE_HOOK = 'value'

/*
Get the Version deprecation information.
Firebase DB Format:
(New) Add minVersion child to versions category with a string of the mininum version as string
*/
export async function isAppVersionDeprecated() {
export function appVersionDeprecationChannel() {
if (!FIREBASE_ENABLED) {
return false
return null
}

Logger.info(TAG, 'Checking version info')
const version = DeviceInfo.getVersion()
const errorCallback = (error: Error) => {
Logger.warn(TAG, error.toString())
}

const versionsInfo = (
await firebase
return eventChannel((emit: any) => {
const emitter = (snapshot: FirebaseDatabaseTypes.DataSnapshot) => {
const minVersion = snapshot.val().minVersion
emit(minVersion)
}
const cancel = () => {
firebase
.database()
.ref('versions')
.off(VALUE_CHANGE_HOOK, emitter)
}

firebase
.database()
.ref('versions')
.once('value')
).val()
if (!versionsInfo || !versionsInfo.minVersion) {
return false
}
const minVersion: string = versionsInfo.minVersion
return isVersionBelowMinimum(version, minVersion)
.on(VALUE_CHANGE_HOOK, emitter, errorCallback)
return cancel
})
}

export async function setUserLanguage(address: string, language: string) {
Expand Down
24 changes: 19 additions & 5 deletions packages/mobile/src/navigator/NavigatorWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import AsyncStorage from '@react-native-community/async-storage'
import { DefaultTheme, NavigationContainer, NavigationState } from '@react-navigation/native'
import * as React from 'react'
import { StyleSheet, View } from 'react-native'
import DeviceInfo from 'react-native-device-info'
import AlertBanner from 'src/alert/AlertBanner'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { getAppLocked } from 'src/app/selectors'
import UpgradeScreen from 'src/app/UpgradeScreen'
import { DEV_RESTORE_NAV_STATE_ON_RELOAD } from 'src/config'
import { isVersionBelowMinimum } from 'src/firebase/firebase'
import { navigationRef } from 'src/navigator/NavigationService'
import Navigator from 'src/navigator/Navigator'
import PincodeLock from 'src/pincode/PincodeLock'
Expand Down Expand Up @@ -46,8 +49,21 @@ export const NavigatorWrapper = () => {
const [isReady, setIsReady] = React.useState(RESTORE_STATE ? false : true)
const [initialState, setInitialState] = React.useState()
const appLocked = useTypedSelector(getAppLocked)
const minRequiredVersion = useTypedSelector((state) => state.app.minVersion)
const routeNameRef = React.useRef()

const updateRequired = React.useMemo(() => {
if (!minRequiredVersion) {
return false
}
const version = DeviceInfo.getVersion()
Logger.info(
'NavigatorWrapper',
`Current version: ${version}. Required min version: ${minRequiredVersion}`
)
return isVersionBelowMinimum(version, minRequiredVersion)
}, [minRequiredVersion])

React.useEffect(() => {
if (navigationRef && navigationRef.current) {
const state = navigationRef.current.getRootState()
Expand Down Expand Up @@ -121,13 +137,11 @@ export const NavigatorWrapper = () => {
>
<View style={styles.container}>
<Navigator />
{appLocked && (
<View style={styles.locked}>
<PincodeLock />
</View>
{(appLocked || updateRequired) && (
<View style={styles.locked}>{updateRequired ? <UpgradeScreen /> : <PincodeLock />}</View>
)}
<View style={styles.floating}>
{!appLocked && <BackupPrompt />}
{!appLocked && !updateRequired && <BackupPrompt />}
<AlertBanner />
</View>
</View>
Expand Down
3 changes: 2 additions & 1 deletion packages/mobile/src/redux/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AnyAction } from 'redux'
import { call, select, spawn, takeEvery } from 'redux-saga/effects'
import { accountSaga } from 'src/account/saga'
import { devModeSelector } from 'src/account/selectors'
import { appInit, appSaga } from 'src/app/saga'
import { appInit, appSaga, appVersionSaga } from 'src/app/saga'
import { dappKitSaga } from 'src/dappkit/dappkit'
import { escrowSaga } from 'src/escrow/saga'
import { exchangeSaga } from 'src/exchange/saga'
Expand Down Expand Up @@ -73,6 +73,7 @@ export function* rootSaga() {
yield call(appInit)

// Note, the order of these does matter in certain cases
yield spawn(appVersionSaga)
yield spawn(loggerSaga)
yield spawn(appSaga)
yield spawn(sentrySaga)
Expand Down
4 changes: 4 additions & 0 deletions packages/mobile/test/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,10 @@ export const v5Schema = {
},
addressToDisplayName: {},
},
app: {
...v3Schema.app,
minVersion: null,
},
}

export function getLatestSchema(): Partial<RootState> {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-components/components/FullscreenCTA.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
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 variables from '@celo/react-components/styles/variables'
import * as React from 'react'
Expand Down Expand Up @@ -38,6 +39,7 @@ class FullscreenCTA extends React.PureComponent<Props> {

const styles = StyleSheet.create({
container: {
backgroundColor: Colors.light,
height: variables.height,
width: variables.width,
flex: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ exports[`FullscreenCTA renders correctly 1`] = `
<RNCSafeAreaView
style={
Object {
"backgroundColor": "#FFFFFF",
"flex": 1,
"height": 1292,
"justifyContent": "space-between",
Expand Down

0 comments on commit 4ea4a0a

Please sign in to comment.