mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-05 10:28:54 +00:00
* On iOS, resuming after the 5-minute window showed no biometric prompt — just a blocking alert whose only button logged the user out. A failure to evaluate device security was indistinguishable from a verdict that the device is not secured. Android had the same class of bug, where user-cancel codes led to logout. Outcomes are now classified three ways, so only a genuine credential rejection or a genuinely unsecured device acts destructively; cancels and indeterminate results offer a retry. SecurityManager and ManagedApp share the policy-free parts via app/utils/security/. Also fixes a resume stacking a second prompt behind an open alert, a backgroundSince bypass, and a latched flag that disabled the MDM PIN. * feedback review * gate app state * fix tests
84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {type AppStateStatus} from 'react-native';
|
|
|
|
import {logDebug} from '@utils/log';
|
|
|
|
import {PROMPT_AUTH_AFTER, type AuthenticationSource} from './constants';
|
|
|
|
export type ResumeGateState = {
|
|
backgroundSince: number;
|
|
previousAppState?: AppStateStatus;
|
|
};
|
|
|
|
export type ResumeGateStrategy = {
|
|
|
|
/** Must be synchronous: it decides precedence before any await can interleave. */
|
|
isEnabled: () => boolean;
|
|
|
|
/** True while a prompt or alert is on screen. */
|
|
isGateOpen: () => boolean;
|
|
|
|
/** Blocks authentication without consuming the expiry window (e.g. jailbroken). */
|
|
shouldBlock?: () => Promise<boolean>;
|
|
|
|
/** Resolves true only when the user authenticated. */
|
|
authenticate: (authExpired: boolean) => Promise<boolean>;
|
|
|
|
/** Run the gate even when the window has not elapsed. */
|
|
promptWhenNotExpired: boolean;
|
|
|
|
source: AuthenticationSource;
|
|
};
|
|
|
|
/**
|
|
* Applies the resume/expiry rules to `state`, mutating it in place.
|
|
*
|
|
* `backgroundSince` is cleared only when the user is affirmatively let in, so a blocked or
|
|
* interrupted attempt cannot be bypassed by backgrounding the app again.
|
|
*/
|
|
export const handleAppStateResume = async (
|
|
appState: AppStateStatus,
|
|
state: ResumeGateState,
|
|
strategy: ResumeGateStrategy,
|
|
): Promise<void> => {
|
|
const isActive = appState === 'active';
|
|
const isBackground = appState === 'background';
|
|
|
|
if (isBackground) {
|
|
state.backgroundSince = Date.now();
|
|
}
|
|
|
|
const isResuming = isActive && state.previousAppState === 'background';
|
|
|
|
if (appState !== 'inactive') {
|
|
state.previousAppState = appState;
|
|
}
|
|
|
|
if (!isResuming || !strategy.isEnabled()) {
|
|
return;
|
|
}
|
|
|
|
// The open gate owns the decision; starting another would stack a second prompt
|
|
// behind the alert already on screen.
|
|
if (strategy.isGateOpen()) {
|
|
logDebug(`${strategy.source}: Resume ignored, authentication already in progress`);
|
|
return;
|
|
}
|
|
|
|
const authExpired = state.backgroundSince > 0 && (Date.now() - state.backgroundSince) >= PROMPT_AUTH_AFTER;
|
|
|
|
if (authExpired || strategy.promptWhenNotExpired) {
|
|
const blocked = await strategy.shouldBlock?.();
|
|
if (!blocked) {
|
|
const authenticated = await strategy.authenticate(authExpired);
|
|
if (authenticated) {
|
|
state.backgroundSince = 0;
|
|
}
|
|
}
|
|
} else {
|
|
state.backgroundSince = 0;
|
|
}
|
|
};
|