diff --git a/android/app/src/main/java/com/expensify/chat/AppStateTrackerModule.java b/android/app/src/main/java/com/expensify/chat/AppStateTrackerModule.java new file mode 100644 index 000000000000..d0c6cd4a246c --- /dev/null +++ b/android/app/src/main/java/com/expensify/chat/AppStateTrackerModule.java @@ -0,0 +1,34 @@ +package com.expensify.chat; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; + +import androidx.annotation.NonNull; + +public class AppStateTrackerModule extends ReactContextBaseJavaModule { + private final ReactApplicationContext reactContext; + + public AppStateTrackerModule(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; + } + + @Override + @NonNull + public String getName() { + return "AppStateTracker"; + } + + @ReactMethod + public void getApplicationState(Promise promise) { + MainApplication applicationContext = (MainApplication) this.reactContext.getApplicationContext(); + WritableMap params = Arguments.createMap(); + params.putString("currentState", applicationContext.getCurrentState()); + params.putString("prevState", applicationContext.getPrevState()); + promise.resolve(params); + } +} diff --git a/android/app/src/main/java/com/expensify/chat/ExpensifyAppPackage.java b/android/app/src/main/java/com/expensify/chat/ExpensifyAppPackage.java index 3d651e907d76..a839f2f9db33 100644 --- a/android/app/src/main/java/com/expensify/chat/ExpensifyAppPackage.java +++ b/android/app/src/main/java/com/expensify/chat/ExpensifyAppPackage.java @@ -23,6 +23,7 @@ public List createNativeModules( modules.add(new StartupTimer(reactContext)); modules.add(new ShareActionHandlerModule(reactContext)); + modules.add(new AppStateTrackerModule(reactContext)); return modules; } diff --git a/android/app/src/main/java/com/expensify/chat/MainApplication.kt b/android/app/src/main/java/com/expensify/chat/MainApplication.kt index 137346d62beb..5d0948ee3405 100644 --- a/android/app/src/main/java/com/expensify/chat/MainApplication.kt +++ b/android/app/src/main/java/com/expensify/chat/MainApplication.kt @@ -1,10 +1,14 @@ package com.expensify.chat +import android.app.Activity import com.facebook.react.common.assets.ReactFontManager import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo +import android.content.Context import android.content.res.Configuration import android.database.CursorWindow +import android.os.Bundle import android.os.Process import androidx.multidex.MultiDexApplication import com.expensify.chat.bootsplash.BootSplashPackage @@ -27,6 +31,11 @@ import expo.modules.ApplicationLifecycleDispatcher import expo.modules.ReactNativeHostWrapper class MainApplication : MultiDexApplication(), ReactApplication { + var currentState: String = "active" + private set + var prevState: String = "inactive" + private set + override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) { override fun getUseDeveloperSupport() = BuildConfig.DEBUG @@ -64,6 +73,30 @@ class MainApplication : MultiDexApplication(), ReactApplication { return } + registerActivityLifecycleCallbacks(object: ActivityLifecycleCallbacks { + override fun onActivityStarted(p0: Activity) { + prevState = currentState + currentState = "active" + } + + override fun onActivityStopped(p0: Activity) { + val isOnForeground = isAppOnForeground() + prevState = currentState + currentState = if (isOnForeground) "active" else "background" + } + + override fun onActivityDestroyed(p0: Activity) { + val isOnForeground = isAppOnForeground() + prevState = currentState + currentState = if (isOnForeground) "active" else "background" + } + + override fun onActivityCreated(p0: Activity, p1: Bundle?) {} + override fun onActivityResumed(p0: Activity) {} + override fun onActivityPaused(p0: Activity) {} + override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {} + }) + SoLoader.init(this, OpenSourceMergedSoMapping) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. @@ -106,4 +139,22 @@ class MainApplication : MultiDexApplication(), ReactApplication { it.pid == pid && it.processName.endsWith(":onfido_process") } } + + /** + * Checks if the application is currently running in the foreground. + * https://stackoverflow.com/a/8490088/8398300 + */ + private fun isAppOnForeground(): Boolean { + val activityManager = applicationContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val appProcesses = activityManager.runningAppProcesses ?: return false + val packageName: String = applicationContext.getPackageName() + for (appProcess in appProcesses) { + if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND && + appProcess.processName == packageName + ) { + return true + } + } + return false + } } diff --git a/src/components/Onfido/index.native.tsx b/src/components/Onfido/index.native.tsx index a30d70ea7464..af4ef07b6083 100644 --- a/src/components/Onfido/index.native.tsx +++ b/src/components/Onfido/index.native.tsx @@ -1,6 +1,6 @@ import {OnfidoCaptureType, OnfidoCountryCode, OnfidoDocumentType, Onfido as OnfidoSDK, OnfidoTheme} from '@onfido/react-native-sdk'; import React, {useEffect} from 'react'; -import {Alert, Linking} from 'react-native'; +import {Alert, Linking, NativeModules} from 'react-native'; import {checkMultiple, PERMISSIONS, RESULTS} from 'react-native-permissions'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useLocalize from '@hooks/useLocalize'; @@ -10,6 +10,8 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {OnfidoError, OnfidoProps} from './types'; +const {AppStateTracker} = NativeModules; + function Onfido({sdkToken, onUserExit, onSuccess, onError}: OnfidoProps) { const {translate} = useLocalize(); @@ -39,7 +41,15 @@ function Onfido({sdkToken, onUserExit, onSuccess, onError}: OnfidoProps) { // If the user cancels the Onfido flow we won't log this error as it's normal. In the React Native SDK the user exiting the flow will trigger this error which we can use as // our "user exited the flow" callback. On web, this event has it's own callback passed as a config so we don't need to bother with this there. if (([CONST.ONFIDO.ERROR.USER_CANCELLED, CONST.ONFIDO.ERROR.USER_TAPPED_BACK, CONST.ONFIDO.ERROR.USER_EXITED] as string[]).includes(errorMessage)) { - onUserExit(); + if (getPlatform() === CONST.PLATFORM.ANDROID) { + AppStateTracker.getApplicationState().then((appState) => { + const wasInBackground = appState.prevState === 'background'; + onUserExit(!wasInBackground); + }); + return; + } + + onUserExit(true); return; } diff --git a/src/components/Onfido/types.ts b/src/components/Onfido/types.ts index f08cdbb566b0..93570b3805b7 100644 --- a/src/components/Onfido/types.ts +++ b/src/components/Onfido/types.ts @@ -15,8 +15,8 @@ type OnfidoProps = { /** Token used to initialize the Onfido SDK */ sdkToken: string; - /** Called when the user intentionally exits the flow without completing it */ - onUserExit: () => void; + /** Called when the user exits the flow without completing it */ + onUserExit: (isUserInitiated?: boolean) => void; /** Called when the user is totally done with Onfido */ onSuccess: (data: OnfidoData) => void; diff --git a/src/pages/ReimbursementAccount/USD/Requestor/VerifyIdentity/VerifyIdentity.tsx b/src/pages/ReimbursementAccount/USD/Requestor/VerifyIdentity/VerifyIdentity.tsx index b936c4faa3da..8b2f6accbdc8 100644 --- a/src/pages/ReimbursementAccount/USD/Requestor/VerifyIdentity/VerifyIdentity.tsx +++ b/src/pages/ReimbursementAccount/USD/Requestor/VerifyIdentity/VerifyIdentity.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; @@ -8,7 +8,7 @@ import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Growl from '@libs/Growl'; -import * as BankAccounts from '@userActions/BankAccounts'; +import {clearOnfidoToken, goToWithdrawalAccountSetupStep, updateReimbursementAccountDraft, verifyIdentityForBankAccount} from '@userActions/BankAccounts'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -23,29 +23,40 @@ function VerifyIdentity({onBackButtonPress}: VerifyIdentityProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); - const [onfidoApplicantID] = useOnyx(ONYXKEYS.ONFIDO_APPLICANT_ID); - const [onfidoToken] = useOnyx(ONYXKEYS.ONFIDO_TOKEN); + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true}); + const [onfidoApplicantID] = useOnyx(ONYXKEYS.ONFIDO_APPLICANT_ID, {canBeMissing: false}); + const [onfidoToken] = useOnyx(ONYXKEYS.ONFIDO_TOKEN, {canBeMissing: false}); + const [onfidoKey, setOnfidoKey] = useState(() => Math.floor(Math.random() * 1000000)); + + const policyID = reimbursementAccount?.achData?.policyID; + const bankAccountID = reimbursementAccount?.achData?.bankAccountID; - const policyID = reimbursementAccount?.achData?.policyID ?? '-1'; const handleOnfidoSuccess = useCallback( (onfidoData: OnfidoData) => { - BankAccounts.verifyIdentityForBankAccount(Number(reimbursementAccount?.achData?.bankAccountID ?? '-1'), {...onfidoData, applicantID: onfidoApplicantID}, policyID); - BankAccounts.updateReimbursementAccountDraft({isOnfidoSetupComplete: true}); + if (!policyID) { + return; + } + + verifyIdentityForBankAccount(Number(bankAccountID), {...onfidoData, applicantID: onfidoApplicantID}, policyID); + updateReimbursementAccountDraft({isOnfidoSetupComplete: true}); }, - [reimbursementAccount, onfidoApplicantID, policyID], + [bankAccountID, onfidoApplicantID, policyID], ); const handleOnfidoError = () => { // In case of any unexpected error we log it to the server, show a growl, and return the user back to the requestor step so they can try again. Growl.error(translate('onfidoStep.genericError'), ONFIDO_ERROR_DISPLAY_DURATION); - BankAccounts.clearOnfidoToken(); - BankAccounts.goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR); + clearOnfidoToken(); + goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR); }; - const handleOnfidoUserExit = () => { - BankAccounts.clearOnfidoToken(); - BankAccounts.goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR); + const handleOnfidoUserExit = (isUserInitiated?: boolean) => { + if (isUserInitiated) { + clearOnfidoToken(); + goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR); + } else { + setOnfidoKey(Math.floor(Math.random() * 1000000)); + } }; return ( @@ -60,6 +71,7 @@ function VerifyIdentity({onBackButtonPress}: VerifyIdentityProps) { Promise<{ + currentState: AppStateStatus; + prevState: AppStateStatus; + }>; +}; + type RNTextInputResetModule = { resetKeyboardInput: (nodeHandle: number | null) => void; }; @@ -39,6 +46,7 @@ declare module 'react-native' { } interface NativeModulesStatic { + AppStateTracker: AppStateTrackerModule; BootSplash: BootSplashModule; StartupTimer: StartupTimer; RNTextInputReset: RNTextInputResetModule;