diff --git a/src/common/constants.ts b/src/common/constants.ts index ed1f82805..793212854 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -6,6 +6,8 @@ export const slippiHomepage = "https://slippi.gg"; export const slippiActivationUrl = "https://slippi.gg/online/enable"; export const slippiManagePage = "https://slippi.gg/manage"; +export const currentRulesVersion = 1; + export const socials = { twitterId: "ProjectSlippi", discordUrl: "https://slippi.gg/discord", diff --git a/src/renderer/containers/ActivateOnlineForm.tsx b/src/renderer/containers/ActivateOnlineForm.tsx index fbd7c518f..7b6fecac1 100644 --- a/src/renderer/containers/ActivateOnlineForm.tsx +++ b/src/renderer/containers/ActivateOnlineForm.tsx @@ -8,7 +8,7 @@ import Typography from "@mui/material/Typography"; import React from "react"; import { Controller, useForm } from "react-hook-form"; -import { useAccount, usePlayKey } from "@/lib/hooks/useAccount"; +import { useAccount, useUserData } from "@/lib/hooks/useAccount"; import { useToasts } from "@/lib/hooks/useToasts"; import { validateConnectCodeStart } from "@/lib/validate"; import { useServices } from "@/services"; @@ -17,7 +17,7 @@ const log = window.electron.log; export const ActivateOnlineForm: React.FC<{ onSubmit?: () => void }> = ({ onSubmit }) => { const user = useAccount((store) => store.user); - const refreshActivation = usePlayKey(); + const refreshActivation = useUserData(); return (
Your connect code is used for players to connect with you directly.
diff --git a/src/renderer/containers/Header/ActivateOnlineDialog.tsx b/src/renderer/containers/Header/ActivateOnlineDialog.tsx index 2ff5aaa3f..8f4436b3d 100644 --- a/src/renderer/containers/Header/ActivateOnlineDialog.tsx +++ b/src/renderer/containers/Header/ActivateOnlineDialog.tsx @@ -6,7 +6,7 @@ import { useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; import React from "react"; -import { usePlayKey } from "@/lib/hooks/useAccount"; +import { useUserData } from "@/lib/hooks/useAccount"; import { useToasts } from "@/lib/hooks/useToasts"; import { ActivateOnlineForm } from "../ActivateOnlineForm"; @@ -20,11 +20,11 @@ export interface ActivateOnlineDialogProps { export const ActivateOnlineDialog: React.FC = ({ open, onClose, onSubmit }) => { const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - const refreshPlayKey = usePlayKey(); + const refreshUserData = useUserData(); const { showError } = useToasts(); const handleSubmit = () => { - refreshPlayKey() + refreshUserData() .then(() => { onClose(); onSubmit(); diff --git a/src/renderer/containers/Header/UserMenu.tsx b/src/renderer/containers/Header/UserMenu.tsx index d055732ea..b70a3ac56 100644 --- a/src/renderer/containers/Header/UserMenu.tsx +++ b/src/renderer/containers/Header/UserMenu.tsx @@ -21,7 +21,7 @@ export const UserMenu: React.FC<{ handleError: (error: any) => void; }> = ({ user, handleError }) => { const { authService } = useServices(); - const playKey = useAccount((store) => store.playKey); + const userData = useAccount((store) => store.userData); const displayName = useAccount((store) => store.displayName); const loading = useAccount((store) => store.loading); const serverError = useAccount((store) => store.serverError); @@ -54,7 +54,7 @@ export const UserMenu: React.FC<{ const generateMenuItems = (): IconMenuItem[] => { const items: IconMenuItem[] = []; - if (!playKey && !serverError) { + if (!userData?.playKey && !serverError) { items.push({ onClick: () => { closeMenu(); @@ -65,7 +65,7 @@ export const UserMenu: React.FC<{ }); } - if (playKey) { + if (userData) { items.push({ onClick: () => { closeMenu(); @@ -90,7 +90,7 @@ export const UserMenu: React.FC<{ let errMessage: string | undefined = undefined; if (serverError) { errMessage = "Slippi server error"; - } else if (!playKey) { + } else if (!userData?.playKey) { errMessage = "Online activation required"; } @@ -100,7 +100,7 @@ export const UserMenu: React.FC<{ diff --git a/src/renderer/containers/Header/index.tsx b/src/renderer/containers/Header/index.tsx index 54397419f..90ec6c8fc 100644 --- a/src/renderer/containers/Header/index.tsx +++ b/src/renderer/containers/Header/index.tsx @@ -46,7 +46,7 @@ export const Header: React.FC = ({ menuItems }) => { const openModal = useLoginModal((store) => store.openModal); const { open } = useSettingsModal(); const currentUser = useAccount((store) => store.user); - const playKey = useAccount((store) => store.playKey); + const userData = useAccount((store) => store.userData); const serverError = useAccount((store) => store.serverError); const meleeIsoPath = useSettings((store) => store.settings.isoPath) || undefined; const { showError } = useToasts(); @@ -62,15 +62,15 @@ export const Header: React.FC = ({ menuItems }) => { } // Ensure user has a valid play key - if (!playKey && !serverError) { + if (!userData?.playKey && !serverError) { setActivateOnlineModal(true); return; } - if (playKey) { + if (userData?.playKey) { // Ensure the play key is saved to disk try { - await slippiBackendService.assertPlayKey(playKey); + await slippiBackendService.assertPlayKey(userData.playKey); } catch (err) { showError(err); return; @@ -87,7 +87,7 @@ export const Header: React.FC = ({ menuItems }) => { return; }, - [currentUser, launchNetplay, meleeIsoPath, playKey, serverError, showError, slippiBackendService], + [currentUser, launchNetplay, meleeIsoPath, userData, serverError, showError, slippiBackendService], ); return ( diff --git a/src/renderer/containers/QuickStart/AcceptRulesStep.tsx b/src/renderer/containers/QuickStart/AcceptRulesStep.tsx new file mode 100644 index 000000000..d14779e45 --- /dev/null +++ b/src/renderer/containers/QuickStart/AcceptRulesStep.tsx @@ -0,0 +1,177 @@ +import { colors } from "@common/colors"; +import { css } from "@emotion/react"; +import styled from "@emotion/styled"; +import { Button, Checkbox, CircularProgress, FormControlLabel, Typography } from "@mui/material"; +import Box from "@mui/material/Box"; +import React, { useState } from "react"; + +import { ExternalLink as A } from "@/components/ExternalLink"; +import { useAccount, useUserData } from "@/lib/hooks/useAccount"; +import { useToasts } from "@/lib/hooks/useToasts"; +import { useServices } from "@/services"; + +import { QuickStartHeader } from "./QuickStartHeader"; + +const Container = styled.div` + margin: 0 auto; + width: 100%; + max-width: 800px; +`; + +const classes = { + sectionHeader: css` + margin-top: 32px; + margin-bottom: 12px; + font-weight: bold; + `, + rulesContainer: css` + color: ${colors.textSecondary}; + padding: 8px; + background-color: #00000040; + border-radius: 8px; + margin-bottom: 12px; + `, + rulesList: css` + margin-left: 8px; + margin-top: 8px; + display: grid; + grid-template-columns: auto 1fr; + grid-gap: 8px; + gap: 8px; + `, + policiesList: css` + margin-left: 16px; + margin-top: 8px; + margin-bottom: 8px; + display: grid; + grid-template-columns: auto 1fr; + grid-gap: 8px 14px; + gap: 8px 14px; + `, + button: css` + margin-top: 32px; + width: 150px; + height: 54px; + `, + link: css` + color: #b984bb; + `, +}; + +export const AcceptRulesStep: React.FC = () => { + const { slippiBackendService } = useServices(); + const { showError } = useToasts(); + const refreshUserData = useUserData(); + const user = useAccount((store) => store.user); + const [rulesChecked, setRulesChecked] = useState(false); + const [policiesChecked, setPoliciesChecked] = useState(false); + const [processing, setProcessing] = useState(false); + + const handleAcceptClick = async () => { + setProcessing(true); + + try { + await slippiBackendService.acceptRules(); + await refreshUserData(); + } catch (err: any) { + showError(err.message); + } finally { + setProcessing(false); + } + }; + + // TODO: Only show slippi rules if rulesAccepted is null/0 ? + + let stepBody = null; + if (user) { + stepBody = ( + <> + Slippi Online Rules +
+ + These are a set of rules to follow when using Slippi. Breaking these rules may result in a suspension or ban + depending on severity and frequency. This is not an exhaustive list, we reserve the right to suspend or ban + an account for any reason. + +
+ 1. + + Racist, homophobic, transphobic, or otherwise bigoted names and codes are not allowed. Targeted harassment + in names and codes is also not allowed. + + 2. + + Slippi does its best to promote fairness in terms of player matching, result reporting, etc. Attempting to + circumvent these systems is not allowed. + + 3. + Intentionally manipulating the game performance for your own gain is not allowed. + 4. + Macros and bots are not allowed. +
+
+ setRulesChecked(value)} + sx={{ "& .MuiSvgIcon-root": { fontSize: 28 } }} + /> + } + /> + Privacy Policy and Terms of Service +
+ + + Click to review the{" "} + + Slippi Privacy Policy + + + + + Click to review the{" "} + + Slippi Terms of Service + + +
+ setPoliciesChecked(value)} + sx={{ "& .MuiSvgIcon-root": { fontSize: 28 } }} + /> + } + /> +
+ +
+ + ); + } else { + stepBody =
An error occurred. The application does not have a user.
; + } + + return ( + + + Accept rules and policies + {stepBody} + + + ); +}; diff --git a/src/renderer/containers/QuickStart/VerifyEmailStep.tsx b/src/renderer/containers/QuickStart/VerifyEmailStep.tsx index 7d4313489..41b56647e 100644 --- a/src/renderer/containers/QuickStart/VerifyEmailStep.tsx +++ b/src/renderer/containers/QuickStart/VerifyEmailStep.tsx @@ -9,6 +9,7 @@ import React, { useEffect } from "react"; import { ExternalLink as A } from "@/components/ExternalLink"; import { useAccount } from "@/lib/hooks/useAccount"; +import { useToasts } from "@/lib/hooks/useToasts"; import { useServices } from "@/services"; import { QuickStartHeader } from "./QuickStartHeader"; @@ -69,14 +70,14 @@ const classes = { export const VerifyEmailStep: React.FC = () => { const { authService } = useServices(); - const setServerError = useAccount((store) => store.setServerError); + const { showError } = useToasts(); const user = useAccount((store) => store.user); const emailVerificationSent = useAccount((store) => store.emailVerificationSent); const setEmailVerificationSent = useAccount((store) => store.setEmailVerificationSent); const handleCheckVerification = async () => { authService.refreshUser().catch((err) => { - setServerError(err.message); + showError(err.message); }); }; @@ -86,14 +87,14 @@ export const VerifyEmailStep: React.FC = () => { await authService.sendVerificationEmail(); setEmailVerificationSent(true); } catch (err: any) { - setServerError(err.message); + showError(err.message); } }; if (user && !user.emailVerified && !emailVerificationSent) { void sendVerificationEmail(); } - }, [emailVerificationSent, setEmailVerificationSent, setServerError, user, authService]); + }, [emailVerificationSent, setEmailVerificationSent, showError, user, authService]); const preVerification = ( <> @@ -104,7 +105,7 @@ export const VerifyEmailStep: React.FC = () => { Check Verification
- Not finding email?{" "} + Can't find the email? Check your spam folder. Still missing?{" "} send again diff --git a/src/renderer/containers/QuickStart/index.tsx b/src/renderer/containers/QuickStart/index.tsx index 80673704f..57f1370b3 100644 --- a/src/renderer/containers/QuickStart/index.tsx +++ b/src/renderer/containers/QuickStart/index.tsx @@ -10,6 +10,7 @@ import { useMousetrap } from "@/lib/hooks/useMousetrap"; import { QuickStartStep } from "@/lib/hooks/useQuickStart"; import { platformTitleBarStyles } from "@/styles/platformTitleBarStyles"; +import { AcceptRulesStep } from "./AcceptRulesStep"; import { ActivateOnlineStep } from "./ActivateOnlineStep"; import { ImportDolphinSettingsStep } from "./ImportDolphinSettingsStep"; import { IsoSelectionStep } from "./IsoSelectionStep"; @@ -39,6 +40,8 @@ const getStepContent = (step: QuickStartStep | null) => { return ; case QuickStartStep.VERIFY_EMAIL: return ; + case QuickStartStep.ACCEPT_RULES: + return ; case QuickStartStep.ACTIVATE_ONLINE: return ; case QuickStartStep.MIGRATE_DOLPHIN: diff --git a/src/renderer/containers/SpectatePage/ShareGameplayBlock/index.tsx b/src/renderer/containers/SpectatePage/ShareGameplayBlock/index.tsx index 87c76d784..29a5c37e8 100644 --- a/src/renderer/containers/SpectatePage/ShareGameplayBlock/index.tsx +++ b/src/renderer/containers/SpectatePage/ShareGameplayBlock/index.tsx @@ -16,7 +16,7 @@ const ip = "127.0.0.1"; const port = Ports.DEFAULT; export const ShareGameplayBlock: React.FC<{ className?: string }> = ({ className }) => { - const playKey = useAccount((store) => store.playKey); + const userData = useAccount((store) => store.userData); const startTime = useConsole((store) => store.startTime); const endTime = useConsole((store) => store.endTime); const slippiStatus = useConsole((store) => store.slippiConnectionStatus); @@ -38,7 +38,7 @@ export const ShareGameplayBlock: React.FC<{ className?: string }> = ({ className ip, port, viewerId, - name: playKey ? playKey.connectCode : undefined, + name: userData?.playKey ? userData.playKey.connectCode : undefined, }); } catch (err) { log.error(err); diff --git a/src/renderer/lib/hooks/useAccount.ts b/src/renderer/lib/hooks/useAccount.ts index 613052ee1..8782e1664 100644 --- a/src/renderer/lib/hooks/useAccount.ts +++ b/src/renderer/lib/hooks/useAccount.ts @@ -1,17 +1,17 @@ -import type { PlayKey } from "@dolphin/types"; import { useCallback } from "react"; import create from "zustand"; import { combine } from "zustand/middleware"; import { useServices } from "@/services"; import type { AuthUser } from "@/services/auth/types"; +import type { UserData } from "@/services/slippi/types"; export const useAccount = create( combine( { user: null as AuthUser | null, loading: false, - playKey: null as PlayKey | null, + userData: null as UserData | null, serverError: false, displayName: "", emailVerificationSent: false, @@ -33,7 +33,7 @@ export const useAccount = create( set({ user, displayName, emailVerificationSent }); }, setLoading: (loading: boolean) => set({ loading }), - setPlayKey: (playKey: PlayKey | null) => set({ playKey }), + setUserData: (userData: UserData | null) => set({ userData }), setServerError: (serverError: boolean) => set({ serverError }), setDisplayName: (displayName: string) => set({ displayName }), setEmailVerificationSent: (emailVerificationSent: boolean) => set({ emailVerificationSent }), @@ -41,14 +41,14 @@ export const useAccount = create( ), ); -export const usePlayKey = () => { +export const useUserData = () => { const { slippiBackendService } = useServices(); const loading = useAccount((store) => store.loading); const setLoading = useAccount((store) => store.setLoading); - const setPlayKey = useAccount((store) => store.setPlayKey); + const setUserData = useAccount((store) => store.setUserData); const setServerError = useAccount((store) => store.setServerError); - const refreshPlayKey = useCallback(async () => { + const refreshUserData = useCallback(async () => { // We're already refreshing the key if (loading) { return; @@ -56,18 +56,18 @@ export const usePlayKey = () => { setLoading(true); await slippiBackendService - .fetchPlayKey() - .then((playKey) => { - setPlayKey(playKey); + .fetchUserData() + .then((userData) => { + setUserData(userData); setServerError(false); }) .catch((err) => { console.warn("Error fetching play key: ", err); - setPlayKey(null); + setUserData(null); setServerError(true); }) .finally(() => setLoading(false)); - }, [loading, setLoading, setPlayKey, setServerError, slippiBackendService]); + }, [loading, setLoading, setUserData, setServerError, slippiBackendService]); - return refreshPlayKey; + return refreshUserData; }; diff --git a/src/renderer/lib/hooks/useApp.ts b/src/renderer/lib/hooks/useApp.ts index 34a428685..6e986811a 100644 --- a/src/renderer/lib/hooks/useApp.ts +++ b/src/renderer/lib/hooks/useApp.ts @@ -37,7 +37,7 @@ export const useAppInitialization = () => { const initialized = useAppStore((store) => store.initialized); const setInitializing = useAppStore((store) => store.setInitializing); const setInitialized = useAppStore((store) => store.setInitialized); - const setPlayKey = useAccount((store) => store.setPlayKey); + const setUserData = useAccount((store) => store.setUserData); const setUser = useAccount((store) => store.setUser); const setServerError = useAccount((store) => store.setServerError); const setDesktopAppExists = useDesktopApp((store) => store.setExists); @@ -67,9 +67,9 @@ export const useAppInitialization = () => { if (user) { try { - const key = await slippiBackendService.fetchPlayKey(); + const userData = await slippiBackendService.fetchUserData(); setServerError(false); - setPlayKey(key); + setUserData(userData); } catch (err) { setServerError(true); log.warn(err); diff --git a/src/renderer/lib/hooks/useAppListeners.ts b/src/renderer/lib/hooks/useAppListeners.ts index e296d4dcb..0dabc3567 100644 --- a/src/renderer/lib/hooks/useAppListeners.ts +++ b/src/renderer/lib/hooks/useAppListeners.ts @@ -10,7 +10,7 @@ import { useToasts } from "@/lib/hooks/useToasts"; import { useServices } from "@/services"; import { useDolphinListeners } from "../dolphin/useDolphinListeners"; -import { useAccount, usePlayKey } from "./useAccount"; +import { useAccount, useUserData } from "./useAccount"; import { useBroadcast } from "./useBroadcast"; import { useBroadcastList, useBroadcastListStore } from "./useBroadcastList"; import { useConsoleDiscoveryStore } from "./useConsoleDiscovery"; @@ -34,7 +34,7 @@ export const useAppListeners = () => { // Subscribe to user auth changes to keep store up to date const setUser = useAccount((store) => store.setUser); - const refreshPlayKey = usePlayKey(); + const refreshUserData = useUserData(); React.useEffect(() => { // Only start subscribing to user change events after we've finished initializing if (!initialized) { @@ -46,7 +46,7 @@ export const useAppListeners = () => { setUser(user); // Refresh the play key - void refreshPlayKey(); + void refreshUserData(); }); // Unsubscribe on unmount @@ -56,7 +56,7 @@ export const useAppListeners = () => { } return; - }, [initialized, refreshPlayKey, setUser, authService]); + }, [initialized, refreshUserData, setUser, authService]); const setSlippiConnectionStatus = useConsole((store) => store.setSlippiConnectionStatus); React.useEffect(() => { diff --git a/src/renderer/lib/hooks/useQuickStart.ts b/src/renderer/lib/hooks/useQuickStart.ts index 49b4c8739..f86cf170f 100644 --- a/src/renderer/lib/hooks/useQuickStart.ts +++ b/src/renderer/lib/hooks/useQuickStart.ts @@ -1,3 +1,4 @@ +import { currentRulesVersion } from "@common/constants"; import React from "react"; import { useNavigate } from "react-router-dom"; import create from "zustand"; @@ -10,6 +11,7 @@ import { useAccount } from "./useAccount"; export enum QuickStartStep { LOGIN = "LOGIN", VERIFY_EMAIL = "VERIFY_EMAIL", + ACCEPT_RULES = "ACCEPT_RULES", MIGRATE_DOLPHIN = "MIGRATE_DOLPHIN", ACTIVATE_ONLINE = "ACTIVATE_ONLINE", SET_ISO_PATH = "SET_ISO_PATH", @@ -21,6 +23,7 @@ function generateSteps( hasUser: boolean; hasPlayKey: boolean; hasVerifiedEmail: boolean; + showRules: boolean; serverError: boolean; hasIso: boolean; hasOldDesktopApp: boolean; @@ -41,7 +44,11 @@ function generateSteps( steps.unshift(QuickStartStep.ACTIVATE_ONLINE); } - if (!options.hasVerifiedEmail) { + if (options.showRules && !options.serverError) { + steps.unshift(QuickStartStep.ACCEPT_RULES); + } + + if (!options.hasVerifiedEmail && !options.serverError) { steps.unshift(QuickStartStep.VERIFY_EMAIL); } @@ -56,14 +63,15 @@ export const useQuickStart = () => { const navigate = useNavigate(); const savedIsoPath = useSettings((store) => store.settings.isoPath); const user = useAccount((store) => store.user); - const playKey = useAccount((store) => store.playKey); + const userData = useAccount((store) => store.userData); const serverError = useAccount((store) => store.serverError); const desktopAppPathExists = useDesktopApp((store) => store.exists); const options = { hasUser: Boolean(user), hasIso: Boolean(savedIsoPath), hasVerifiedEmail: Boolean(user?.emailVerified), - hasPlayKey: Boolean(playKey), + hasPlayKey: Boolean(userData?.playKey), + showRules: Boolean((userData?.rulesAccepted ?? 0) < currentRulesVersion), serverError: Boolean(serverError), hasOldDesktopApp: desktopAppPathExists, }; @@ -90,7 +98,11 @@ export const useQuickStart = () => { stepToShow = QuickStartStep.ACTIVATE_ONLINE; } - if (!options.hasVerifiedEmail) { + if (options.showRules && !options.serverError) { + stepToShow = QuickStartStep.ACCEPT_RULES; + } + + if (!options.hasVerifiedEmail && !options.serverError) { stepToShow = QuickStartStep.VERIFY_EMAIL; } @@ -100,14 +112,15 @@ export const useQuickStart = () => { setCurrentStep(stepToShow); }, [ - history, steps, options.hasIso, options.hasVerifiedEmail, options.hasOldDesktopApp, options.hasPlayKey, options.hasUser, + options.showRules, options.serverError, + navigate, ]); const nextStep = () => { diff --git a/src/renderer/services/slippi/graphqlEndpoints.ts b/src/renderer/services/slippi/graphqlEndpoints.ts index 5a89bb6e1..15daf4b5a 100644 --- a/src/renderer/services/slippi/graphqlEndpoints.ts +++ b/src/renderer/services/slippi/graphqlEndpoints.ts @@ -15,6 +15,7 @@ type User = { connectCode: Nullable; displayName: Nullable; fbUid: string; + rulesAccepted: number; private: Nullable; }; @@ -36,9 +37,9 @@ export const QUERY_VALIDATE_USER_ID: TypedDocumentNode< } `; -export const QUERY_GET_USER_KEY: TypedDocumentNode< +export const QUERY_GET_USER_DATA: TypedDocumentNode< { - getUser: Nullable>; + getUser: Nullable>; getLatestDolphin: Nullable>; }, { @@ -54,6 +55,7 @@ export const QUERY_GET_USER_KEY: TypedDocumentNode< private { playKey } + rulesAccepted } getLatestDolphin { version @@ -74,6 +76,19 @@ export const MUTATION_RENAME_USER: TypedDocumentNode< } `; +export const MUTATION_ACCEPT_RULES: TypedDocumentNode< + { + userAcceptRules: Nullable>; + }, + { num: number } +> = gql` + mutation AcceptRules($num: Int!) { + userAcceptRules(num: $num) { + rulesAccepted + } + } +`; + export const MUTATION_INIT_NETPLAY: TypedDocumentNode< { userInitNetplay: Nullable>; diff --git a/src/renderer/services/slippi/slippi.service.mock.ts b/src/renderer/services/slippi/slippi.service.mock.ts index ec7f4e1e8..6ccaf0069 100644 --- a/src/renderer/services/slippi/slippi.service.mock.ts +++ b/src/renderer/services/slippi/slippi.service.mock.ts @@ -2,16 +2,19 @@ import type { PlayKey } from "@dolphin/types"; import type { AuthService } from "../auth/types"; import { delayAndMaybeError } from "../utils"; -import type { SlippiBackendService } from "./types"; +import type { SlippiBackendService, UserData } from "./types"; const SHOULD_ERROR = false; -const fakeUsers: PlayKey[] = [ +const fakeUsers: UserData[] = [ { - uid: "userid", - connectCode: "DEMO#000", - playKey: "playkey", - displayName: "Demo user", + playKey: { + uid: "userid", + connectCode: "DEMO#000", + playKey: "playkey", + displayName: "Demo user", + }, + rulesAccepted: 0, }, ]; @@ -20,25 +23,25 @@ class MockSlippiBackendClient implements SlippiBackendService { @delayAndMaybeError(SHOULD_ERROR) public async validateUserId(userId: string): Promise<{ displayName: string; connectCode: string }> { - const key = fakeUsers.find((key) => key.uid === userId); - if (!key) { + const userData = fakeUsers.find((userData) => userData.playKey?.uid === userId); + if (!userData || !userData.playKey) { throw new Error("No user with that ID"); } return { - displayName: key.displayName, - connectCode: key.connectCode, + displayName: userData.playKey.displayName, + connectCode: userData.playKey.connectCode, }; } @delayAndMaybeError(SHOULD_ERROR) - public async fetchPlayKey(): Promise { + public async fetchUserData(): Promise { const user = this.authService.getCurrentUser(); if (!user) { throw new Error("No user logged in"); } - const key = fakeUsers.find((key) => key.uid === user.uid); - return key ?? null; + const userData = fakeUsers.find((userData) => userData.playKey?.uid === user.uid); + return userData ?? null; } @delayAndMaybeError(SHOULD_ERROR) @@ -56,6 +59,11 @@ class MockSlippiBackendClient implements SlippiBackendService { await this.authService.updateDisplayName(name); } + @delayAndMaybeError(SHOULD_ERROR) + public async acceptRules() { + // TODO: make it possible to accept the rules in the mock + } + @delayAndMaybeError(SHOULD_ERROR) public async initializeNetplay(_codeStart: string): Promise { // Do nothing diff --git a/src/renderer/services/slippi/slippi.service.ts b/src/renderer/services/slippi/slippi.service.ts index fe03a47ba..53bcb3db2 100644 --- a/src/renderer/services/slippi/slippi.service.ts +++ b/src/renderer/services/slippi/slippi.service.ts @@ -3,17 +3,19 @@ import { ApolloClient, ApolloLink, HttpLink, InMemoryCache } from "@apollo/clien import { setContext } from "@apollo/client/link/context"; import { onError } from "@apollo/client/link/error"; import { RetryLink } from "@apollo/client/link/retry"; +import { currentRulesVersion } from "@common/constants"; import type { DolphinService, PlayKey } from "@dolphin/types"; import type { GraphQLError } from "graphql"; import type { AuthService } from "../auth/types"; import { + MUTATION_ACCEPT_RULES, MUTATION_INIT_NETPLAY, MUTATION_RENAME_USER, - QUERY_GET_USER_KEY, + QUERY_GET_USER_DATA, QUERY_VALIDATE_USER_ID, } from "./graphqlEndpoints"; -import type { SlippiBackendService } from "./types"; +import type { SlippiBackendService, UserData } from "./types"; const log = window.electron.log; const SLIPPI_BACKEND_URL = process.env.SLIPPI_GRAPHQL_ENDPOINT; @@ -104,14 +106,14 @@ class SlippiBackendClient implements SlippiBackendService { throw new Error("No user with that ID"); } - public async fetchPlayKey(): Promise { + public async fetchUserData(): Promise { const user = this.authService.getCurrentUser(); if (!user) { throw new Error("User is not logged in"); } const res = await this.client.query({ - query: QUERY_GET_USER_KEY, + query: QUERY_GET_USER_DATA, variables: { fbUid: user.uid, }, @@ -123,18 +125,23 @@ class SlippiBackendClient implements SlippiBackendService { const connectCode = res.data.getUser?.connectCode?.code; const playKey = res.data.getUser?.private?.playKey; const displayName = res.data.getUser?.displayName || ""; - if (!connectCode || !playKey) { - // If we don't have a connect code or play key, return this as null such that logic that - // handles it will cause the user to set them up. - return null; + + // If we don't have a connect code or play key, return it as null such that logic that + // handles it will cause the user to set them up. + let playKeyObj: PlayKey | null = null; + if (connectCode && playKey) { + playKeyObj = { + uid: user.uid, + connectCode, + playKey, + displayName, + latestVersion: res.data.getLatestDolphin?.version, + }; } return { - uid: user.uid, - connectCode, - playKey, - displayName, - latestVersion: res.data.getLatestDolphin?.version, + playKey: playKeyObj, + rulesAccepted: res.data.getUser?.rulesAccepted ?? 0, }; } public async assertPlayKey(playKey: PlayKey) { @@ -170,6 +177,24 @@ class SlippiBackendClient implements SlippiBackendService { await this.authService.updateDisplayName(name); } + public async acceptRules() { + const user = this.authService.getCurrentUser(); + if (!user) { + throw new Error("User is not logged in"); + } + + const res = await this.client.mutate({ + mutation: MUTATION_ACCEPT_RULES, + variables: { num: currentRulesVersion }, + }); + + handleErrors(res.errors); + + if (res.data?.userAcceptRules?.rulesAccepted !== currentRulesVersion) { + throw new Error("Could not accept rules"); + } + } + public async initializeNetplay(codeStart: string): Promise { const res = await this.client.mutate({ mutation: MUTATION_INIT_NETPLAY, variables: { codeStart } }); handleErrors(res.errors); diff --git a/src/renderer/services/slippi/types.ts b/src/renderer/services/slippi/types.ts index 88d85ad77..3a9d831e5 100644 --- a/src/renderer/services/slippi/types.ts +++ b/src/renderer/services/slippi/types.ts @@ -1,10 +1,16 @@ import type { PlayKey } from "@dolphin/types"; +export interface UserData { + playKey: PlayKey | null; + rulesAccepted: number; +} + export interface SlippiBackendService { validateUserId(userId: string): Promise<{ displayName: string; connectCode: string }>; - fetchPlayKey(): Promise; + fetchUserData(): Promise; assertPlayKey(playKey: PlayKey): Promise; deletePlayKey(): Promise; changeDisplayName(name: string): Promise; + acceptRules(): Promise; initializeNetplay(codeStart: string): Promise; }