MOBILE-3320 config: BUILD and CONFIG constants

Generate constants from webpack script to support jsonc files and read environment config
main
Noel De Martin 2020-10-21 17:56:01 +02:00
parent 51c1e423fd
commit a1445dcf99
25 changed files with 207 additions and 109 deletions

1
.eslintignore 100644
View File

@ -0,0 +1 @@
*.js

View File

@ -1,4 +1,4 @@
var appConfig = {
const appConfig = {
env: {
browser: true,
es6: true,

2
.gitignore vendored
View File

@ -28,3 +28,5 @@ npm-debug.log*
/platforms
/plugins
/www
/config/config.*.json

8
.vscode/settings.json vendored 100644
View File

@ -0,0 +1,8 @@
{
"files.associations": {
"config.json": "jsonc",
"config.*.json": "jsonc",
},
}

View File

@ -46,12 +46,6 @@
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,

View File

@ -1,9 +1,20 @@
/**
* Application config.
*
* You can create your own environment files such as "config.prod.json" and "config.dev.json"
* to override some values. The values will be merged, so you don't need to duplicate everything
* in this file.
*/
{
"app_id": "com.moodle.moodlemobile",
"appname": "Moodle Mobile",
"desktopappname": "Moodle Desktop",
"versioncode": 3930,
// @todo This could be read from package.json.
"versionname": "3.9.3-dev",
"cache_update_frequency_usually": 420000,
"cache_update_frequency_often": 1200000,
"cache_update_frequency_sometimes": 3600000,

48
config/utils.js 100644
View File

@ -0,0 +1,48 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const { execSync } = require('child_process');
const { resolve } = require('path');
export function getConfig(environment) {
const { parse: parseJsonc } = require('jsonc-parser');
const { readFileSync, existsSync } = require('fs');
const envSuffixesMap = {
testing: ['test', 'testing'],
development: ['dev', 'development'],
production: ['prod', 'production'],
};
const config = parseJsonc(readFileSync(resolve('config/config.json')).toString());
const envSuffixes = (envSuffixesMap[environment] || []);
const envConfigPath = envSuffixes.map(suffix => resolve(`config/config.${suffix}.json`)).find(existsSync);
if (envConfigPath) {
const envConfig = parseJsonc(readFileSync(envConfigPath).toString());
for (const [key, value] of Object.entries(envConfig)) {
config[key] = value;
}
}
return config;
}
export function getBuild(environment) {
return {
environment,
isProduction: environment === 'production',
lastCommitHash: execSync('git log -1 --pretty=format:"%H"').toString(),
compilationTime: Date.now(),
};
}

View File

@ -12,10 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
const { webpack } = require('webpack');
const webpack = require('webpack');
const { getConfig, getBuild } = require('./utils');
const { resolve } = require('path');
module.exports = (config, options, targetOptions) => {
module.exports = config => {
config.resolve.alias['@'] = resolve('src');
config.resolve.alias['@addon'] = resolve('src/app/addon');
config.resolve.alias['@app'] = resolve('src/app');
@ -27,5 +28,14 @@ module.exports = (config, options, targetOptions) => {
config.resolve.alias['@services'] = resolve('src/app/services');
config.resolve.alias['@singletons'] = resolve('src/app/singletons');
config.plugins.push(
new webpack.DefinePlugin({
'window.MoodleApp': {
CONFIG: JSON.stringify(getConfig(process.env.NODE_ENV || 'development')),
BUILD: JSON.stringify(getBuild(process.env.NODE_ENV || 'development')),
},
}),
);
return config;
};

6
package-lock.json generated
View File

@ -12519,6 +12519,12 @@
"minimist": "^1.2.5"
}
},
"jsonc-parser": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz",
"integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==",
"dev": true
},
"jsonfile": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz",

View File

@ -144,6 +144,7 @@
"gulp-slash": "^1.1.3",
"jest": "^26.5.0",
"jest-preset-angular": "^8.3.1",
"jsonc-parser": "^2.3.1",
"ts-jest": "^26.4.1",
"ts-node": "~8.3.0",
"typescript": "~3.9.5"

View File

@ -26,7 +26,6 @@ import { CoreTimeUtils } from '@services/utils/time';
import { CoreUrlUtils, CoreUrlParams } from '@services/utils/url';
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { CoreConstants } from '@core/constants';
import CoreConfigConstants from '@app/config.json';
import { SQLiteDB } from '@classes/sqlitedb';
import { CoreError } from '@classes/errors/error';
import { CoreWSError } from '@classes/errors/wserror';
@ -73,10 +72,10 @@ export class CoreSite {
// Possible cache update frequencies.
protected readonly UPDATE_FREQUENCIES = [
CoreConfigConstants.cache_update_frequency_usually || 420000,
CoreConfigConstants.cache_update_frequency_often || 1200000,
CoreConfigConstants.cache_update_frequency_sometimes || 3600000,
CoreConfigConstants.cache_update_frequency_rarely || 43200000,
CoreConstants.CONFIG.cache_update_frequency_usually || 420000,
CoreConstants.CONFIG.cache_update_frequency_often || 1200000,
CoreConstants.CONFIG.cache_update_frequency_sometimes || 3600000,
CoreConstants.CONFIG.cache_update_frequency_rarely || 43200000,
];
// Rest of variables.
@ -227,9 +226,9 @@ export class CoreSite {
* @return Site name.
*/
getSiteName(): string {
if (CoreConfigConstants.sitename) {
if (CoreConstants.CONFIG.sitename) {
// Overridden by config.
return CoreConfigConstants.sitename;
return CoreConstants.CONFIG.sitename;
} else {
return this.infos?.sitename || '';
}
@ -1284,7 +1283,7 @@ export class CoreSite {
*/
async checkLocalMobilePlugin(retrying?: boolean): Promise<LocalMobileResponse> {
const checkUrl = this.siteUrl + '/local/mobile/check.php';
const service = CoreConfigConstants.wsextservice;
const service = CoreConstants.CONFIG.wsextservice;
if (!service) {
// External service not defined.

View File

@ -28,6 +28,7 @@ export const enum ContextLevel {
* Static class to contain all the core constants.
*/
export class CoreConstants {
/* eslint-disable max-len */
static readonly SECONDS_YEAR = 31536000;
@ -100,9 +101,14 @@ export class CoreConstants {
static readonly FEATURE_SHOW_DESCRIPTION = 'showdescription'; // True if module can show description on course main page.
static readonly FEATURE_USES_QUESTIONS = 'usesquestions'; // True if module uses the question bank.
// Possbile archetypes for modules.
// Possible archetypes for modules.
static readonly MOD_ARCHETYPE_OTHER = 0; // Unspecified module archetype.
static readonly MOD_ARCHETYPE_RESOURCE = 1; // Resource-like type module.
static readonly MOD_ARCHETYPE_ASSIGNMENT = 2; // Assignment module archetype.
static readonly MOD_ARCHETYPE_SYSTEM = 3; // System (not user-addable) module archetype.
// Config & environment constants.
static readonly CONFIG = (window as unknown as MoodleAppWindow).MoodleApp.CONFIG; // Data parsed from config.json files.
static readonly BUILD = (window as unknown as MoodleAppWindow).MoodleApp.BUILD; // Environment info.
}

View File

@ -20,7 +20,7 @@ import { CoreDB } from '@services/db';
import { CoreEvents, CoreEventsProvider } from '@services/events';
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb';
import CoreConfigConstants from '@app/config.json';
import { CoreConstants } from '@core/constants';
import { makeSingleton, Keyboard, Network, StatusBar, Platform } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
@ -645,21 +645,21 @@ export class CoreAppProvider {
* Set StatusBar color depending on platform.
*/
setStatusBarColor(): void {
if (typeof CoreConfigConstants.statusbarbgios == 'string' && this.isIOS()) {
if (typeof CoreConstants.CONFIG.statusbarbgios == 'string' && this.isIOS()) {
// IOS Status bar properties.
StatusBar.instance.overlaysWebView(false);
StatusBar.instance.backgroundColorByHexString(CoreConfigConstants.statusbarbgios);
CoreConfigConstants.statusbarlighttextios ? StatusBar.instance.styleLightContent() : StatusBar.instance.styleDefault();
} else if (typeof CoreConfigConstants.statusbarbgandroid == 'string' && this.isAndroid()) {
StatusBar.instance.backgroundColorByHexString(CoreConstants.CONFIG.statusbarbgios);
CoreConstants.CONFIG.statusbarlighttextios ? StatusBar.instance.styleLightContent() : StatusBar.instance.styleDefault();
} else if (typeof CoreConstants.CONFIG.statusbarbgandroid == 'string' && this.isAndroid()) {
// Android Status bar properties.
StatusBar.instance.backgroundColorByHexString(CoreConfigConstants.statusbarbgandroid);
CoreConfigConstants.statusbarlighttextandroid ?
StatusBar.instance.backgroundColorByHexString(CoreConstants.CONFIG.statusbarbgandroid);
CoreConstants.CONFIG.statusbarlighttextandroid ?
StatusBar.instance.styleLightContent() : StatusBar.instance.styleDefault();
} else if (typeof CoreConfigConstants.statusbarbg == 'string') {
} else if (typeof CoreConstants.CONFIG.statusbarbg == 'string') {
// Generic Status bar properties.
this.isIOS() && StatusBar.instance.overlaysWebView(false);
StatusBar.instance.backgroundColorByHexString(CoreConfigConstants.statusbarbg);
CoreConfigConstants.statusbarlighttext ? StatusBar.instance.styleLightContent() : StatusBar.instance.styleDefault();
StatusBar.instance.backgroundColorByHexString(CoreConstants.CONFIG.statusbarbg);
CoreConstants.CONFIG.statusbarlighttext ? StatusBar.instance.styleLightContent() : StatusBar.instance.styleDefault();
} else {
// Default Status bar properties.
this.isAndroid() ? StatusBar.instance.styleLightContent() : StatusBar.instance.styleDefault();
@ -670,14 +670,14 @@ export class CoreAppProvider {
* Reset StatusBar color if any was set.
*/
resetStatusBarColor(): void {
if (typeof CoreConfigConstants.statusbarbgremotetheme == 'string' &&
((typeof CoreConfigConstants.statusbarbgios == 'string' && this.isIOS()) ||
(typeof CoreConfigConstants.statusbarbgandroid == 'string' && this.isAndroid()) ||
typeof CoreConfigConstants.statusbarbg == 'string')) {
if (typeof CoreConstants.CONFIG.statusbarbgremotetheme == 'string' &&
((typeof CoreConstants.CONFIG.statusbarbgios == 'string' && this.isIOS()) ||
(typeof CoreConstants.CONFIG.statusbarbgandroid == 'string' && this.isAndroid()) ||
typeof CoreConstants.CONFIG.statusbarbg == 'string')) {
// If the status bar has been overriden and there's a fallback color for remote themes, use it now.
this.isIOS() && StatusBar.instance.overlaysWebView(false);
StatusBar.instance.backgroundColorByHexString(CoreConfigConstants.statusbarbgremotetheme);
CoreConfigConstants.statusbarlighttextremotetheme ?
StatusBar.instance.backgroundColorByHexString(CoreConstants.CONFIG.statusbarbgremotetheme);
CoreConstants.CONFIG.statusbarlighttextremotetheme ?
StatusBar.instance.styleLightContent() : StatusBar.instance.styleDefault();
}
}

View File

@ -21,7 +21,7 @@ import { CoreWSExternalFile } from '@services/ws';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import CoreConfigConstants from '@app/config.json';
import { CoreConstants } from '@core/constants';
import { CoreError } from '@classes/errors/error';
import { CoreLogger } from '@singletons/logger';
@ -1227,7 +1227,7 @@ export class CoreFileProvider {
return src;
}
return src.replace(CoreConfigConstants.ioswebviewscheme + '://localhost/_app_file_', 'file://');
return src.replace(CoreConstants.CONFIG.ioswebviewscheme + '://localhost/_app_file_', 'file://');
}
/**

View File

@ -14,7 +14,7 @@
import { Injectable } from '@angular/core';
import CoreConfigConstants from '@app/config.json';
import { CoreConstants } from '@core/constants';
import { LangChangeEvent } from '@ngx-translate/core';
import { CoreAppProvider } from '@services/app';
import { CoreConfig } from '@services/config';
@ -29,7 +29,7 @@ import * as moment from 'moment';
export class CoreLangProvider {
protected fallbackLanguage = 'en'; // Always use English as fallback language since it contains all strings.
protected defaultLanguage = CoreConfigConstants.default_lang || 'en'; // Lang to use if device lang not valid or is forced.
protected defaultLanguage = CoreConstants.CONFIG.default_lang || 'en'; // Lang to use if device lang not valid or is forced.
protected currentLanguage?: string; // Save current language in a variable to speed up the get function.
protected customStrings: CoreLanguageObject = {}; // Strings defined using the admin tool.
protected customStringsRaw?: string;
@ -252,21 +252,21 @@ export class CoreLangProvider {
}
// User hasn't defined a language. If default language is forced, use it.
if (CoreConfigConstants.default_lang && CoreConfigConstants.forcedefaultlanguage) {
return CoreConfigConstants.default_lang;
if (CoreConstants.CONFIG.default_lang && CoreConstants.CONFIG.forcedefaultlanguage) {
return CoreConstants.CONFIG.default_lang;
}
// No forced language, try to get current language from browser.
let preferredLanguage = navigator.language.toLowerCase();
if (preferredLanguage.indexOf('-') > -1) {
// Language code defined by locale has a dash, like en-US or es-ES. Check if it's supported.
if (CoreConfigConstants.languages && typeof CoreConfigConstants.languages[preferredLanguage] == 'undefined') {
if (CoreConstants.CONFIG.languages && typeof CoreConstants.CONFIG.languages[preferredLanguage] == 'undefined') {
// Code is NOT supported. Fallback to language without dash. E.g. 'en-US' would fallback to 'en'.
preferredLanguage = preferredLanguage.substr(0, preferredLanguage.indexOf('-'));
}
}
if (typeof CoreConfigConstants.languages[preferredLanguage] == 'undefined') {
if (typeof CoreConstants.CONFIG.languages[preferredLanguage] == 'undefined') {
// Language not supported, use default language.
return this.defaultLanguage;
}

View File

@ -26,7 +26,6 @@ import { CoreSite } from '@classes/site';
import { CoreQueueRunner } from '@classes/queue-runner';
import { CoreError } from '@classes/errors/error';
import { CoreConstants } from '@core/constants';
import CoreConfigConstants from '@app/config.json';
import { makeSingleton, NgZone, Platform, Translate, LocalNotifications, Push, Device } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
@ -611,7 +610,7 @@ export class CoreLocalNotificationsProvider {
if (CoreApp.instance.isAndroid()) {
notification.icon = notification.icon || 'res://icon';
notification.smallIcon = notification.smallIcon || 'res://smallicon';
notification.color = notification.color || CoreConfigConstants.notificoncolor;
notification.color = notification.color || CoreConstants.CONFIG.notificoncolor;
if (notification.led !== false) {
let ledColor = 'FF9900';

View File

@ -24,7 +24,6 @@ import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
import { CoreConstants } from '@core/constants';
import CoreConfigConstants from '@app/config.json';
import {
CoreSite,
CoreSiteWSPreSets,
@ -278,7 +277,7 @@ export class CoreSitesProvider {
* @return Site data if it's a demo site, undefined otherwise.
*/
getDemoSiteData(name: string): CoreSitesDemoSiteData | undefined {
const demoSites = CoreConfigConstants.demo_sites;
const demoSites = CoreConstants.CONFIG.demo_sites;
name = name.toLowerCase();
if (typeof demoSites != 'undefined' && typeof demoSites[name] != 'undefined') {
@ -396,7 +395,7 @@ export class CoreSitesProvider {
});
}
data.service = data.service || CoreConfigConstants.wsservice;
data.service = data.service || CoreConstants.CONFIG.wsservice;
this.services[siteUrl] = data.service; // No need to store it in DB.
if (data.coreSupported || (data.code != CoreConstants.LOGIN_SSO_CODE && data.code != CoreConstants.LOGIN_SSO_INAPP_CODE)) {
@ -746,7 +745,7 @@ export class CoreSitesProvider {
}
// Return default service.
return CoreConfigConstants.wsservice;
return CoreConstants.CONFIG.wsservice;
}
/**
@ -883,7 +882,7 @@ export class CoreSitesProvider {
}
const requiredVersion = this.convertVersionName(config.tool_mobile_minimumversion);
const appVersion = this.convertVersionName(CoreConfigConstants.versionname);
const appVersion = this.convertVersionName(CoreConstants.CONFIG.versionname);
if (requiredVersion > appVersion) {
const storesConfig: CoreStoreConfig = {
@ -1215,7 +1214,7 @@ export class CoreSitesProvider {
id: site.id,
siteUrl: site.siteUrl,
fullName: siteInfo?.fullname,
siteName: CoreConfigConstants.sitename ?? siteInfo?.sitename,
siteName: CoreConstants.CONFIG.sitename ?? siteInfo?.sitename,
avatar: siteInfo?.userpictureurl,
siteHomeId: siteInfo?.siteid || 1,
};

View File

@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreConfig } from '@services/config';
import { CoreInitHandler, CoreInitDelegate } from '@services/init';
import CoreConfigConstants from '@app/config.json';
import { CoreConstants } from '@core/constants';
import { makeSingleton } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
@ -49,7 +49,7 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
*/
async load(): Promise<void> {
const promises = [];
const versionCode = CoreConfigConstants.versioncode;
const versionCode = CoreConstants.CONFIG.versioncode;
const versionApplied = await CoreConfig.instance.get<number>(VERSION_APPLIED, 0);

View File

@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreLang } from '@services/lang';
import { CoreTextUtils } from '@services/utils/text';
import CoreConfigConstants from '@app/config.json';
import { CoreConstants } from '@/app/core/constants';
import { makeSingleton } from '@singletons/core.singletons';
import { CoreUrl } from '@singletons/url';
@ -442,7 +442,7 @@ export class CoreUrlUtilsProvider {
return scheme == 'cdvfile' ||
scheme == 'file' ||
scheme == 'filesystem' ||
scheme == CoreConfigConstants.ioswebviewscheme;
scheme == CoreConstants.CONFIG.ioswebviewscheme;
}
/**

View File

@ -13,8 +13,8 @@
// limitations under the License.
import moment from 'moment';
import { environment } from '@/environments/environment';
import { CoreConstants } from '@core/constants';
/**
* Log function type.
@ -57,7 +57,7 @@ export class CoreLogger {
*/
static getInstance(className: string): CoreLogger {
// Disable log on production.
if (environment.production) {
if (CoreConstants.BUILD.isProduction) {
// eslint-disable-next-line no-console
console.warn('Log is disabled in production app');

View File

@ -1,17 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export const environment = {
production: true,
};

View File

@ -1,30 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

View File

@ -16,9 +16,9 @@ import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { CoreConstants } from './app/core/constants';
if (environment.production) {
if (CoreConstants.BUILD.isProduction) {
enableProdMode();
}

View File

@ -12,4 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
/* eslint-disable @typescript-eslint/naming-convention */
import 'jest-preset-angular';
import { getConfig, getBuild } from '../../config/utils';
(window as unknown as MoodleAppWindow).MoodleApp = {
CONFIG: getConfig('testing'),
BUILD: getBuild('testing'),
};

60
src/types/global.d.ts vendored
View File

@ -12,17 +12,69 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import '';
/* eslint-disable @typescript-eslint/naming-convention */
import { CoreSitesDemoSiteData } from '@/app/services/sites';
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/naming-convention
__Zone_disable_customElements: boolean;
}
type MoodleAppWindow = {
MoodleApp: {
CONFIG: {
app_id: string;
appname: string;
desktopappname: string;
versioncode: number;
versionname: string;
cache_update_frequency_usually: number;
cache_update_frequency_often: number;
cache_update_frequency_sometimes: number;
cache_update_frequency_rarely: number;
default_lang: string;
languages: Record<string, string>;
wsservice: string;
wsextservice: string;
demo_sites: Record<string, Record<string, CoreSitesDemoSiteData>>;
font_sizes: number[];
customurlscheme: string;
siteurl: string;
sitename: string;
multisitesdisplay: string;
sitefindersettings: Record<string, unknown>;
onlyallowlistedsites: boolean;
skipssoconfirmation: boolean;
forcedefaultlanguage: boolean;
privacypolicy: string;
notificoncolor: string;
statusbarbg: boolean;
statusbarlighttext: boolean;
statusbarbgios: string;
statusbarlighttextios: boolean;
statusbarbgandroid: string;
statusbarlighttextandroid: boolean;
statusbarbgremotetheme: string;
statusbarlighttextremotetheme: boolean;
enableanalytics: boolean;
enableonboarding: boolean;
forceColorScheme: string;
forceLoginLogo: boolean;
ioswebviewscheme: string;
appstores: Record<string, string>;
};
BUILD: {
environment: string;
isProduction: boolean;
lastCommitHash: string;
compilationTime: number;
};
};
};
}
/**