Skip to content

Commit

Permalink
tweak(ext/cfx-ui): enforce TOS accepting
Browse files Browse the repository at this point in the history
  • Loading branch information
nihonium-cfx committed Sep 19, 2023
1 parent 9729577 commit e06177c
Show file tree
Hide file tree
Showing 14 changed files with 330 additions and 48 deletions.
26 changes: 22 additions & 4 deletions ext/cfx-ui/src/cfx/apps/mpMenu/components/MpMenuApp/MpMenuApp.tsx
@@ -1,3 +1,4 @@
import { observer } from "mobx-react-lite";
import { Outlet } from 'react-router-dom';
import { NavBar } from 'cfx/apps/mpMenu/parts/NavBar/NavBar';
import { AuthFlyout } from 'cfx/apps/mpMenu/parts/AuthFlyout/AuthFlyout';
Expand All @@ -7,13 +8,14 @@ import { LegacyConnectingModal } from 'cfx/apps/mpMenu/parts/LegacyConnectingMod
import { LegacyUiMessageModal } from 'cfx/apps/mpMenu/parts/LegacyUiMessageModal/LegacyUiMessageModal';
import { ServerBoostModal } from 'cfx/apps/mpMenu/parts/ServerBoostModal/ServerBoostModal';
import { AcitivityItemMediaViewerProvider } from '../AcitivityItemMediaViewer/AcitivityItemMediaViewer.context';
import { LegalAccepter } from "cfx/apps/mpMenu/parts/LegalAccepter/LegalAccepter";
import { useLegalService } from "cfx/apps/mpMenu/services/legal/legal.service";
import { NavigationTracker } from './PageViewTracker';
import s from './MpMenuApp.module.scss';

