diff --git a/eslint.config.mjs b/eslint.config.mjs
index ce9836a..5998ce0 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -4,6 +4,8 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
+import tseslint from '@typescript-eslint/eslint-plugin';
+import tsparser from '@typescript-eslint/parser';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -17,7 +19,8 @@ export default [
...compat.extends(
'eslint:recommended',
'plugin:react/recommended',
- 'plugin:jasmine/recommended'
+ 'plugin:jasmine/recommended',
+ 'plugin:@typescript-eslint/recommended'
),
{
ignores: [
@@ -27,9 +30,10 @@ export default [
],
},
{
- files: ['**/*.{js,jsx,mjs}'],
+ files: ['**/*.{js,jsx,mjs,ts,tsx}'],
plugins: {
jasmine,
+ '@typescript-eslint': tseslint,
},
settings: {
@@ -39,6 +43,7 @@ export default [
},
languageOptions: {
+ parser: tsparser,
globals: {
...globals.browser,
...globals.jasmine,
@@ -47,10 +52,8 @@ export default [
SharedArrayBuffer: 'readonly',
React: 'readonly',
},
-
ecmaVersion: 'latest',
sourceType: 'module',
-
parserOptions: {
ecmaFeatures: {
jsx: true,
@@ -60,6 +63,8 @@ export default [
rules: {
'react/prop-types': 'off',
+ '@typescript-eslint/explicit-function-return-type': 'off',
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
},
}
];
diff --git a/package.json b/package.json
index 4501751..97f931e 100644
--- a/package.json
+++ b/package.json
@@ -12,13 +12,19 @@
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/preset-react": "^7.25.9",
+ "@babel/preset-typescript": "^7.23.3",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
+ "@types/chrome": "^0.0.260",
+ "@types/react": "^18.2.64",
+ "@types/react-dom": "^18.2.21",
+ "@typescript-eslint/eslint-plugin": "^7.1.1",
+ "@typescript-eslint/parser": "^7.1.1",
"babel-loader": "^9.2.1",
"classnames": "^2.5.1",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
- "eslint": "^9.14.0",
+ "eslint": "^8.56.0",
"eslint-plugin-jasmine": "^4.2.2",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.12.0",
@@ -32,6 +38,7 @@
"sass": "^1.81.0",
"sass-loader": "^16.0.3",
"style-loader": "^0.23.1",
+ "typescript": "^5.3.3",
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4",
"webpack-merge": "^6.0.1"
diff --git a/src/assets/js/components/popup-spinner.jsx b/src/assets/js/components/popup-spinner.tsx
similarity index 87%
rename from src/assets/js/components/popup-spinner.jsx
rename to src/assets/js/components/popup-spinner.tsx
index d8461f4..c4243c5 100644
--- a/src/assets/js/components/popup-spinner.jsx
+++ b/src/assets/js/components/popup-spinner.tsx
@@ -1,4 +1,4 @@
-export const PopupSpinner = () => {
+export const PopupSpinner: React.FC = () => {
return (
{
);
-};
-
+};
\ No newline at end of file
diff --git a/src/assets/js/components/proposal.jsx b/src/assets/js/components/proposal.tsx
similarity index 52%
rename from src/assets/js/components/proposal.jsx
rename to src/assets/js/components/proposal.tsx
index 4f101e9..582dcdb 100644
--- a/src/assets/js/components/proposal.jsx
+++ b/src/assets/js/components/proposal.tsx
@@ -21,81 +21,106 @@ import {
openSettings,
} from '@/features/popupSlice';
-export const Proposal = () => {
+interface ErrorState {
+ show: boolean;
+ message: string;
+}
+
+interface ProposalState {
+ user: any; // TODO: Define proper user type
+ title: string;
+ url: string;
+ cookieJar: Cookie[];
+ state?: SavedAssetState;
+}
+
+interface SavedAssetState {
+ assetId: string;
+ withCookies: boolean;
+}
+
+interface Cookie {
+ domain: string;
+ name: string;
+ value: string;
+ hostOnly?: boolean;
+}
+
+interface ResourceEntry {
+ name: string;
+}
+
+type ButtonState = 'add' | 'update' | 'loading';
+
+export const Proposal: React.FC = () => {
const dispatch = useDispatch();
- const [isLoading, setIsLoading] = useState(false);
- const [assetTitle, setAssetTitle] = useState('');
- const [assetUrl, setAssetUrl] = useState('');
- const [assetHostname, setAssetHostname] = useState('');
- const [buttonState, setButtonState] = useState('add');
- const [error, setError] = useState({
+ const [isLoading, setIsLoading] = useState(false);
+ const [assetTitle, setAssetTitle] = useState('');
+ const [assetUrl, setAssetUrl] = useState('');
+ const [assetHostname, setAssetHostname] = useState('');
+ const [buttonState, setButtonState] = useState('add');
+ const [error, setError] = useState({
show: false,
message: 'Failed to add or update asset'
});
- const [bypassVerification, setBypassVerification] = useState(false);
- const [saveAuthentication, setSaveAuthentication] = useState(false);
- const [proposal, setProposal] = useState(null);
+ const [bypassVerification, setBypassVerification] = useState(false);
+ const [saveAuthentication, setSaveAuthentication] = useState(false);
+ const [proposal, setProposal] = useState(null);
- const updateProposal = (newProposal) => {
- setError((prev) => {
- return {
- ...prev,
- show: false
- };
- });
+ const updateProposal = async (newProposal: ProposalState) => {
+ setError((prev) => ({
+ ...prev,
+ show: false
+ }));
let currentProposal = newProposal;
const url = currentProposal.url;
- return State.getSavedAssetState(url)
- .then((state) => {
- if (state)
- // Does the asset still exist?
- return getWebAsset(state.assetId, newProposal.user)
- .then(() => {
- // Yes it does. Proceed with the update path.
- return state;
- })
- .catch((error) => {
- if (error.status === 404) {
- // It's gone. Proceed like if we never had it.
- State.setSavedAssetState(url, null);
- return undefined;
- }
-
- throw error;
- });
- })
- .then((state) => {
- currentProposal.state = state;
-
- setAssetTitle(currentProposal.title);
- setAssetUrl(currentProposal.url);
- setAssetHostname(new URL(url).hostname);
-
- setProposal(currentProposal);
-
- if (state) {
- setSaveAuthentication(state.withCookies);
- setButtonState('update');
- } else {
- setButtonState('add');
+ try {
+ const state = await State.getSavedAssetState(url);
+
+ if (state) {
+ try {
+ await getWebAsset(state.assetId, newProposal.user);
+ currentProposal.state = state;
+ } catch (error: any) {
+ if (error.status === 404) {
+ State.setSavedAssetState(url, null);
+ currentProposal.state = undefined;
+ } else {
+ throw error;
+ }
}
- })
- .catch((error) => {
- // Unknown error.
- setError((prev) => {
- return {
- ...prev,
- show: true,
- message: 'Failed to check asset.'
- };
- });
- throw error;
- });
- }
+ }
+
+ setAssetTitle(currentProposal.title);
+ setAssetUrl(currentProposal.url);
+ setAssetHostname(new URL(url).hostname);
- const proposeToAddToScreenly = async(user, url, title, cookieJar) => {
+ setProposal(currentProposal);
+
+ if (currentProposal.state) {
+ setSaveAuthentication(currentProposal.state.withCookies);
+ setButtonState('update');
+ } else {
+ setButtonState('add');
+ }
+ } catch (error) {
+ setError((prev) => ({
+ ...prev,
+ show: true,
+ message: 'Failed to check asset.'
+ }));
+ throw error;
+ }
+ };
+
+ const proposeToAddToScreenly = async (
+ user: any,
+ url: string,
+ title: string,
+ cookieJar: Cookie[]
+ ) => {
await updateProposal({
user,
title,
@@ -104,7 +129,7 @@ export const Proposal = () => {
});
};
- const prepareToAddToScreenly = async() => {
+ const prepareToAddToScreenly = async () => {
const onlyPrimaryDomain = true;
const user = await getUser();
@@ -112,12 +137,14 @@ export const Proposal = () => {
return;
}
- let tabs = await browser.tabs.query({ active: true, currentWindow: true });
- let tabId = tabs[0].id;
+ const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ const tabId = tabs[0].id;
+
+ if (!tabId) return;
try {
- let result = await browser.scripting.executeScript({
- target: { tabId: tabId },
+ const result = await browser.scripting.executeScript({
+ target: { tabId },
func: () => {
return [
window.location.href,
@@ -127,7 +154,7 @@ export const Proposal = () => {
}
});
- let [pageUrl, pageTitle, resourceEntries] = result[0].result;
+ const [pageUrl, pageTitle, resourceEntries] = result[0].result;
if (!resourceEntries) {
return;
@@ -135,7 +162,7 @@ export const Proposal = () => {
const originDomain = new URL(pageUrl).host;
- let results = await Promise.all(
+ const results = await Promise.all(
resourceEntries.map(url =>
browser.cookies.getAll({ url })
)
@@ -155,10 +182,9 @@ export const Proposal = () => {
);
if (onlyPrimaryDomain) {
- // noinspection JSUnresolvedVariable
cookieJar = cookieJar.filter(cookie =>
cookie.domain === originDomain || (!cookie.hostOnly && originDomain.endsWith(cookie.domain))
- )
+ );
}
await proposeToAddToScreenly(user, pageUrl, pageTitle, cookieJar);
@@ -175,123 +201,108 @@ export const Proposal = () => {
});
}, []);
- const handleSettings = (event) => {
+ const handleSettings = (event: React.MouseEvent) => {
event.preventDefault();
dispatch(openSettings());
};
- const handleSubmission = async (event) => {
+ const handleSubmission = async (event: React.FormEvent) => {
event.preventDefault();
- let currentProposal = proposal;
-
- if (buttonState === 'loading') {
+ if (!proposal || buttonState === 'loading') {
return;
}
setButtonState('loading');
- let headers = {};
+ let headers: Record = {};
- if (saveAuthentication && currentProposal.cookieJar) {
+ if (saveAuthentication && proposal.cookieJar) {
headers = {
- 'Cookie': currentProposal.cookieJar.map(
+ 'Cookie': proposal.cookieJar.map(
cookie => cookiejs.serialize(cookie.name, cookie.value)
).join('; ')
};
}
- const state = currentProposal.state;
- let action = (
- !state ?
- createWebAsset(
- currentProposal.user,
- currentProposal.url,
- currentProposal.title,
+ const state = proposal.state;
+ try {
+ const result = !state
+ ? await createWebAsset(
+ proposal.user,
+ proposal.url,
+ proposal.title,
headers,
bypassVerification
- ) :
- updateWebAsset(
+ )
+ : await updateWebAsset(
state.assetId,
- currentProposal.user,
- currentProposal.url,
- currentProposal.title,
+ proposal.user,
+ proposal.url,
+ proposal.title,
headers,
bypassVerification
- )
- );
+ );
- action
- .then((result) => {
- if (result.length === 0) {
- throw 'No asset data returned';
- }
+ if (result.length === 0) {
+ throw new Error('No asset data returned');
+ }
- State.setSavedAssetState(
- currentProposal.url,
- result[0].id,
- saveAuthentication,
- bypassVerification
- );
+ State.setSavedAssetState(
+ proposal.url,
+ result[0].id,
+ saveAuthentication,
+ bypassVerification
+ );
- setButtonState(state ? 'update' : 'add');
+ setButtonState(state ? 'update' : 'add');
- const event = new CustomEvent('set-asset-dashboard-link', {
- detail: getAssetDashboardLink(result[0].id)
- });
- document.dispatchEvent(event);
-
- dispatch(notifyAssetSaveSuccess());
- })
- .catch((error) => {
- if (error.statusCode === 401) {
- setError((prev) => {
- return {
- ...prev,
- show: true,
- message: 'Screenly authentication failed. Try signing out and back in again.'
- };
- });
- return;
+ const event = new CustomEvent('set-asset-dashboard-link', {
+ detail: getAssetDashboardLink(result[0].id)
+ });
+ document.dispatchEvent(event);
+
+ dispatch(notifyAssetSaveSuccess());
+ } catch (error: any) {
+ if (error.statusCode === 401) {
+ setError((prev) => ({
+ ...prev,
+ show: true,
+ message: 'Screenly authentication failed. Try signing out and back in again.'
+ }));
+ return;
+ }
+
+ try {
+ const errorJson = await error.json();
+ if (
+ errorJson.type &&
+ errorJson.type[0] === 'AssetUnreachableError'
+ ) {
+ setBypassVerification(true);
+ setError((prev) => ({
+ ...prev,
+ show: true,
+ message: 'Screenly couldn\'t reach this web page. To save it anyhow, use the Bypass Verification option.'
+ }));
+ } else if (!errorJson.type) {
+ throw JSON.stringify(errorJson);
+ } else {
+ throw new Error('Unknown error');
}
+ } catch (jsonError) {
+ const prefix = state
+ ? 'Failed to update asset'
+ : 'Failed to save web page';
- error.json()
- .then((errorJson) => {
- if (
- errorJson.type &&
- errorJson.type[0] === 'AssetUnreachableError'
- ) {
- setBypassVerification(true);
- setError((prev) => {
- return {
- ...prev,
- show: true,
- message: 'Screenly couldn\'t reach this web page. To save it anyhow, use the Bypass Verification option.'
- };
- });
- } else if (!errorJson.type) {
- throw JSON.stringify(errorJson);
- } else {
- throw 'Unknown error';
- }
- }).catch((error) => {
- const prefix = (
- state ?
- 'Failed to update asset' :
- 'Failed to save web page'
- );
- setError((prev) => {
- return {
- ...prev,
- show: true,
- message: (
- `${prefix}: ${error}`
- )
- };
- });
-
- setButtonState(state ? 'update' : 'add');
- });
- });
+ setError((prev) => ({
+ ...prev,
+ show: true,
+ message: `${prefix}: ${jsonError}`
+ }));
+
+ setButtonState(state ? 'update' : 'add');
+ }
+ }
};
if (isLoading) {
@@ -325,7 +336,7 @@ export const Proposal = () => {
className="form-check-label"
htmlFor="with-auth-check"
>
- Save Authentication
+ Save Authentication
@@ -394,4 +405,4 @@ export const Proposal = () => {
);
-};
+};
\ No newline at end of file
diff --git a/src/assets/js/components/save-auth-help.jsx b/src/assets/js/components/save-auth-help.tsx
similarity index 86%
rename from src/assets/js/components/save-auth-help.jsx
rename to src/assets/js/components/save-auth-help.tsx
index 182b6ec..ab40095 100644
--- a/src/assets/js/components/save-auth-help.jsx
+++ b/src/assets/js/components/save-auth-help.tsx
@@ -1,4 +1,4 @@
-export const SaveAuthHelp = () => {
+export const SaveAuthHelp: React.FC = () => {
return (
@@ -8,4 +8,4 @@ export const SaveAuthHelp = () => {
);
-};
+};
\ No newline at end of file
diff --git a/src/assets/js/components/save-auth-warning.jsx b/src/assets/js/components/save-auth-warning.jsx
deleted file mode 100644
index e5643a0..0000000
--- a/src/assets/js/components/save-auth-warning.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { useEffect, useState } from 'react';
-
-export const SaveAuthWarning = (props) => {
- const [hostname, setHostname] = useState('');
-
- useEffect(() => {
- setHostname(props.hostname);
- }, [props.hostname]);
-
- return (
-
-
-
- Warning: a determined attacker with physical access to your digital sign could extract these saved credentials for
-
-
- {` ${hostname} `}
-
-
-
- and gain access to your account.
-
-
-
- );
-};
diff --git a/src/assets/js/components/save-auth-warning.tsx b/src/assets/js/components/save-auth-warning.tsx
new file mode 100644
index 0000000..397be1a
--- /dev/null
+++ b/src/assets/js/components/save-auth-warning.tsx
@@ -0,0 +1,33 @@
+import { useEffect, useState } from 'react';
+
+interface SaveAuthWarningProps {
+ hostname: string;
+ hidden: boolean;
+}
+
+export const SaveAuthWarning: React.FC = ({ hostname, hidden }) => {
+ const [currentHostname, setCurrentHostname] = useState('');
+
+ useEffect(() => {
+ setCurrentHostname(hostname);
+ }, [hostname]);
+
+ return (
+
+
+
+ Warning: a determined attacker with physical access to your digital sign could extract these saved credentials for
+
+
+ {` ${currentHostname} `}
+
+
+ and gain access to your account.
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/assets/js/components/settings.jsx b/src/assets/js/components/settings.tsx
similarity index 87%
rename from src/assets/js/components/settings.jsx
rename to src/assets/js/components/settings.tsx
index 0ad63e5..f4cc2ef 100644
--- a/src/assets/js/components/settings.jsx
+++ b/src/assets/js/components/settings.tsx
@@ -4,11 +4,11 @@ import classNames from 'classnames';
import { signOut } from '@/features/popupSlice';
-export const Settings = () => {
+export const Settings: React.FC = () => {
const dispatch = useDispatch();
- const [isLoading, setIsLoading] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
- const handleSignOut = async (event) => {
+ const handleSignOut = async (event: React.MouseEvent) => {
event.preventDefault();
setIsLoading(true);
dispatch(signOut());
@@ -56,4 +56,4 @@ export const Settings = () => {
);
-};
+};
\ No newline at end of file
diff --git a/src/assets/js/components/sign-in-success.jsx b/src/assets/js/components/sign-in-success.tsx
similarity index 94%
rename from src/assets/js/components/sign-in-success.jsx
rename to src/assets/js/components/sign-in-success.tsx
index 4f30870..f977a83 100644
--- a/src/assets/js/components/sign-in-success.jsx
+++ b/src/assets/js/components/sign-in-success.tsx
@@ -1,4 +1,4 @@
-export const SignInSuccess = () => {
+export const SignInSuccess: React.FC = () => {
return (
@@ -25,4 +25,4 @@ export const SignInSuccess = () => {
);
-};
+};
\ No newline at end of file
diff --git a/src/assets/js/components/sign-in.jsx b/src/assets/js/components/sign-in.tsx
similarity index 80%
rename from src/assets/js/components/sign-in.jsx
rename to src/assets/js/components/sign-in.tsx
index 46e3114..dad8149 100644
--- a/src/assets/js/components/sign-in.jsx
+++ b/src/assets/js/components/sign-in.tsx
@@ -10,7 +10,11 @@ import {
} from '@/features/popupSlice';
import { TokenHelpText } from '@/components/token-help-text';
-const SignInFormError = ({ message }) => {
+interface SignInFormErrorProps {
+ message?: string;
+}
+
+const SignInFormError: React.FC = ({ message }) => {
return (
Unable to sign in? Check your credentials and internet connectivity,
@@ -21,13 +25,13 @@ const SignInFormError = ({ message }) => {
);
};
-export const SignInForm = () => {
- const [isLoading, setIsLoading] = useState(false);
- const [showSignInFormError, setShowSignInFormError] = useState(false);
- const [token, setToken] = useState('');
+export const SignInForm: React.FC = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [showSignInFormError, setShowSignInFormError] = useState(false);
+ const [token, setToken] = useState('');
const dispatch = useDispatch();
- const handleSignIn = async (event) => {
+ const handleSignIn = async (event: React.FormEvent) => {
event.preventDefault();
setIsLoading(true);
@@ -68,12 +72,13 @@ export const SignInForm = () => {
className="mb-3"
src="assets/images/screenly-logo.svg"
width="64"
+ alt="Screenly Logo"
/>
Sign In