* refactor: implement custom ExpoImage wrapper for cache control Add ExpoImage component with automatic cacheKey/cachePath management and replace all expo-image imports across the app * refactor(ios): convert Gekidou to CocoaPods Migrate from Swift Package Manager to CocoaPods, add Keychain write operations, refactor notification handler to remove react-native-notifications headers, and upgrade Swift to 5.0 * npm audit * update fastlane * feat(ci): integrate Intune MAM for enterprise builds with strict OSS protection Add Intune submodule, CI actions, Fastlane configuration, developer scripts, pre-commit hooks, and validation workflows to enable internal MAM builds while protecting OSS repository * fix tests by mocking @mattermost/intune * feat: implement Intune MAM integration with comprehensive security enforcement Add IntuneManager, refactor SecurityManager/SessionManager for MAM policies, implement native OIDC auth flow, add biometric enforcement, conditional launch blocking, and file protection controls * fix alerts when no server database is present * Unify cache strategy * fix emit config changed after it was stored in the db * Handle Mid-Session Enrollment Detection * fix ADALLogOverrideDisabled missing in Fastfile * fix flow for initial enrollment * fix and add unit tests * enable Intune configuration for PR and beta builds, CLIENT_ID should be changed before actual release * Update intune submodule with addressed feedback * fix validate-intune-clean workflow * feat(intune): add comprehensive error handling and SAML+Entra support Add production-ready error handling for native Entra authentication with user-friendly i18n messages, comprehensive test coverage, and support for Entra login when server requires SAML. * update i18n * update intune submodule * update build-pr token * fix race condition between server auth and intune enrollment * fix CI workflow to build with intune * use deploy key for intune submodule * set the config directly in the submodule .git * debug injection * try setting GIT_SSH_COMMAND * remove action debug * fix server url input * match pod cache with intune hash * Fastfile and envs * have workflows check for intune/.git * have ci cache intune frameworks as well * update Fastlane to set no-cache to artifacts uploaded * fix s3 upload * fix pblist template * Attempt to remove the cache control for PR uploads to s3 * use hash from commit for S3 path * Implement crash-resilient selective wipe with automatic retry and add removeInternetPassword to Gekidou Keychain * Fix surface errors from intune login * fix postinstall scripts * use cacheKey for draft md images * remove unnecessary double await during test * Have isMinimumLicenseTier accept valid license sku tier as target * Add missing Auth error messages * remove the last period for intune errors in i18n * do not call unenroll with wipe on manual logout * Fix tests and Intune error messages * do not filter any SSO type regardless of which is used for Intune * fix 412 to not retry * fix tests, app logs sharing and share_extension avatar cache * apply setScreenCapturePolicy on license change Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * re-apply screen capture on enrollment Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * use userData from intunr login and prevent getMe Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * Check for Biometrics and Jailbreak as we used to --------- Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>
271 lines
7.7 KiB
TypeScript
271 lines
7.7 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import RNUtils, {type SplitViewResult} from '@mattermost/rnutils';
|
|
import moment, {type Moment} from 'moment-timezone';
|
|
import {Platform} from 'react-native';
|
|
|
|
import License from '@constants/license';
|
|
import {STATUS_BAR_HEIGHT} from '@constants/view';
|
|
|
|
// isMinimumServerVersion will return true if currentVersion is equal to higher or than
|
|
// the provided minimum version. A non-equal major version will ignore minor and dot
|
|
// versions, and a non-equal minor version will ignore dot version.
|
|
// currentVersion is a string, e.g '4.6.0'
|
|
// minMajorVersion, minMinorVersion, minDotVersion are integers
|
|
export const isMinimumServerVersion = (currentVersion = '', minMajorVersion = 0, minMinorVersion = 0, minDotVersion = 0): boolean => {
|
|
if (!currentVersion || typeof currentVersion !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
const split = currentVersion.split('.');
|
|
|
|
const major = parseInt(split[0], 10);
|
|
const minor = parseInt(split[1] || '0', 10);
|
|
const dot = parseInt(split[2] || '0', 10);
|
|
|
|
if (major > minMajorVersion) {
|
|
return true;
|
|
}
|
|
if (major < minMajorVersion) {
|
|
return false;
|
|
}
|
|
|
|
// Major version is equal, check minor
|
|
if (minor > minMinorVersion) {
|
|
return true;
|
|
}
|
|
if (minor < minMinorVersion) {
|
|
return false;
|
|
}
|
|
|
|
// Minor version is equal, check dot
|
|
if (dot > minDotVersion) {
|
|
return true;
|
|
}
|
|
if (dot < minDotVersion) {
|
|
return false;
|
|
}
|
|
|
|
// Dot version is equal
|
|
return true;
|
|
};
|
|
|
|
export type LicenseTierSku = keyof typeof License.LicenseSkuTier;
|
|
|
|
// isMinimumLicenseTier will return true if the license meets or exceeds the target SKU tier
|
|
// license is a ClientLicense object
|
|
// targetSku is a string, e.g 'professional', 'enterprise', 'advanced'
|
|
export const isMinimumLicenseTier = (license: Partial<ClientLicense> | undefined, targetSku: LicenseTierSku): boolean => {
|
|
if (!targetSku || !license) {
|
|
return false;
|
|
}
|
|
|
|
const isLicensed = license.IsLicensed === 'true';
|
|
if (!isLicensed) {
|
|
return false;
|
|
}
|
|
|
|
// Only use SkuShortName if it's a valid SKU
|
|
const sku = license.SkuShortName as LicenseTierSku;
|
|
if (!sku || !(sku in License.LicenseSkuTier)) {
|
|
return false;
|
|
}
|
|
|
|
const currentTier = License.LicenseSkuTier[sku];
|
|
const targetTier = License.LicenseSkuTier[targetSku];
|
|
|
|
return Boolean(currentTier) && Boolean(targetTier) && currentTier >= targetTier;
|
|
};
|
|
|
|
export function buildQueryString(parameters: Dictionary<any>): string {
|
|
const keys = Object.keys(parameters);
|
|
if (keys.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
let query = '?';
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = keys[i];
|
|
if (parameters[key] == null) {
|
|
continue;
|
|
}
|
|
query += key + '=' + encodeURIComponent(parameters[key]);
|
|
|
|
if (i < keys.length - 1) {
|
|
query += '&';
|
|
}
|
|
}
|
|
|
|
if (query.endsWith('&')) {
|
|
return query.slice(0, -1);
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
export function isEmail(email: string): boolean {
|
|
// writing a regex to match all valid email addresses is really, really hard. (see http://stackoverflow.com/a/201378)
|
|
// this regex ensures:
|
|
// - at least one character that is not a space, comma, or @ symbol
|
|
// - followed by a single @ symbol
|
|
// - followed by at least one character that is not a space, comma, or @ symbol
|
|
// this prevents <Outlook Style> outlook.style@domain.com addresses and multiple comma-separated addresses from being accepted
|
|
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
const regexWithoutTLDN = /^[^\s@]+@[^\s@]+$/;
|
|
|
|
return regex.test(email) || regexWithoutTLDN.test(email);
|
|
}
|
|
|
|
export function identity<T>(arg: T): T {
|
|
return arg;
|
|
}
|
|
|
|
export function safeParseJSON(rawJson: string | Record<string, unknown> | unknown[]) {
|
|
let data = rawJson;
|
|
try {
|
|
if (typeof rawJson == 'string') {
|
|
data = JSON.parse(rawJson);
|
|
}
|
|
} catch {
|
|
// Do nothing
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
export function safeParseJSONStringArray(rawJson: unknown) {
|
|
if (Array.isArray(rawJson)) {
|
|
return rawJson.filter((v) => typeof v === 'string');
|
|
}
|
|
|
|
if (typeof rawJson !== 'string') {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const data = JSON.parse(rawJson);
|
|
if (Array.isArray(data)) {
|
|
return data.filter((v) => typeof v === 'string');
|
|
}
|
|
} catch {
|
|
// Do nothing
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
export function getCurrentMomentForTimezone(timezone: string | null) {
|
|
return timezone ? moment.tz(timezone) : moment();
|
|
}
|
|
|
|
export function getUtcOffsetForTimeZone(timezone: string) {
|
|
return moment.tz(timezone).utcOffset();
|
|
}
|
|
|
|
export function toTitleCase(str: string) {
|
|
function doTitleCase(txt: string) {
|
|
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
|
}
|
|
return str.replace(/\w\S*/g, doTitleCase);
|
|
}
|
|
|
|
export function getRoundedTime(value: Moment, roundedTo: number) {
|
|
const start = moment(value);
|
|
const diff = start.minute() % roundedTo;
|
|
if (diff === 0) {
|
|
return value;
|
|
}
|
|
const remainder = roundedTo - diff;
|
|
return start.add(remainder, 'm').seconds(0).milliseconds(0);
|
|
}
|
|
|
|
export function isTablet() {
|
|
const result: SplitViewResult = RNUtils.isRunningInSplitView();
|
|
if (!result) {
|
|
return false;
|
|
}
|
|
return result.isTablet && !result.isSplit;
|
|
}
|
|
|
|
export const pluckUnique = (key: string) => (array: Array<{[key: string]: unknown}>) => Array.from(new Set(array.map((obj) => obj[key])));
|
|
|
|
export function bottomSheetSnapPoint(itemsCount: number, itemHeight: number) {
|
|
return (itemsCount * itemHeight) + STATUS_BAR_HEIGHT;
|
|
}
|
|
|
|
export function hasTrailingSpaces(term: string) {
|
|
return term.length !== term.trimEnd().length;
|
|
}
|
|
|
|
/**
|
|
* isMainActivity returns true if the current activity on Android is the MainActivity otherwise it returns false,
|
|
* on iOS the result is always true
|
|
*
|
|
* @returns boolean
|
|
*/
|
|
export function isMainActivity() {
|
|
if (Platform.OS === 'android') {
|
|
const MattermostShare = require('@mattermost/rnshare').default;
|
|
return MattermostShare?.getCurrentActivityName().includes('MainActivity');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function localeCompare(a: string, b: string) {
|
|
return a.localeCompare(b);
|
|
}
|
|
|
|
export function areBothStringArraysEqual(a: string[], b: string[]) {
|
|
if (a.length !== b.length) {
|
|
return false;
|
|
}
|
|
|
|
if (a.length === 0 && b.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
const aSorted = a.sort(localeCompare);
|
|
const bSorted = b.sort(localeCompare);
|
|
const areBothEqual = aSorted.every((value, index) => value === bSorted[index]);
|
|
|
|
return areBothEqual;
|
|
}
|
|
|
|
/**
|
|
* Efficiently compares two arrays to check if they have different elements.
|
|
* Uses O(n) complexity by comparing sets directly.
|
|
* @param oldArray - The original array
|
|
* @param newArray - The new array to compare against
|
|
* @returns true if arrays have different elements, false if they're the same
|
|
*/
|
|
export function hasArrayChanged(oldArray: string[], newArray: string[]): boolean {
|
|
if (oldArray.length !== newArray.length) {
|
|
return true;
|
|
}
|
|
|
|
const oldSet = new Set(oldArray);
|
|
const newSet = new Set(newArray);
|
|
|
|
// If sets have different sizes, arrays have different unique elements
|
|
if (oldSet.size !== newSet.size) {
|
|
return true;
|
|
}
|
|
|
|
// Check both directions: all elements in oldSet exist in newSet
|
|
// AND all elements in newSet exist in oldSet
|
|
for (const item of oldSet) {
|
|
if (!newSet.has(item)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
for (const item of newSet) {
|
|
if (!oldSet.has(item)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|