An error occurred. The application does not have a user.
;
+ }
+
+ return (
+
- 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;
}