UNPKG

16.9 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.validate = exports.validateStorageGroup = exports.validateDefaultLanguage = exports.validateAppUUID = exports.validateSupportedLocales = exports.validateLocaleDisplayNames = exports.validateLocaleDisplayName = exports.validateBuildTarget = exports.validateRequestedPermissions = exports.validateWipeColor = exports.validateProjectDisplayName = exports.validateDisplayName = exports.validateAppType = exports.normalizeProjectConfig = exports.normalizeLocales = exports.getAllPermissionTypes = exports.Permission = exports.Locales = exports.MAX_DISPLAY_NAME_LENGTH = exports.MAX_LENGTH_APP_CLUSTER_ID = exports.VALID_APP_TYPES = exports.AppType = void 0;
4const tslib_1 = require("tslib");
5const humanize_list_1 = tslib_1.__importDefault(require("humanize-list"));
6const validator_1 = tslib_1.__importDefault(require("validator"));
7const lodash_1 = tslib_1.__importDefault(require("lodash"));
8const semver_1 = tslib_1.__importDefault(require("semver"));
9const buildTargets_1 = tslib_1.__importDefault(require("./buildTargets"));
10const DiagnosticList_1 = tslib_1.__importDefault(require("./DiagnosticList"));
11const languageTag_1 = require("./languageTag");
12const sdkVersion_1 = tslib_1.__importDefault(require("./sdkVersion"));
13const knownBuildTargets = Object.keys(buildTargets_1.default);
14var AppType;
15(function (AppType) {
16 AppType["APP"] = "app";
17 AppType["CLOCKFACE"] = "clockface";
18})(AppType = exports.AppType || (exports.AppType = {}));
19exports.VALID_APP_TYPES = Object.values(AppType);
20exports.MAX_LENGTH_APP_CLUSTER_ID = 64;
21exports.MAX_DISPLAY_NAME_LENGTH = 30;
22var Locales;
23(function (Locales) {
24 Locales["en-US"] = "English (US)";
25 Locales["de-DE"] = "German";
26 Locales["es-ES"] = "Spanish";
27 Locales["fr-FR"] = "French";
28 Locales["it-IT"] = "Italian";
29 Locales["ja-JP"] = "Japanese";
30 Locales["ko-KR"] = "Korean";
31 Locales["nl-NL"] = "Dutch";
32 Locales["sv-SE"] = "Swedish";
33 Locales["zh-CN"] = "Chinese (Simplified)";
34 Locales["zh-TW"] = "Chinese (Traditional)";
35 Locales["pt-BR"] = "Portuguese (Brazillian)";
36 Locales["id-ID"] = "Indonesian (Bahasa)";
37 Locales["ro-RO"] = "Romanian";
38 Locales["ru-RU"] = "Russian";
39 Locales["pl-PL"] = "Polish";
40 Locales["cs-CZ"] = "Czech";
41})(Locales = exports.Locales || (exports.Locales = {}));
42const languageTags = Object.keys(Locales);
43var Permission;
44(function (Permission) {
45 Permission["ACCESS_ACTIVITY"] = "access_activity";
46 Permission["ACCESS_AOD"] = "access_aod";
47 Permission["ACCESS_APP_CLUSTER_STORAGE"] = "access_app_cluster_storage";
48 Permission["ACCESS_CALENDAR"] = "access_calendar";
49 Permission["ACCESS_EXERCISE"] = "access_exercise";
50 Permission["ACCESS_HEART_RATE"] = "access_heart_rate";
51 Permission["ACCESS_INTERNET"] = "access_internet";
52 Permission["ACCESS_LOCATION"] = "access_location";
53 Permission["ACCESS_SECURE_EXCHANGE"] = "access_secure_exchange";
54 Permission["ACCESS_USER_PROFILE"] = "access_user_profile";
55 Permission["FITBIT_TOKEN"] = "fitbit_token";
56 Permission["RUN_BACKGROUND"] = "run_background";
57 Permission["EXTERNAL_APP_COMMUNICATION"] = "external_app_communication";
58 Permission["MOBILE_NOTIFICATIONS"] = "mobile_notifications";
59})(Permission = exports.Permission || (exports.Permission = {}));
60const permissionTypes = [
61 {
62 key: Permission.ACCESS_ACTIVITY,
63 name: 'Activity',
64 description: 'Read user activities for today (distance, calories, steps, elevation and active minutes), and daily goals',
65 },
66 {
67 key: Permission.ACCESS_USER_PROFILE,
68 name: 'User Profile',
69 description: 'Read non-identifiable personal information (gender, age, height, weight, resting HR, basal metabolic rate, stride, HR zones)',
70 },
71 {
72 key: Permission.ACCESS_HEART_RATE,
73 name: 'Heart Rate',
74 description: 'Application may read the heart-rate sensor in real-time',
75 },
76 {
77 key: Permission.ACCESS_LOCATION,
78 name: 'Location',
79 description: 'Application and companion may use GPS',
80 },
81 {
82 key: Permission.ACCESS_INTERNET,
83 name: 'Internet',
84 description: 'Companion may communicate with the Internet using your phone data connection',
85 },
86 {
87 key: Permission.RUN_BACKGROUND,
88 name: 'Run in background',
89 description: 'Companion may run even when the application is not actively in use',
90 },
91 {
92 key: Permission.ACCESS_EXERCISE,
93 name: 'Exercise Tracking',
94 description: 'Application may track an exercise',
95 sdkVersion: '>=3.0.0',
96 },
97 {
98 key: Permission.ACCESS_APP_CLUSTER_STORAGE,
99 name: 'App Cluster Storage',
100 description: 'Application may access storage shared by other applications from the same developer',
101 sdkVersion: '>=4.0.0',
102 },
103 {
104 key: Permission.ACCESS_CALENDAR,
105 name: 'Calendars',
106 description: 'Application may access calendar data stored on the mobile device',
107 sdkVersion: '>=4.1.0',
108 },
109];
110const restrictedPermissionTypes = [
111 {
112 key: Permission.FITBIT_TOKEN,
113 name: '[Restricted] Fitbit Token',
114 description: 'Access Fitbit API token',
115 },
116 {
117 key: Permission.EXTERNAL_APP_COMMUNICATION,
118 name: '[Restricted] External Application Communication',
119 description: 'Allows communication between external mobile applications and companion',
120 },
121 {
122 key: Permission.ACCESS_SECURE_EXCHANGE,
123 name: '[Restricted] Secure Exchange',
124 description: 'Allows securing any data and verifying that data was secured',
125 },
126 {
127 key: Permission.ACCESS_AOD,
128 name: '[Restricted] Always-on Display',
129 description: 'Application may stay active whilst always-on display mode is active',
130 sdkVersion: '>=4.1.0',
131 },
132 {
133 key: Permission.MOBILE_NOTIFICATIONS,
134 name: '[Restricted] Mobile Notifications',
135 description: 'Application may display notifications on the mobile device',
136 sdkVersion: '>=4.1.0',
137 },
138];
139function getAllPermissionTypes(options) {
140 const { enableProposedAPI, includeRestrictedPermissions } = Object.assign({ enableProposedAPI: false, includeRestrictedPermissions: true }, options);
141 return [
142 ...permissionTypes,
143 ...(includeRestrictedPermissions ? restrictedPermissionTypes : []),
144 ].filter((permission) => !permission.sdkVersion ||
145 semver_1.default.satisfies(sdkVersion_1.default(), permission.sdkVersion) ||
146 enableProposedAPI);
147}
148exports.getAllPermissionTypes = getAllPermissionTypes;
149function constrainedSetDiagnostics({ actualValues, knownValues, valueTypeNoun, notFoundIsFatal = false, }) {
150 const unknownValues = lodash_1.default.without(actualValues, ...knownValues);
151 const diagnostics = new DiagnosticList_1.default();
152 if (unknownValues.length > 0) {
153 const unknownValueStrings = unknownValues.filter(lodash_1.default.isString);
154 const unknownValueOther = lodash_1.default
155 .without(unknownValues, ...unknownValueStrings)
156 .map(String);
157 if (unknownValueStrings.length) {
158 const errStr = `One or more ${valueTypeNoun} was invalid: ${unknownValueStrings.join(', ')}`;
159 if (notFoundIsFatal)
160 diagnostics.pushFatalError(errStr);
161 else
162 diagnostics.pushWarning(errStr);
163 }
164 if (unknownValueOther.length) {
165 diagnostics.pushFatalError(`One or more ${valueTypeNoun} was not a string: ${unknownValueOther.join(', ')}`);
166 }
167 }
168 const duplicatedValues = lodash_1.default
169 .uniq(actualValues)
170 .filter((value) => actualValues.indexOf(value) !== actualValues.lastIndexOf(value));
171 if (duplicatedValues.length > 0) {
172 diagnostics.pushWarning(`One or more ${valueTypeNoun} was specified multiple times: ${duplicatedValues.join(', ')}`);
173 }
174 return diagnostics;
175}
176function normalizeLanguageTag(languageTag) {
177 const match = /^([a-z]{2})(-[a-z]{2})?$/i.exec(languageTag);
178 if (match === null)
179 return languageTag;
180 const [, language, region] = match;
181 return language.toLowerCase() + (region || '').toUpperCase();
182}
183function normalizeLocales(locales) {
184 const localeMapping = lodash_1.default.mapKeys(Object.keys(Locales), (tag) => tag.split('-')[0]);
185 const normalizedLocales = {};
186 for (const [locale, localeConfig] of Object.entries(locales)) {
187 const mappedLocale = localeMapping[locale];
188 if (mappedLocale === undefined) {
189 normalizedLocales[normalizeLanguageTag(locale)] = localeConfig;
190 continue;
191 }
192 if (locales[mappedLocale] !== undefined) {
193 continue;
194 }
195 normalizedLocales[mappedLocale] = localeConfig;
196 }
197 return normalizedLocales;
198}
199exports.normalizeLocales = normalizeLocales;
200function normalizeProjectConfig(config, defaults) {
201 if (!lodash_1.default.isPlainObject(config)) {
202 throw new TypeError('Project configuration root must be an object');
203 }
204 const mergedConfig = Object.assign(Object.assign({ appUUID: '', appType: AppType.APP, appDisplayName: '', iconFile: 'resources/icon.png', wipeColor: '', requestedPermissions: [], buildTargets: [], i18n: {}, defaultLanguage: 'en-US' }, defaults), config.fitbit);
205 const { requestedPermissions } = mergedConfig;
206 if (!Array.isArray(requestedPermissions)) {
207 throw new TypeError(`fitbit.requestedPermissions must be an array, not ${typeof requestedPermissions}`);
208 }
209 mergedConfig.i18n = normalizeLocales(mergedConfig.i18n);
210 return mergedConfig;
211}
212exports.normalizeProjectConfig = normalizeProjectConfig;
213function validateAppType(config) {
214 const diagnostics = new DiagnosticList_1.default();
215 if (exports.VALID_APP_TYPES.indexOf(config.appType) === -1) {
216 const appTypeNames = humanize_list_1.default(exports.VALID_APP_TYPES, { conjunction: 'or' });
217 diagnostics.pushFatalError(`App type '${config.appType}' is invalid, expected ${appTypeNames}`);
218 }
219 return diagnostics;
220}
221exports.validateAppType = validateAppType;
222function validateDisplayName(name) {
223 if (name.length === 0) {
224 return 'Display name must not be blank';
225 }
226 if (name.length > exports.MAX_DISPLAY_NAME_LENGTH) {
227 return `Display name must not exceed ${exports.MAX_DISPLAY_NAME_LENGTH} characters`;
228 }
229 return true;
230}
231exports.validateDisplayName = validateDisplayName;
232function validateProjectDisplayName(config) {
233 const diagnostics = new DiagnosticList_1.default();
234 const result = validateDisplayName(config.appDisplayName);
235 if (result !== true) {
236 diagnostics.pushFatalError(result);
237 }
238 return diagnostics;
239}
240exports.validateProjectDisplayName = validateProjectDisplayName;
241function validateWipeColor(config) {
242 const diagnostics = new DiagnosticList_1.default();
243 if (config.appType !== AppType.CLOCKFACE &&
244 !validator_1.default.isHexColor(config.wipeColor)) {
245 diagnostics.pushFatalError('Wipe color must be a valid hex color');
246 }
247 return diagnostics;
248}
249exports.validateWipeColor = validateWipeColor;
250function validateRequestedPermissions({ enableProposedAPI, requestedPermissions, }) {
251 return constrainedSetDiagnostics({
252 actualValues: requestedPermissions,
253 knownValues: getAllPermissionTypes({
254 enableProposedAPI: !!enableProposedAPI,
255 }).map((permission) => permission.key),
256 valueTypeNoun: 'requested permissions',
257 notFoundIsFatal: false,
258 });
259}
260exports.validateRequestedPermissions = validateRequestedPermissions;
261function validateBuildTarget({ buildTargets }, { hasNativeComponents }) {
262 const diagnostics = constrainedSetDiagnostics({
263 actualValues: buildTargets,
264 knownValues: knownBuildTargets,
265 valueTypeNoun: 'build targets',
266 notFoundIsFatal: true,
267 });
268 if ((buildTargets === undefined || buildTargets.length === 0) &&
269 !hasNativeComponents) {
270 diagnostics.pushFatalError('At least one build target must be enabled');
271 }
272 return diagnostics;
273}
274exports.validateBuildTarget = validateBuildTarget;
275function validateLocaleDisplayName({ i18n }, localeKey) {
276 const diagnostics = new DiagnosticList_1.default();
277 const locale = i18n[localeKey];
278 if (!locale)
279 return diagnostics;
280 if (!locale.name || locale.name.length === 0) {
281 diagnostics.pushFatalError(`Localized display name for ${Locales[localeKey]} must not be blank`);
282 }
283 if (locale.name.length > exports.MAX_DISPLAY_NAME_LENGTH) {
284 diagnostics.pushFatalError(`Localized display name for ${Locales[localeKey]} must not exceed ${exports.MAX_DISPLAY_NAME_LENGTH} characters`);
285 }
286 return diagnostics;
287}
288exports.validateLocaleDisplayName = validateLocaleDisplayName;
289function validateLocaleDisplayNames(config) {
290 const diagnostics = new DiagnosticList_1.default();
291 for (const localeKey of Object.keys(Locales)) {
292 diagnostics.extend(validateLocaleDisplayName(config, localeKey));
293 }
294 return diagnostics;
295}
296exports.validateLocaleDisplayNames = validateLocaleDisplayNames;
297function validateSupportedLocales({ i18n }) {
298 const diagnostics = new DiagnosticList_1.default();
299 const unknownLocales = lodash_1.default.without(Object.keys(i18n), ...Object.keys(Locales));
300 if (unknownLocales.length > 0) {
301 diagnostics.pushWarning(`Invalid locales: ${unknownLocales.join(', ')}`);
302 }
303 return diagnostics;
304}
305exports.validateSupportedLocales = validateSupportedLocales;
306function validateAppUUID({ appUUID }) {
307 const diagnostics = new DiagnosticList_1.default();
308 if (!validator_1.default.isUUID(String(appUUID))) {
309 diagnostics.pushFatalError('appUUID must be a valid UUID, run "npx fitbit-build generate-appid" to fix');
310 }
311 return diagnostics;
312}
313exports.validateAppUUID = validateAppUUID;
314function validateDefaultLanguage(config) {
315 const diagnostics = new DiagnosticList_1.default();
316 if (!languageTag_1.validateLanguageTag(config.defaultLanguage)) {
317 diagnostics.pushFatalError(`Default language is an invalid language tag: ${config.defaultLanguage}. Must be ${humanize_list_1.default(languageTags, { conjunction: 'or' })}.`);
318 }
319 return diagnostics;
320}
321exports.validateDefaultLanguage = validateDefaultLanguage;
322function validateStorageGroup(config) {
323 const diagnostics = new DiagnosticList_1.default();
324 const hasRequestedPermission = getAllPermissionTypes({
325 enableProposedAPI: !!config.enableProposedAPI,
326 })
327 .map((permission) => permission.key)
328 .filter((permission) => (config.requestedPermissions || []).includes(permission))
329 .includes(Permission.ACCESS_APP_CLUSTER_STORAGE);
330 if (hasRequestedPermission) {
331 if (config.appClusterID === undefined) {
332 diagnostics.pushFatalError('App Cluster ID must be set when the App Cluster Storage permission is requested');
333 }
334 else if (config.appClusterID.length < 1 ||
335 config.appClusterID.length > exports.MAX_LENGTH_APP_CLUSTER_ID) {
336 diagnostics.pushFatalError('App Cluster ID must be between 1-64 characters');
337 }
338 else if (!/^([a-z0-9]+)(\.[a-z0-9]+)*$/.test(config.appClusterID)) {
339 diagnostics.pushFatalError('App Cluster ID may only contain alphanumeric characters separated by periods, eg: my.app.123');
340 }
341 if (config.developerID === undefined) {
342 diagnostics.pushFatalError('Developer ID must be set when the App Cluster Storage permission is requested');
343 }
344 else if (!validator_1.default.isUUID(String(config.developerID))) {
345 diagnostics.pushFatalError('Developer ID must be a valid UUID');
346 }
347 }
348 else if (config.appClusterID !== undefined ||
349 config.developerID !== undefined) {
350 diagnostics.pushFatalError('App Cluster Storage permission must be requested to set App Cluster ID and Developer ID fields');
351 }
352 return diagnostics;
353}
354exports.validateStorageGroup = validateStorageGroup;
355function validate(config, options) {
356 const { hasNativeComponents } = Object.assign({ hasNativeComponents: false }, options);
357 const diagnostics = new DiagnosticList_1.default();
358 [
359 validateAppUUID,
360 validateProjectDisplayName,
361 validateAppType,
362 validateWipeColor,
363 validateRequestedPermissions,
364 validateSupportedLocales,
365 validateLocaleDisplayNames,
366 validateDefaultLanguage,
367 validateStorageGroup,
368 ].forEach((validator) => diagnostics.extend(validator(config)));
369 diagnostics.extend(validateBuildTarget(config, { hasNativeComponents }));
370 return diagnostics;
371}
372exports.validate = validate;