Skip to content
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public List<NativeModule> createNativeModules(

modules.add(new StartupTimer(reactContext));
modules.add(new ShareActionHandlerModule(reactContext));
modules.add(new AppStateTrackerModule(reactContext));

return modules;
}
Expand Down
51 changes: 51 additions & 0 deletions android/app/src/main/java/com/expensify/chat/MainApplication.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
}
14 changes: 12 additions & 2 deletions src/components/Onfido/index.native.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();

Expand Down Expand Up @@ -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;
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/Onfido/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand All @@ -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) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This change was not covered correctly to all usages. More details - #77492

if (isUserInitiated) {
clearOnfidoToken();
goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR);
} else {
setOnfidoKey(Math.floor(Math.random() * 1000000));
}
};

return (
Expand All @@ -60,6 +71,7 @@ function VerifyIdentity({onBackButtonPress}: VerifyIdentityProps) {
<FullPageOfflineBlockingView addBottomSafeAreaPadding>
<ScrollView contentContainerStyle={styles.flex1}>
<Onfido
key={onfidoKey}
sdkToken={onfidoToken ?? ''}
onUserExit={handleOnfidoUserExit}
onError={handleOnfidoError}
Expand Down
10 changes: 9 additions & 1 deletion src/types/modules/react-native.d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import type {TargetedEvent} from 'react-native';
import type {AppStateStatus, TargetedEvent} from 'react-native';
import type {BootSplashModule} from '@libs/BootSplash/types';
import type {EnvironmentCheckerModule} from '@libs/Environment/betaChecker/types';
import type {NavBarButtonStyle, NavigationBarType} from '@libs/NavBarManager/types';
import type {ShareActionHandlerModule} from '@libs/ShareActionHandlerModule';
import type {ShortcutManagerModule} from '@libs/ShortcutManager';
import type StartupTimer from '@libs/StartupTimer/types';

type AppStateTrackerModule = {
getApplicationState: () => Promise<{
currentState: AppStateStatus;
prevState: AppStateStatus;
}>;
};

type RNTextInputResetModule = {
resetKeyboardInput: (nodeHandle: number | null) => void;
};
Expand Down Expand Up @@ -39,6 +46,7 @@ declare module 'react-native' {
}

interface NativeModulesStatic {
AppStateTracker: AppStateTrackerModule;
BootSplash: BootSplashModule;
StartupTimer: StartupTimer;
RNTextInputReset: RNTextInputResetModule;
Expand Down