Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Rebase] Warn users on possible risks on installing third party unvetted plugins #676

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions backend/decky_loader/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@
"testing": "Testing"
}
},
"WarnThirdParty":{
"title_zip": "Third-Party Plugin Installation",
"title_repo": "Third-Party Store Selection",
"button_processing_one": "Please wait {{timer}} second",
"button_processing_many": "Please wait {{timer}} seconds",
"button_idle": "Continue",
"desc_zip": "The Decky Loader team has not reviewed this plugin. It may contain malware, such as software to steal your Steam account or harm your device. By installing this plugin, you agree you have assumed all risks to your device.",
"desc_repo": "The Decky Loader team does not maintain this plugin store. It and its plugins may contain malware, such as software to steal your Steam account or harm your device. By adding this store, you agree you have assumed all risks to your device."
},
"Testing": {
"download": "Download",
"error": "Error Installing PR",
Expand Down
78 changes: 78 additions & 0 deletions frontend/src/components/modals/WarnThirdParty.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ConfirmModal } from '@decky/ui';
import { FC, useEffect, useState } from 'react';
import { FaExclamationTriangle } from 'react-icons/fa';

import { WarnThirdPartyType } from '../../utils/globalTypes';
import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper';

interface WarnThirdPartyProps {
seconds?: number;
type: WarnThirdPartyType;
onOK(): void;
onCancel(): void;
closeModal?(): void;
}

const WarnThirdParty: FC<WarnThirdPartyProps> = ({ seconds = 5, type, onOK, onCancel, closeModal }) => {
const [waitTimer, setWaitTimer] = useState(seconds);

useEffect(() => {
// exit early when we reach 0
if (waitTimer <= 0) return;

// save intervalId to clear the interval when the
// component re-renders
const intervalId = setInterval(() => {
setWaitTimer(waitTimer - 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't set timeout work better for this then? Unless I'm misunderstanding?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, fair enough. Not really that skilled with react.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AAGaming00 current solution isn't ideal but should it be changed or can this PR go ahead?

Copy link
Contributor Author

@RodoMa92 RodoMa92 Aug 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rereading this now, no, because I'm referencing the timeout on the translation on line 58 to show the remaining time ticking down to give additional feedback to the user, so I can't see how I would be doing this with one single timeout tick compared to one for each second. It was like 6 months from when I wrote this, so I was not that sure why I did stuff at the time, sorry XD. Let me know if I'm missing something.

}, 1000);

// clear interval on re-render to avoid memory leaks
return () => clearInterval(intervalId);
// add waitTimer as a dependency to re-rerun the effect
// when we update it
}, [waitTimer]);

return (
<ConfirmModal
bOKDisabled={waitTimer > 0}
closeModal={closeModal}
onOK={async () => {
await onOK();
}}
onCancel={async () => {
await onCancel();
}}
strTitle={
<div>
<FaExclamationTriangle />
<TranslationHelper transClass={TranslationClass.WARN_THIRD_PARTY} transText="title" warnType={type} />
</div>
}
strOKButtonText={
waitTimer > 0 ? (
<div>
<TranslationHelper
transClass={TranslationClass.WARN_THIRD_PARTY}
transText="button_processing"
i18nArgs={{
timer: waitTimer,
}}
/>
</div>
) : (
<div>
<TranslationHelper transClass={TranslationClass.WARN_THIRD_PARTY} transText="button_idle" />
</div>
)
}
>
<span style={{ color: 'red' }}>
<div>
<TranslationHelper transClass={TranslationClass.WARN_THIRD_PARTY} transText="desc" warnType={type} />
</div>
</span>
</ConfirmModal>
);
};

export default WarnThirdParty;
22 changes: 21 additions & 1 deletion frontend/src/components/settings/pages/developer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Navigation,
TextField,
Toggle,
showModal,
} from '@decky/ui';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -15,9 +16,11 @@ import { FaFileArchive, FaLink, FaReact, FaSteamSymbol, FaTerminal } from 'react
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
import Logger from '../../../../logger';
import { installFromURL } from '../../../../store';
import { WarnThirdPartyType } from '../../../../utils/globalTypes';
import { useSetting } from '../../../../utils/hooks/useSetting';
import { getSetting } from '../../../../utils/settings';
import { FileSelectionType } from '../../../modals/filepicker';
import WarnThirdParty from '../../../modals/WarnThirdParty';
import RemoteDebuggingSettings from '../general/RemoteDebugging';

