import isRegex from 'is-regex'; import semver from 'semver'; import * as yup from 'yup'; import { DesktopifyApp2 } from './desktopify2'; import { DesktopAppPlugin } from './plugin'; export const appTitleValidation = yup .string() .label('App title') .max(26) .matches( /^[\w\-\s]+$/, 'App title may contain letters, numbers, spaces and dashes only', ) .matches(/^[^\s]+(\s+[^\s]+)*$/, 'App title may not begin or end with space') .required(); function isNumeric(str: string) { if (typeof str !== 'string') return false; return !Number.isNaN(Number(str)) && !Number.isNaN(parseFloat(str)); } function isNumericSemver(value: string) { // check if valid semver if (!semver.valid(value)) return false; // ensure that all values are numbers if (!value.split('.').every((num) => isNumeric(num))) return false; return true; } export const forceVersionValidation = yup .string() .label('App version') .required() // can have multiple keys included - https://www.npmjs.com/package/yup#schemawhenkeys-string--string-builder-object--values-any-schema--schema-schema .when('$currentVersion', ([currentVersion], schema) => schema.test( 'semantic-version', 'App version must follow a numeric SemVer versioning scheme (e.g. 1.0.0) and be greater than prior version.', (forceVersion: string) => { return ( isNumericSemver(forceVersion) && semver.gt(forceVersion, currentVersion) ); }, ), ); export const iconValidation = yup .string() .label('Icon') .url() .required('You must upload an icon for your app'); export const urlValidation = yup .string() .matches( /^(?:([a-z0-9+.-]+):\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/, { excludeEmptyString: true, message: "Invalid URL. Don't forget to include the protocol (e.g.. https://).", }, ) .label('Website URL') .required(); export const urlValidationForm = yup.object().shape({ url: urlValidation, }); export const heightValidation = yup .number() .label('Height') .moreThan(1) .lessThan(2000) .required(); export const widthValidation = yup .number() .label('Width') .moreThan(1) .lessThan(3000) .required(); export const internalAppRegexValidation = yup .string() .required() .label('Internal URLs') .test( 'escape-forward-slash', "a forward-slash ('/') must be escaped with a back-slash ('\\')", (value: string) => { if (value.includes('/')) { const forwardSlashCount = value .split('') .map((char) => char === '/') .filter((isTrue) => isTrue).length; const backSlashEscapeCount = ((value || '').match(/(?=\\\/)/g) || []) .length; return forwardSlashCount === backSlashEscapeCount; } return true; }, ) .test( 'is-regex', // eslint-disable-next-line no-template-curly-in-string '${path} must be valid regular expression', (value: string) => isRegex(RegExp(value)), ); export const appProtocolValidation = yup .string() .required() .label('App Protocol') .test( 'ends-with-protocol', 'Protocols must end with `://`', (value: string) => value.endsWith('://'), ) .test( 'is-lowercase', 'Protocols must be lowercase', (value) => value === value.toLowerCase(), ) .matches( /^[a-zA-Z-.]+:\/\/$/, 'Protocols contain letters, dots (.) and dashes (-) only', ) .min(5); export const appConfigValidation = yup.object({ customUserAgent: yup.string().required(), disableDevTools: yup.boolean().required(), iconUrl: iconValidation, id: yup.string().required(), internalURLs: yup.string().required(), isFrameBlocked: yup.boolean().required(), meta: yup .object({ appIterations: yup.number().required(), hasAppChanged: yup.boolean().required(), publishedVersions: yup.object({ desktopify: yup.string().notRequired(), electron: yup.string().notRequired(), version: yup.string().notRequired(), }), schemaVersion: yup.number().required(), }) .required(), name: appTitleValidation, secret: yup.string().required(), singleInstance: yup.boolean().required(), url: urlValidation, windowOptions: yup .object({ hasMaxHeight: yup.boolean().required(), hasMaxWidth: yup.boolean().required(), hasMinHeight: yup.boolean().required(), hasMinWidth: yup.boolean().required(), height: yup.number().required(), isFullscreenable: yup.boolean().required(), isMaximizable: yup.boolean().required(), isMinimizable: yup.boolean().required(), isResizable: yup.boolean().required(), maxHeight: yup.number().required(), maxWidth: yup.number().required(), minHeight: yup.number().required(), minWidth: yup.number().required(), startInFullscreenMode: yup.boolean().required(), width: yup.number().required(), alwaysOnTop: yup.boolean().required(), autoHideMenuBar: yup.boolean().required(), transparentInsetTitlebar: yup.boolean().required(), transparentTitlebar: yup.boolean().required(), }) .required(), }); export const shouldMinimizeToTrayIsActive = ( desktopApp: DesktopifyApp2, ) => { return desktopApp.trays.some( (t) => t.leftClick.role === 'toggleMenu' || t.rightClick.role === 'toggleMenu', ); };