mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 10:50:13 +00:00
* MM-70866 explain Android location permission request Co-authored-by: mattermost-code <matty-code@mattermost.com> * MM-70866 build Android location prompt at request time Create the Android location pre-prompt formatter when the prompt is shown so tests that mock react-intl can import the manager, and use the current user locale for those strings. Co-authored-by: mattermost-code <matty-code@mattermost.com> * chore: retrigger CI after unrelated recent_searches flake The ci test job failed on RecentSearches database removal, which this PR does not change. gh run rerun is not permitted for this token. Co-authored-by: mattermost-code <matty-code@mattermost.com> --------- Co-authored-by: mattermost-code <matty-code@mattermost.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
225 lines
8.6 KiB
TypeScript
225 lines
8.6 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {
|
|
removeSessionAttributesField,
|
|
removeSessionAttributesServer,
|
|
setSessionAttributesEnabled,
|
|
setSessionAttributesManifest,
|
|
setSessionAttributesStableValues,
|
|
upsertSessionAttributesField,
|
|
} from '@mattermost/react-native-network-client';
|
|
import {getAndroidId, getIosIdForVendorAsync, nativeApplicationVersion, nativeBuildVersion} from 'expo-application';
|
|
import {isRootedExperimentalAsync, osVersion} from 'expo-device';
|
|
import {defineMessages} from 'react-intl';
|
|
import {Alert, Platform} from 'react-native';
|
|
import {PERMISSIONS, RESULTS, check, request} from 'react-native-permissions';
|
|
|
|
import {fetchSessionAttributesManifest} from '@actions/remote/session_attributes';
|
|
import {License} from '@constants';
|
|
import {SESSION_ATTRIBUTES_SSID_FIELD} from '@constants/session_attributes';
|
|
import DatabaseManager from '@database/manager';
|
|
import {DEFAULT_LOCALE} from '@i18n';
|
|
import {getConfigBooleanValue, getLicense} from '@queries/servers/system';
|
|
import {getCurrentUser} from '@queries/servers/user';
|
|
import {getFullErrorMessage} from '@utils/errors';
|
|
import {getIntlShape} from '@utils/general';
|
|
import {isMinimumLicenseTier} from '@utils/helpers';
|
|
import {logDebug} from '@utils/log';
|
|
|
|
const messages = defineMessages({
|
|
locationPermissionTitle: {
|
|
id: 'session_attributes.location_permission.title',
|
|
defaultMessage: 'Allow location access?',
|
|
},
|
|
locationPermissionMessage: {
|
|
id: 'session_attributes.location_permission.message',
|
|
defaultMessage: "Your location can be used to report the Wi-Fi network name to your administrator when required by your organization's security policy.",
|
|
},
|
|
locationPermissionCancel: {
|
|
id: 'session_attributes.location_permission.cancel',
|
|
defaultMessage: 'Not now',
|
|
},
|
|
locationPermissionContinue: {
|
|
id: 'session_attributes.location_permission.continue',
|
|
defaultMessage: 'Continue',
|
|
},
|
|
});
|
|
|
|
export class SessionAttributesManagerSingleton {
|
|
private locationPermissionRequest?: Promise<void>;
|
|
|
|
syncStaticValues = async (): Promise<void> => {
|
|
const values = await this.collectStaticValues();
|
|
setSessionAttributesStableValues(values);
|
|
};
|
|
|
|
refreshManifest = async (serverUrl: string): Promise<void> => {
|
|
try {
|
|
await this.syncStaticValues();
|
|
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
if (!database) {
|
|
removeSessionAttributesServer(serverUrl);
|
|
return;
|
|
}
|
|
|
|
const sessionAttributesEnabled = await getConfigBooleanValue(database, 'FeatureFlagSessionAttributes');
|
|
const license = await getLicense(database);
|
|
const enabled = sessionAttributesEnabled &&
|
|
isMinimumLicenseTier(license, License.SKU_SHORT_NAME.EnterpriseAdvanced);
|
|
if (!enabled) {
|
|
removeSessionAttributesServer(serverUrl);
|
|
return;
|
|
}
|
|
|
|
setSessionAttributesEnabled(serverUrl, true);
|
|
|
|
const {manifest, error} = await fetchSessionAttributesManifest(serverUrl);
|
|
if (error) {
|
|
logDebug('[SessionAttributesManager.refreshManifest]', getFullErrorMessage(error));
|
|
return;
|
|
}
|
|
|
|
if (!Array.isArray(manifest) || !manifest.length) {
|
|
setSessionAttributesManifest(serverUrl, []);
|
|
return;
|
|
}
|
|
|
|
setSessionAttributesManifest(serverUrl, manifest);
|
|
|
|
if (manifest.some((field) => field.name === SESSION_ATTRIBUTES_SSID_FIELD)) {
|
|
// Not awaited so the permission prompt never blocks the reconnect sync.
|
|
this.requestLocationPermission(serverUrl);
|
|
}
|
|
} catch (error) {
|
|
logDebug('[SessionAttributesManager.refreshManifest]', getFullErrorMessage(error));
|
|
removeSessionAttributesServer(serverUrl);
|
|
}
|
|
};
|
|
|
|
removeServer = (serverUrl: string) => {
|
|
removeSessionAttributesServer(serverUrl);
|
|
};
|
|
|
|
upsertManifestField = (serverUrl: string, field: SAField) => {
|
|
upsertSessionAttributesField(serverUrl, field);
|
|
|
|
if (field.name === SESSION_ATTRIBUTES_SSID_FIELD) {
|
|
this.requestLocationPermission(serverUrl);
|
|
}
|
|
};
|
|
|
|
removeManifestField = (serverUrl: string, name: string) => {
|
|
removeSessionAttributesField(serverUrl, name);
|
|
};
|
|
|
|
/**
|
|
* The SSID is read natively, which both platforms gate behind location authorization.
|
|
* Only the resulting status is logged, never the network name.
|
|
*/
|
|
private requestLocationPermission = (serverUrl: string): Promise<void> => {
|
|
if (!this.locationPermissionRequest) {
|
|
this.locationPermissionRequest = this.requestLocationPermissionInternal(serverUrl).finally(() => {
|
|
this.locationPermissionRequest = undefined;
|
|
});
|
|
}
|
|
|
|
return this.locationPermissionRequest;
|
|
};
|
|
|
|
private requestLocationPermissionInternal = async (serverUrl: string): Promise<void> => {
|
|
const location = Platform.select({
|
|
ios: PERMISSIONS.IOS.LOCATION_WHEN_IN_USE,
|
|
default: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
|
|
});
|
|
|
|
try {
|
|
const status = await check(location);
|
|
if (status === RESULTS.DENIED) {
|
|
if (Platform.OS === 'android' && !await this.confirmAndroidLocationPermission(serverUrl)) {
|
|
logDebug('[SessionAttributesManager.requestLocationPermission] Android location pre-prompt declined');
|
|
return;
|
|
}
|
|
|
|
const result = await request(location);
|
|
logDebug('[SessionAttributesManager.requestLocationPermission] requested for the ssid attribute:', result);
|
|
return;
|
|
}
|
|
|
|
if (status !== RESULTS.GRANTED) {
|
|
logDebug('[SessionAttributesManager.requestLocationPermission] the ssid attribute cannot be collected:', status);
|
|
}
|
|
} catch (error) {
|
|
logDebug('[SessionAttributesManager.requestLocationPermission]', getFullErrorMessage(error));
|
|
}
|
|
};
|
|
|
|
private confirmAndroidLocationPermission = async (serverUrl: string): Promise<boolean> => {
|
|
const intl = getIntlShape(await this.getCurrentLocale(serverUrl));
|
|
|
|
return new Promise((resolve) => {
|
|
Alert.alert(
|
|
intl.formatMessage(messages.locationPermissionTitle),
|
|
intl.formatMessage(messages.locationPermissionMessage),
|
|
[
|
|
{
|
|
text: intl.formatMessage(messages.locationPermissionCancel),
|
|
style: 'cancel',
|
|
onPress: () => resolve(false),
|
|
},
|
|
{
|
|
text: intl.formatMessage(messages.locationPermissionContinue),
|
|
onPress: () => resolve(true),
|
|
},
|
|
],
|
|
{
|
|
cancelable: true,
|
|
onDismiss: () => resolve(false),
|
|
},
|
|
);
|
|
});
|
|
};
|
|
|
|
private getCurrentLocale = async (serverUrl: string): Promise<string> => {
|
|
try {
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const user = await getCurrentUser(database);
|
|
return user?.locale || DEFAULT_LOCALE;
|
|
} catch {
|
|
return DEFAULT_LOCALE;
|
|
}
|
|
};
|
|
|
|
private collectStaticValues = async (): Promise<Record<string, string>> => {
|
|
let clientVersion = '';
|
|
if (nativeApplicationVersion) {
|
|
if (nativeBuildVersion) {
|
|
clientVersion = `${nativeApplicationVersion}+${nativeBuildVersion}`;
|
|
} else {
|
|
clientVersion = nativeApplicationVersion;
|
|
}
|
|
}
|
|
|
|
let clientDeviceId = '';
|
|
if (Platform.OS === 'android') {
|
|
clientDeviceId = getAndroidId();
|
|
} else if (Platform.OS === 'ios') {
|
|
clientDeviceId = (await getIosIdForVendorAsync()) ?? '';
|
|
}
|
|
|
|
const isRooted = await isRootedExperimentalAsync();
|
|
|
|
return {
|
|
jailbreak_detected: isRooted ? 'true' : 'false',
|
|
os_version: osVersion ?? '',
|
|
os_platform: Platform.OS,
|
|
client_version: clientVersion,
|
|
client_device_id: clientDeviceId,
|
|
};
|
|
};
|
|
}
|
|
|
|
const SessionAttributesManager = new SessionAttributesManagerSingleton();
|
|
export default SessionAttributesManager;
|