const logger = new Logger('DeveloperIndex');
Expand All @@ -43,6 +46,8 @@ export default function DeveloperSettings() {
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', '');
const [acceptedWarning, setAcceptedWarning] = useSetting<boolean>('developer.warn.third_party', false);
const waitTime = acceptedWarning ? 0 : 5;
const [pluginURL, setPluginURL] = useState('');
const textRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
Expand Down Expand Up @@ -72,7 +77,22 @@ export default function DeveloperSettings() {
}
icon={<FaLink style={{ display: 'block' }} />}
>
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
<DialogButton
disabled={pluginURL.length == 0}
onClick={() =>
showModal(
<WarnThirdParty
type={WarnThirdPartyType.ZIP}
onOK={() => {
setAcceptedWarning(true);
installFromURL(pluginURL);
}}
onCancel={() => {}}
seconds={waitTime}
/>,
)
}
>
{t('SettingsDeveloperIndex.third_party_plugins.button_install')}
</DialogButton>
</Field>
Expand Down
44 changes: 29 additions & 15 deletions frontend/src/components/settings/pages/general/StoreSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { Dropdown, Field, TextField } from '@decky/ui';
import { Dropdown, Field, TextField, showModal } from '@decky/ui';
import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import { FaShapes } from 'react-icons/fa';

import Logger from '../../../../logger';
import { Store } from '../../../../store';
import { WarnThirdPartyType } from '../../../../utils/globalTypes';
import { useSetting } from '../../../../utils/hooks/useSetting';
import WarnThirdParty from '../../../modals/WarnThirdParty';

const logger = new Logger('StoreSelect');

const StoreSelect: FunctionComponent<{}> = () => {
const [selectedStore, setSelectedStore] = useSetting<Store>('store', Store.Default);
const [selectedStoreURL, setSelectedStoreURL] = useSetting<string | null>('store-url', null);
const [acceptedWarning, setAcceptedWarning] = useSetting<boolean>('store_select.warn.third_party', false);
const waitTime = acceptedWarning ? 0 : 5;
const { t } = useTranslation();
const tStores = [
t('StoreSelect.store_channel.default'),
Expand All @@ -38,20 +42,30 @@ const StoreSelect: FunctionComponent<{}> = () => {
}}
/>
</Field>
{selectedStore == Store.Custom && (
<Field
label={t('StoreSelect.custom_store.label')}
indentLevel={1}
description={
<TextField
label={t('StoreSelect.custom_store.url_label')}
value={selectedStoreURL || undefined}
onChange={(e) => setSelectedStoreURL(e?.target.value || null)}
/>
}
icon={<FaShapes style={{ display: 'block' }} />}
></Field>
)}
{selectedStore == Store.Custom &&
showModal(
<WarnThirdParty
type={WarnThirdPartyType.REPO}
seconds={waitTime}
onOK={() => {
setAcceptedWarning(true);
}}
onCancel={() => setSelectedStore(Store.Default)}
/>,
) && (
<Field
label={t('StoreSelect.custom_store.label')}
indentLevel={1}
description={
<TextField
label={t('StoreSelect.custom_store.url_label')}
value={selectedStoreURL || undefined}
onChange={(e) => setSelectedStoreURL(e?.target.value || null)}
/>
}
icon={<FaShapes style={{ display: 'block' }} />}
></Field>
)}
</>
);
};
Expand Down
30 changes: 29 additions & 1 deletion frontend/src/utils/TranslationHelper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,32 @@ import { Translation } from 'react-i18next';

import Logger from '../logger';
import { InstallType } from '../plugin';
import { WarnThirdPartyType } from './globalTypes';

export enum TranslationClass {
PLUGIN_LOADER = 'PluginLoader',
PLUGIN_INSTALL_MODAL = 'PluginInstallModal',
DEVELOPER = 'Developer',
WARN_THIRD_PARTY = 'WarnThirdParty',
}

interface TranslationHelperProps {
transClass: TranslationClass;
transText: string;
i18nArgs?: {};
installType?: number;
warnType?: WarnThirdPartyType;
}

const logger = new Logger('TranslationHelper');

const TranslationHelper: FC<TranslationHelperProps> = ({ transClass, transText, i18nArgs = null, installType = 0 }) => {
const TranslationHelper: FC<TranslationHelperProps> = ({
transClass,
transText,
i18nArgs = null,
installType = 0,
warnType = WarnThirdPartyType.REPO,
}) => {
return (
<Translation>
{(t, {}) => {
Expand Down Expand Up @@ -47,6 +56,25 @@ const TranslationHelper: FC<TranslationHelperProps> = ({ transClass, transText,
return i18nArgs
? t(TranslationClass.DEVELOPER + '.' + transText, i18nArgs)
: t(TranslationClass.DEVELOPER + '.' + transText);
//Handle different messages in different class cases
case TranslationClass.WARN_THIRD_PARTY:
//Needed only for title and description
if (!transText.startsWith('button')) {
switch (warnType) {
case WarnThirdPartyType.REPO:
return i18nArgs
? t(TranslationClass.WARN_THIRD_PARTY + '.' + transText + '_repo', i18nArgs)
: t(TranslationClass.WARN_THIRD_PARTY + '.' + transText + '_repo');
case WarnThirdPartyType.ZIP:
return i18nArgs
? t(TranslationClass.WARN_THIRD_PARTY + '.' + transText + '_zip', i18nArgs)
: t(TranslationClass.WARN_THIRD_PARTY + '.' + transText + '_zip');
}
} else {
return i18nArgs
? t(TranslationClass.WARN_THIRD_PARTY + '.' + transText, i18nArgs)
: t(TranslationClass.WARN_THIRD_PARTY + '.' + transText);
}
default:
logger.error('We should never fall in the default case!');
return '';
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/utils/globalTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum WarnThirdPartyType {
REPO = 0,
ZIP = 1,
}
Loading