export function MpMenuApp() {
function MpMenuUI() {
return (
<AcitivityItemMediaViewerProvider>
<ThemeManager />
<>
<NavigationTracker />

<AuthFlyout />
Expand All @@ -31,6 +33,22 @@ export function MpMenuApp() {
<Outlet />
</div>
</div>
</AcitivityItemMediaViewerProvider>
</>
);
}

export const MpMenuApp = observer(function MpMenuApp() {
const legalService = useLegalService();

const mainUI = legalService.hasUserAccepted
? <MpMenuUI />
: <LegalAccepter />;

return (
<AcitivityItemMediaViewerProvider>
<ThemeManager />

{mainUI}
</AcitivityItemMediaViewerProvider>
);
});
30 changes: 7 additions & 23 deletions ext/cfx-ui/src/cfx/apps/mpMenu/index.tsx
Expand Up @@ -40,8 +40,10 @@ import { IServersConnectService } from 'cfx/common/services/servers/serversConne
import { Handle404 } from './pages/404';
import { registerHomeScreenServerList } from './services/servers/list/HomeScreenServerList.service';
import { registerSentryService } from './services/sentry/sentry.service';
import { registerLegalService } from "./services/legal/legal.service";
import { SentryLogProvider } from './services/sentry/sentryLogProvider';
import { animationFrame, idleCallback, timeout } from 'cfx/utils/async';
import { timeout } from 'cfx/utils/async';
import { shutdownLoadingSplash } from "./utils/loadingSplash";

startBrowserApp({
defineServices(container) {
Expand All @@ -56,6 +58,8 @@ startBrowserApp({
MatomoAnalyticsProvider,
]);

registerLegalService(container);

registerSettingsService(container, {
settings: GAME_SETTINGS,
defaultSettingsCategoryId: DEFAULT_GAME_SETTINGS_CATEGORY_ID,
Expand Down Expand Up @@ -142,29 +146,9 @@ startBrowserApp({
mpMenu.invokeNative('executeCommand', 'nui_devtools mpMenu');
}

mpMenu.invokeNative('getMinModeInfo');
mpMenu.showGameWindow();

// Not using await here so app won't wait for this to end
timeout(1000).then(animationFrame).then(() => {
const $loader = document.getElementById('loader');
if (!$loader) {
console.error('No #loader found, did it get deleted from index.html?');
return;
}

$loader.classList.add('hide');

const $loaderMask = $loader.querySelector('#loader-mask');
if (!$loaderMask) {
console.error('No #loader-mask found, did it get deleted from index.html?');
return;
}

$loaderMask.addEventListener('animationend', async () => {
await idleCallback(1000);

$loader.parentNode?.removeChild($loader);
});
});
timeout(1000).then(shutdownLoadingSplash);
},
});
23 changes: 21 additions & 2 deletions ext/cfx-ui/src/cfx/apps/mpMenu/mpMenu.ts
Expand Up @@ -60,7 +60,9 @@ class NicknameStore {

export namespace mpMenu {
export function invokeNative(native: string, arg = '') {
console.log('Invoking native', native, JSON.stringify(arg));
if (__CFXUI_DEV__) {
console.log('Invoking native', native, JSON.stringify(arg));
}

nuiWindow.invokeNative(native, arg);
}
Expand All @@ -79,6 +81,21 @@ export namespace mpMenu {
invokeNative('openUrl', url);
}

let showGameWindowRequested = false;
export function showGameWindow() {
if (showGameWindowRequested) {
return;
}

showGameWindowRequested = true;

invokeNative('getMinModeInfo');
}

export function exit() {
invokeNative('exit');
}

export const systemLanguages = [...new Set(nuiWindow.nuiSystemLanguages || ['en-us'])];

export const computerName = new AwaitableValue('');
Expand Down Expand Up @@ -152,7 +169,9 @@ export namespace mpMenu {


window.addEventListener('message', (event: MessageEvent) => {
console.log('[WNDMSG]', event.data);
if (__CFXUI_DEV__) {
console.log('[WNDMSG]', event.data);
}

const { data } = event;

Expand Down
@@ -0,0 +1,15 @@
.root {
width: min(max(calc(#{ui.viewport-width()} * .5), 500px), #{ui.viewport-width()});
height: ui.viewport-height();

box-shadow: 0 0 0 2px ui.color-token('island-border') inset;

@include ui.border-radius();
background-color: ui.color-token('flyout-backdrop-background');

.iframe {
width: 100%;
height: 100%;
border: none;
}
}
@@ -0,0 +1,71 @@
import { observer } from "mobx-react-lite";
import { mpMenu } from "../../mpMenu";
import { Pad } from "cfx/ui/Layout/Pad/Pad";
import { Flex } from "cfx/ui/Layout/Flex/Flex";
import { TextBlock } from "cfx/ui/Text/Text";
import { FlexRestricter } from "cfx/ui/Layout/Flex/FlexRestricter";
import { Button } from "cfx/ui/Button/Button";
import { Icons } from "cfx/ui/Icons";
import { Title } from "cfx/ui/Title/Title";
import { Icon } from "cfx/ui/Icon/Icon";
import { CurrentGameBrand } from "cfx/base/gameRuntime";
import { useLegalService } from "cfx/apps/mpMenu/services/legal/legal.service";
import s from './LegalAccepter.module.scss';

export const LegalAccepter = observer(function TOSAccepter() {
const legalService = useLegalService();

return (
<div className={s.root}>
{/* The reason for the reverse order here is so that the "I Accept" button captures focus first when/if user press Tab key */}
<Flex vertical fullHeight fullWidth reverseOrder gap="none">
<Pad size="large">
<Flex repell centered reverseOrder>
<Button
tabIndex={0}
theme="primary"
size="large"
text="I Accept"
onClick={legalService.accept}
/>

<Title delay={1000} title={`Exit ${CurrentGameBrand}`}>
<Button
tabIndex={1}
text="Cancel"
onClick={mpMenu.exit}
/>
</Title>
</Flex>
</Pad>

<FlexRestricter vertical>
<iframe
src={`${legalService.TOS_URL}#toolbar=0&view=FitH`}
className={s.iframe}
></iframe>
</FlexRestricter>

<Pad size="large">
<Flex vertical gap="large">
<TextBlock family="secondary" weight="bolder" size="xxlarge">
Terms of Service
</TextBlock>

<Flex vertical gap="small">
<TextBlock opacity="75">
Last modified: {legalService.CURRENT_TOS_VERSION}
</TextBlock>

<TextBlock typographic opacity="75">
<Title title={legalService.TOS_URL}>
<a href={legalService.TOS_URL}>Open the Terms of Service in your browser <Icon size="small">{Icons.externalLink}</Icon></a>
</Title>
</TextBlock>
</Flex>
</Flex>
</Pad>
</Flex>
</div>
);
});
Expand Up @@ -44,7 +44,7 @@ export function Exitter() {
theme="default-blurred"
size="large"
text={$L('#ExitToDesktop')}
onClick={() => mpMenu.invokeNative('exit')}
onClick={mpMenu.exit}
/>
<Button
theme="primary"
Expand Down
65 changes: 65 additions & 0 deletions ext/cfx-ui/src/cfx/apps/mpMenu/services/legal/legal.service.ts
@@ -0,0 +1,65 @@
import { ServicesContainer, defineService, useService } from "cfx/base/servicesContainer";
import { ASID } from "cfx/utils/asid";
import { joaat } from "cfx/utils/hash";
import { injectable } from "inversify";
import { makeAutoObservable } from "mobx";

export const ILegalService = defineService<LegalService>('legalService');

export function registerLegalService(container: ServicesContainer) {
container.registerImpl(ILegalService, LegalService);
}

export function useLegalService() {
return useService(ILegalService);
}

const LS_KEY = 'legalAcceptanceData';

type TOSVersionHash = number;
type AcceptanceTimestamp = number;

type LegalAcceptanceData = [AcceptanceTimestamp, TOSVersionHash];

@injectable()
export class LegalService {
readonly CURRENT_TOS_VERSION = '2020-09-06';
readonly TOS_URL = 'https://runtime.fivem.net/fivem-service-agreement-4.pdf';

private readonly currentTOSVersionHash = joaat(`${ASID}.${this.CURRENT_TOS_VERSION}`);

private _hasUserAccepted: boolean;
public get hasUserAccepted(): boolean { return this._hasUserAccepted }
private set hasUserAccepted(hasUserAccepted: boolean) { this._hasUserAccepted = hasUserAccepted }

constructor() {
this._hasUserAccepted = this.reviveHasUserAccepted();

makeAutoObservable(this);
}

public readonly accept = () => {
try {
const data: LegalAcceptanceData = [
Date.now(),
this.currentTOSVersionHash,
];

window.localStorage.setItem(LS_KEY, JSON.stringify(data));

this.hasUserAccepted = true;
} catch (e) {
// no-op
}
};

private reviveHasUserAccepted(): boolean {
try {
const [_timestamp, acceptedTOSVersionHash] = JSON.parse(window.localStorage.getItem(LS_KEY) || '[]') as LegalAcceptanceData;

return acceptedTOSVersionHash === this.currentTOSVersionHash;
} catch (e) {
return false;
}
}
}
24 changes: 7 additions & 17 deletions ext/cfx-ui/src/cfx/apps/mpMenu/services/sentry/sentry.service.ts
Expand Up @@ -6,10 +6,10 @@ import { IAccountService } from 'cfx/common/services/account/account.service';
import { IAccount } from 'cfx/common/services/account/types';
import { AppContribution, registerAppContribution } from 'cfx/common/services/app/app.extensions';
import { fetcher } from 'cfx/utils/fetcher';
import { fastRandomId } from 'cfx/utils/random';
import { inject, injectable } from 'inversify';
import { mpMenu } from '../../mpMenu';
import { IConvarService } from '../convars/convars.service';
import { ASID } from "cfx/utils/asid";

const ENABLE_SENTRY = !__CFXUI_DEV__ && process.env.CI_PIPELINE_ID;

Expand Down Expand Up @@ -58,22 +58,12 @@ if (ENABLE_SENTRY) {
},
});

const id = ASID;

// Set stable user id early
try {
if (!window.localStorage['sentryUserId']) {
window.localStorage['sentryUserId'] = `${fastRandomId()}-${fastRandomId()}`;
}

const id = window.localStorage['sentryUserId'];

Sentry.setUser({
id,
username: id,
});
} catch (e) {
// no-op
}
Sentry.setUser({
id,
username: id,
});
}

export function registerSentryService(container: ServicesContainer) {
Expand Down Expand Up @@ -123,7 +113,7 @@ class SentryService implements AppContribution {

Sentry.setContext('convars', Object.fromEntries(
Object.entries(this.convarService.getAll()).filter(([key, value]) => {
if (key.startsWith('cl_')) {
if (key.startsWith('cl_')) {
return false;
}
if (key.startsWith('cam_')) {
Expand Down
41 changes: 41 additions & 0 deletions ext/cfx-ui/src/cfx/apps/mpMenu/utils/loadingSplash.ts
@@ -0,0 +1,41 @@
import { Deferred, idleCallback } from "cfx/utils/async";

let loadingSplashShutdownRequested = true;

export const loadingSplashInactiveDeferred = new Deferred<void>();

export function shutdownLoadingSplash() {
if (!loadingSplashShutdownRequested) {
return;
}

loadingSplashShutdownRequested = false;

try {
const $loader = document.getElementById('loader');
if (!$loader) {
throw new Error('No #loader found, did it get deleted from index.html?');
}

const $loaderMask = $loader.querySelector('#loader-mask');
if (!$loaderMask) {
throw new Error('No #loader-mask found, did it get deleted from index.html?');
}

requestAnimationFrame(() => {
$loader.classList.add('hide');

$loaderMask.addEventListener('animationend', async () => {
await idleCallback(1000);

$loader.parentNode?.removeChild($loader);
});

loadingSplashInactiveDeferred.resolve();
});
} catch (e) {
loadingSplashInactiveDeferred.resolve();

console.error(e);
}
}

0 comments on commit e06177c

Please sign in to comment.