mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-10 05:00:13 +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
(cherry picked from commit f00701ec9a)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
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;
|
|
}
|
|
};
|