diff --git a/src/client/AccountModal.ts b/src/client/AccountModal.ts
index d757f5136..cfc17f5ff 100644
--- a/src/client/AccountModal.ts
+++ b/src/client/AccountModal.ts
@@ -4,8 +4,13 @@ import { ClientEnv } from "src/client/ClientEnv";
import { PlayerStatsTree, UserMeResponse } from "../core/ApiSchemas";
import { assetUrl } from "../core/AssetUrls";
import { Cosmetics } from "../core/CosmeticSchemas";
-import { fetchPlayerById, getUserMe } from "./Api";
-import { discordLogin, logOut, sendMagicLink } from "./Auth";
+import { fetchPlayerById, getUserMe, invalidateUserMe } from "./Api";
+import {
+ discordLogin,
+ logOut,
+ reauthAfterCrazyGamesChange,
+ sendMagicLink,
+} from "./Auth";
import "./components/baseComponents/stats/DiscordUserHeader";
import "./components/baseComponents/stats/PlayerGameHistoryView";
import type { PlayerGameHistoryCache } from "./components/baseComponents/stats/PlayerGameHistoryView";
@@ -19,6 +24,7 @@ import "./components/FriendsList";
import "./components/SubscriptionPanel";
import { modalHeader } from "./components/ui/ModalHeader";
import { fetchCosmetics, SUBSCRIPTIONS_ENABLED } from "./Cosmetics";
+import { crazyGamesSDK, type CrazyGamesUser } from "./CrazyGamesSDK";
import { translateText } from "./Utils";
@customElement("account-modal")
@@ -27,6 +33,9 @@ export class AccountModal extends BaseModal {
@state() private email: string = "";
@state() private isLoadingUser: boolean = false;
+ // Set on CrazyGames when a CrazyGames user is signed in. Their identity comes
+ // from the SDK, not our backend user object.
+ @state() private crazyGamesUser: CrazyGamesUser | null = null;
private userMeResponse: UserMeResponse | null = null;
private cosmetics: Cosmetics | null = null;
@@ -38,6 +47,9 @@ export class AccountModal extends BaseModal {
super();
document.addEventListener("userMeResponse", (event: Event) => {
+ // A CrazyGames sign-in fires userMeResponse (via Main's auth listener);
+ // re-fetch the SDK profile so the modal leaves the sign-in screen.
+ this.refreshCrazyGamesUser();
const customEvent = event as CustomEvent;
if (customEvent.detail) {
const previousPublicId = this.userMeResponse?.player?.publicId;
@@ -58,6 +70,16 @@ export class AccountModal extends BaseModal {
});
}
+ // Refresh the signed-in CrazyGames identity from the SDK. No-op off
+ // CrazyGames; drives isLinkedAccount() so the modal shows the profile.
+ private refreshCrazyGamesUser() {
+ if (!crazyGamesSDK.isOnCrazyGames()) return;
+ void crazyGamesSDK.getUserProfile().then((user) => {
+ this.crazyGamesUser = user;
+ this.requestUpdate();
+ });
+ }
+
private hasAnyStats(): boolean {
if (!this.statsTree) return false;
// Check if statsTree has any data
@@ -99,7 +121,13 @@ export class AccountModal extends BaseModal {
private isLinkedAccount(): boolean {
const me = this.userMeResponse?.user;
- return !!(me?.discord ?? me?.email);
+ // The CrazyGames identity only counts once the backend token exchange
+ // produced a session — otherwise a failed exchange would show a dead
+ // "connected as" view with no way to retry.
+ return (
+ !!(me?.discord ?? me?.email) ||
+ (!!this.crazyGamesUser && this.userMeResponse !== null)
+ );
}
protected modalConfig() {
@@ -124,7 +152,9 @@ export class AccountModal extends BaseModal {
}
if (!this.isLinkedAccount()) {
return html`
- ${this.renderLoginOptions()}
+ ${crazyGamesSDK.isOnCrazyGames()
+ ? this.renderCrazyGamesSignIn()
+ : this.renderLoginOptions()}
`;
}
return html`
@@ -153,6 +183,9 @@ export class AccountModal extends BaseModal {
}
private renderAccountTab(): TemplateResult {
+ if (this.crazyGamesUser) {
+ return this.renderCrazyGamesAccount(this.crazyGamesUser);
+ }
return html`
@@ -175,6 +208,59 @@ export class AccountModal extends BaseModal {
`;
}
+ // CrazyGames "connected as" view: avatar + username from the SDK, plus
+ // currency/subscription. No Discord/Google/email link or logout (CrazyGames
+ // owns the account and its logout).
+ private renderCrazyGamesAccount(user: CrazyGamesUser): TemplateResult {
+ return html`
+
+
+
+
+ ${translateText("account_modal.connected_as")}
+
+
+

+
${user.username}
+ ${this.renderCurrency()}
+
+
+
+ ${this.renderSubscriptionPanel()}
+
+ `;
+ }
+
+ // Shown when a CrazyGames guest opens the modal: hand off to CrazyGames' own
+ // sign-in prompt (no Discord/Google/email on CrazyGames).
+ private renderCrazyGamesSignIn(): TemplateResult {
+ return html`
+
+
+
+ ${translateText("account_modal.sign_in_desc")}
+
+
+
+
+ `;
+ }
+
private renderStatsTab(): TemplateResult {
if (!this.hasAnyStats()) {
return this.renderEmptyState(
@@ -418,6 +504,20 @@ export class AccountModal extends BaseModal {
}
}
+ // CrazyGames sign-in: after their prompt completes, exchange the new token
+ // for a session and refresh the modal so it shows the signed-in profile.
+ private async handleCrazyGamesSignIn() {
+ await crazyGamesSDK.showAuthPrompt();
+ const profile = await crazyGamesSDK.getUserProfile();
+ if (!profile) return; // prompt cancelled / still not signed in
+ invalidateUserMe();
+ await reauthAfterCrazyGamesChange();
+ const userMe = await getUserMe();
+ if (userMe) this.userMeResponse = userMe;
+ this.crazyGamesUser = profile;
+ this.requestUpdate();
+ }
+
private handleDiscordLogin() {
discordLogin();
}
@@ -425,6 +525,8 @@ export class AccountModal extends BaseModal {
protected onOpen(): void {
this.isLoadingUser = true;
+ this.refreshCrazyGamesUser();
+
if (SUBSCRIPTIONS_ENABLED) {
void fetchCosmetics().then((cosmetics) => {
this.cosmetics = cosmetics;
diff --git a/src/client/Auth.ts b/src/client/Auth.ts
index be0899146..5f2fd8632 100644
--- a/src/client/Auth.ts
+++ b/src/client/Auth.ts
@@ -4,6 +4,7 @@ import { z } from "zod";
import { TokenPayload, TokenPayloadSchema } from "../core/ApiSchemas";
import { base64urlToUuid } from "../core/Base64";
import { getApiBase, getAudience } from "./Api";
+import { crazyGamesSDK } from "./CrazyGamesSDK";
import { generateCryptoRandomUUID } from "./Utils";
export type UserAuth = { jwt: string; claims: TokenPayload } | false;
@@ -154,6 +155,15 @@ async function refreshJwt(): Promise
{
}
async function doRefreshJwt(): Promise {
+ if (crazyGamesSDK.isOnCrazyGames()) {
+ const token = await crazyGamesSDK.getUserToken();
+ if (token) {
+ // Signed-in CrazyGames account: exchange their token for our session.
+ // No CrazyGames account / not signed in falls through to the guest flow
+ // below.
+ return doCrazyGamesLogin(token);
+ }
+ }
try {
console.log("Refreshing jwt");
const response = await fetch(getApiBase() + "/auth/refresh", {
@@ -178,6 +188,56 @@ async function doRefreshJwt(): Promise {
}
}
+// Exchange a CrazyGames user token for our session. On CrazyGames the refresh
+// cookie isn't usable (SameSite=Lax, cross-site iframe), so we re-exchange on
+// expiry instead of hitting /auth/refresh.
+async function doCrazyGamesLogin(token: string): Promise {
+ try {
+ console.log("Logging in with CrazyGames");
+ const response = await fetch(getApiBase() + "/auth/crazygames", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ token }),
+ });
+ if (response.status !== 200) {
+ console.error("CrazyGames login failed", response);
+ __jwt = null;
+ return;
+ }
+ const json = await response.json();
+ const { jwt, expiresIn } = json;
+ __expiresAt = Date.now() + expiresIn * 1000;
+ console.log("CrazyGames login succeeded");
+ __jwt = jwt;
+ } catch (e) {
+ console.error("CrazyGames login failed", e);
+ __jwt = null;
+ }
+}
+
+// Called when the CrazyGames auth state changes mid-session (e.g. the player
+// signs in): drop the cached session so userAuth() re-exchanges the new token.
+// Single-flight: Main's auth listener and the account modal's sign-in handler
+// can both react to the same sign-in; sharing one exchange keeps them from
+// racing on __jwt. Any refresh already in flight is allowed to settle first so
+// its stale result can't satisfy the reauth.
+let __reauthPromise: Promise | null = null;
+export async function reauthAfterCrazyGamesChange(): Promise {
+ __reauthPromise ??= (async () => {
+ try {
+ if (__refreshPromise) {
+ await __refreshPromise.catch(() => {});
+ }
+ __jwt = null;
+ __expiresAt = 0;
+ return await userAuth();
+ } finally {
+ __reauthPromise = null;
+ }
+ })();
+ return __reauthPromise;
+}
+
export async function sendMagicLink(email: string): Promise {
try {
const apiBase = getApiBase();
diff --git a/src/client/CrazyGamesAccountButton.ts b/src/client/CrazyGamesAccountButton.ts
new file mode 100644
index 000000000..60a9c68ce
--- /dev/null
+++ b/src/client/CrazyGamesAccountButton.ts
@@ -0,0 +1,102 @@
+import { crazyGamesSDK } from "./CrazyGamesSDK";
+import { closeMobileSidebar } from "./Navigation";
+import { translateText } from "./Utils";
+
+// On CrazyGames the player's identity comes from the CrazyGames SDK, not our
+// backend user object. Show their avatar + username when signed in (clicking
+// opens the account modal), or a "Sign in" affordance that opens CrazyGames'
+// own auth prompt when they're a guest. Applies to every account entry point:
+// the desktop nav pill, the mobile hamburger item, and the homepage top bar
+// (which layout is visible depends on viewport width).
+export async function updateCrazyGamesNavButton() {
+ if (!crazyGamesSDK.isOnCrazyGames()) return;
+ const profile = await crazyGamesSDK.getUserProfile();
+ const signInText = translateText("main.sign_in");
+
+ // Bypass the data-page router (which would open the account modal) and hand
+ // off to CrazyGames' own sign-in prompt instead. stopPropagation also skips
+ // the router's sidebar cleanup, so close it ourselves.
+ const promptSignIn = (e: Event) => {
+ e.stopPropagation();
+ e.preventDefault();
+ closeMobileSidebar();
+ void crazyGamesSDK.showAuthPrompt();
+ };
+
+ // Desktop nav pill: avatar + person icon + text.
+ const desktopButton = document.getElementById(
+ "nav-account-button",
+ ) as HTMLButtonElement | null;
+ const avatarEl = document.getElementById(
+ "nav-account-avatar",
+ ) as HTMLImageElement | null;
+ const personIconEl = document.getElementById("nav-account-person-icon");
+ // CrazyGames accounts have no email, so the email badge is always hidden.
+ document.getElementById("nav-account-email-badge")?.classList.add("hidden");
+ const signInTextEl = document.getElementById("nav-account-signin-text");
+ if (profile) {
+ if (avatarEl) {
+ avatarEl.alt = profile.username;
+ avatarEl.src = profile.profilePictureUrl;
+ avatarEl.classList.remove("hidden");
+ }
+ personIconEl?.classList.add("hidden");
+ if (signInTextEl) {
+ // The translation pass rewrites every [data-i18n] element's text, which
+ // would clobber the username — drop the attribute while it holds one.
+ signInTextEl.removeAttribute("data-i18n");
+ signInTextEl.textContent = profile.username;
+ signInTextEl.classList.remove("hidden");
+ }
+ desktopButton?.classList.remove("border", "border-white/20");
+ if (desktopButton) desktopButton.onclick = null;
+ } else {
+ avatarEl?.classList.add("hidden");
+ personIconEl?.classList.remove("hidden");
+ if (signInTextEl) {
+ // Restore so language changes keep the label translated.
+ signInTextEl.setAttribute("data-i18n", "main.sign_in");
+ signInTextEl.textContent = signInText;
+ signInTextEl.classList.remove("hidden");
+ }
+ desktopButton?.classList.add("border", "border-white/20");
+ if (desktopButton) desktopButton.onclick = promptSignIn;
+ }
+
+ // Mobile hamburger menu item: text only. Same data-i18n handling as above.
+ const mobileButton = document.getElementById(
+ "mobile-nav-account-button",
+ ) as HTMLButtonElement | null;
+ if (mobileButton) {
+ if (profile) {
+ mobileButton.removeAttribute("data-i18n");
+ mobileButton.textContent = profile.username;
+ } else {
+ mobileButton.setAttribute("data-i18n", "main.sign_in");
+ mobileButton.textContent = signInText;
+ }
+ mobileButton.onclick = profile ? null : promptSignIn;
+ }
+
+ // Homepage top bar (narrow layout): avatar or person icon only.
+ const topBarButton = document.getElementById(
+ "crazygames-account-btn",
+ ) as HTMLButtonElement | null;
+ const topBarAvatar = document.getElementById(
+ "crazygames-account-avatar",
+ ) as HTMLImageElement | null;
+ const topBarIcon = document.getElementById("crazygames-account-icon");
+ if (profile) {
+ if (topBarAvatar) {
+ topBarAvatar.alt = profile.username;
+ topBarAvatar.src = profile.profilePictureUrl;
+ topBarAvatar.classList.remove("hidden");
+ }
+ topBarIcon?.classList.add("hidden");
+ if (topBarButton) topBarButton.onclick = null;
+ } else {
+ topBarAvatar?.classList.add("hidden");
+ topBarIcon?.classList.remove("hidden");
+ if (topBarButton) topBarButton.onclick = promptSignIn;
+ }
+}
diff --git a/src/client/CrazyGamesSDK.ts b/src/client/CrazyGamesSDK.ts
index ae2d5bf1d..5c938fcdd 100644
--- a/src/client/CrazyGamesSDK.ts
+++ b/src/client/CrazyGamesSDK.ts
@@ -1,18 +1,20 @@
+export interface CrazyGamesUser {
+ username: string;
+ profilePictureUrl: string;
+}
+
declare global {
interface Window {
CrazyGames?: {
SDK: {
init: () => Promise;
user: {
- getUser(): Promise<{
- username: string;
- } | null>;
+ isUserAccountAvailable: boolean;
+ getUser(): Promise;
+ getUserToken(): Promise;
+ showAuthPrompt(): Promise;
addAuthListener: (
- listener: (
- user: {
- username: string;
- } | null,
- ) => void,
+ listener: (user: CrazyGamesUser | null) => void,
) => void;
};
ad: {
@@ -58,8 +60,10 @@ declare global {
export class CrazyGamesSDK {
private initialized = false;
private isGameplayActive = false;
- private readyPromise: Promise;
- private resolveReady!: () => void;
+ // Resolves true once the SDK initialized, false once init definitively
+ // failed (not on CrazyGames, SDK never loaded, init threw).
+ private readyPromise: Promise;
+ private resolveReady!: (ready: boolean) => void;
constructor() {
this.readyPromise = new Promise((resolve) => {
@@ -72,9 +76,17 @@ export class CrazyGamesSDK {
setTimeout(() => resolve(false), 3000);
});
- const ready = this.readyPromise.then(() => true);
+ return Promise.race([this.readyPromise, timeout]);
+ }
- return Promise.race([ready, timeout]);
+ // Like ready() but without the 3s cap: waits for maybeInit() to actually
+ // finish (SDK load alone can take ~10s on a slow network). Use this for
+ // auth-critical calls, where a premature false logs the player out.
+ private whenReady(): Promise {
+ if (!this.isOnCrazyGames()) {
+ return Promise.resolve(false);
+ }
+ return this.readyPromise;
}
isOnCrazyGames(): boolean {
@@ -110,6 +122,7 @@ export class CrazyGamesSDK {
if (!this.isOnCrazyGames()) {
console.log("Not running on CrazyGames platform, not initializing SDK");
+ this.resolveReady(false);
return;
}
@@ -122,16 +135,18 @@ export class CrazyGamesSDK {
if (typeof window.CrazyGames === "undefined") {
console.warn("CrazyGames SDK not available");
+ this.resolveReady(false);
return;
}
try {
await window.CrazyGames.SDK.init();
this.initialized = true;
- this.resolveReady();
+ this.resolveReady(true);
console.log("CrazyGames SDK initialized");
} catch (error) {
console.error("Failed to initialize CrazyGames SDK:", error);
+ this.resolveReady(false);
}
}
@@ -148,14 +163,55 @@ export class CrazyGamesSDK {
}
}
+ // Returns a fresh CrazyGames-signed user token to exchange with our backend,
+ // or null if accounts aren't available here or no user is signed in.
+ // CrazyGames recommends fetching this fresh each time rather than caching it.
+ async getUserToken(): Promise {
+ if (!(await this.whenReady())) {
+ return null;
+ }
+ try {
+ if (!window.CrazyGames!.SDK.user.isUserAccountAvailable) {
+ return null;
+ }
+ return await window.CrazyGames!.SDK.user.getUserToken();
+ } catch (e) {
+ console.log("error getting CrazyGames user token: ", e);
+ return null;
+ }
+ }
+
+ // Returns the signed-in CrazyGames user (username + avatar), or null if
+ // accounts aren't available here or no user is signed in.
+ async getUserProfile(): Promise {
+ if (!(await this.whenReady())) {
+ return null;
+ }
+ try {
+ return await window.CrazyGames!.SDK.user.getUser();
+ } catch (e) {
+ console.log("error getting CrazyGames user: ", e);
+ return null;
+ }
+ }
+
+ // Opens CrazyGames' own sign-in prompt. On success the auth listener fires,
+ // which drives our re-auth. Resolves regardless of outcome (e.g. cancelled).
+ async showAuthPrompt(): Promise {
+ if (!(await this.whenReady())) {
+ return;
+ }
+ try {
+ await window.CrazyGames!.SDK.user.showAuthPrompt();
+ } catch (e) {
+ console.log("CrazyGames auth prompt dismissed: ", e);
+ }
+ }
+
async addAuthListener(
- listener: (
- user: {
- username: string;
- } | null,
- ) => void,
+ listener: (user: CrazyGamesUser | null) => void,
): Promise {
- if (!(await this.ready())) {
+ if (!(await this.whenReady())) {
return;
}
diff --git a/src/client/GameModeSelector.ts b/src/client/GameModeSelector.ts
index b1dc74cb0..66dca4536 100644
--- a/src/client/GameModeSelector.ts
+++ b/src/client/GameModeSelector.ts
@@ -11,7 +11,6 @@ import {
} from "../core/game/Game";
import { PublicGameInfo, PublicGames } from "../core/Schemas";
import "./components/IOSAddToHomeScreenBanner";
-import { crazyGamesSDK } from "./CrazyGamesSDK";
import { HostLobbyModal } from "./HostLobbyModal";
import { JoinLobbyModal } from "./JoinLobbyModal";
import { PublicLobbySocket } from "./LobbySocket";
@@ -145,13 +144,11 @@ export class GameModeSelector extends LitElement {
this.openHostLobby,
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-action-card-hover)]",
)}
- ${!crazyGamesSDK.isOnCrazyGames()
- ? this.renderSmallActionCard(
- translateText("mode_selector.ranked_title"),
- this.openRankedMenu,
- "bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-action-card-hover)]",
- )
- : html``}
+ ${this.renderSmallActionCard(
+ translateText("mode_selector.ranked_title"),
+ this.openRankedMenu,
+ "bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-action-card-hover)]",
+ )}
${this.renderSmallActionCard(
translateText("main.join"),
this.openJoinLobby,
@@ -159,7 +156,9 @@ export class GameModeSelector extends LitElement {
)}
-
+
${this.lobbies === null
@@ -225,13 +224,11 @@ export class GameModeSelector extends LitElement {
this.openHostLobby,
"bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-action-card-hover)]",
)}
- ${!crazyGamesSDK.isOnCrazyGames()
- ? this.renderSmallActionCard(
- translateText("mode_selector.ranked_title"),
- this.openRankedMenu,
- "bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-action-card-hover)]",
- )
- : html`
`}
+ ${this.renderSmallActionCard(
+ translateText("mode_selector.ranked_title"),
+ this.openRankedMenu,
+ "bg-surface hover:brightness-[1.08] active:brightness-[0.95] hover:scale-105 hover:shadow-[var(--shadow-action-card-hover)]",
+ )}
${this.renderSmallActionCard(
translateText("main.join"),
this.openJoinLobby,
diff --git a/src/client/Main.ts b/src/client/Main.ts
index 9601ea3b2..352510500 100644
--- a/src/client/Main.ts
+++ b/src/client/Main.ts
@@ -15,10 +15,11 @@ import { GameType } from "../core/game/Game";
import { UserSettings } from "../core/game/UserSettings";
import "./AccountModal";
import { getUserMe, invalidateUserMe } from "./Api";
-import { userAuth } from "./Auth";
+import { reauthAfterCrazyGamesChange, userAuth } from "./Auth";
import "./ClanModal";
import { joinLobby, type JoinLobbyResult } from "./ClientGameRunner";
import { getPlayerCosmeticsRefs } from "./Cosmetics";
+import { updateCrazyGamesNavButton } from "./CrazyGamesAccountButton";
import { crazyGamesSDK } from "./CrazyGamesSDK";
import "./FlagInput";
import { FlagInput } from "./FlagInput";
@@ -477,7 +478,11 @@ class Client {
}
const onUserMe = async (userMeResponse: UserMeResponse | false) => {
- updateAccountNavButton(userMeResponse);
+ if (crazyGamesSDK.isOnCrazyGames()) {
+ void updateCrazyGamesNavButton();
+ } else {
+ updateAccountNavButton(userMeResponse);
+ }
const isAdFree =
userMeResponse !== false && userMeResponse.player?.adfree === true;
window.adsEnabled = !isAdFree && !crazyGamesSDK.isOnCrazyGames();
@@ -507,6 +512,15 @@ class Client {
getUserMe().then(onUserMe);
}
+ // Re-run auth when the player signs into CrazyGames mid-session. Logout
+ // reloads the page, so only login needs handling here.
+ crazyGamesSDK.addAuthListener(() => {
+ invalidateUserMe();
+ reauthAfterCrazyGamesChange().then((result) =>
+ result === false ? onUserMe(false) : getUserMe().then(onUserMe),
+ );
+ });
+
const settingsModal = document.querySelector(
"user-setting",
) as UserSettingModal;
@@ -1065,6 +1079,10 @@ const bootstrap = () => {
// Also hide elements after a short delay to catch late-rendered components
setTimeout(hideCrazyGamesElements, 100);
setTimeout(hideCrazyGamesElements, 500);
+
+ // Populate the CrazyGames account buttons once the nav/top-bar have rendered
+ // (onUserMe also refreshes them after auth and on mid-session sign-in).
+ setTimeout(() => void updateCrazyGamesNavButton(), 500);
};
if (document.readyState === "loading") {
diff --git a/src/client/Matchmaking.ts b/src/client/Matchmaking.ts
index 146a6cf1e..0447df5f5 100644
--- a/src/client/Matchmaking.ts
+++ b/src/client/Matchmaking.ts
@@ -7,6 +7,7 @@ import { getPlayToken } from "./Auth";
import { BaseModal } from "./components/BaseModal";
import "./components/Difficulties";
import { modalHeader } from "./components/ui/ModalHeader";
+import { crazyGamesSDK } from "./CrazyGamesSDK";
import { JoinLobbyEvent } from "./Main";
import { translateText } from "./Utils";
@@ -119,11 +120,20 @@ export class MatchmakingModal extends BaseModal {
return;
}
- const isLoggedIn =
- userMe &&
- userMe.user &&
- (userMe.user.discord !== undefined || userMe.user.email !== undefined);
- if (!isLoggedIn) {
+ // CrazyGames players authenticate through the SDK rather than a linked
+ // Discord/email account, so a signed-in CrazyGames user counts as
+ // logged in for ranked.
+ const crazyGamesSignedIn =
+ crazyGamesSDK.isOnCrazyGames() &&
+ (await crazyGamesSDK.getUserProfile()) !== null;
+ if (!this.isModalOpen) {
+ return;
+ }
+
+ if (
+ userMe === false ||
+ (!hasLinkedAccount(userMe) && !crazyGamesSignedIn)
+ ) {
window.dispatchEvent(
new CustomEvent("show-message", {
detail: {
diff --git a/src/client/Navigation.ts b/src/client/Navigation.ts
index 7878155dd..c4185db53 100644
--- a/src/client/Navigation.ts
+++ b/src/client/Navigation.ts
@@ -1,18 +1,18 @@
-export function initNavigation() {
- const closeMobileSidebar = () => {
- const sidebar = document.getElementById("sidebar-menu");
- const backdrop = document.getElementById("mobile-menu-backdrop");
- if (sidebar?.classList.contains("open")) {
- sidebar.classList.remove("open");
- backdrop?.classList.remove("open");
- document.documentElement.classList.remove("overflow-hidden");
- sidebar.setAttribute("aria-hidden", "true");
- backdrop?.setAttribute("aria-hidden", "true");
- const hb = document.getElementById("hamburger-btn");
- if (hb) hb.setAttribute("aria-expanded", "false");
- }
- };
+export function closeMobileSidebar() {
+ const sidebar = document.getElementById("sidebar-menu");
+ const backdrop = document.getElementById("mobile-menu-backdrop");
+ if (sidebar?.classList.contains("open")) {
+ sidebar.classList.remove("open");
+ backdrop?.classList.remove("open");
+ document.documentElement.classList.remove("overflow-hidden");
+ sidebar.setAttribute("aria-hidden", "true");
+ backdrop?.setAttribute("aria-hidden", "true");
+ const hb = document.getElementById("hamburger-btn");
+ if (hb) hb.setAttribute("aria-expanded", "false");
+ }
+}
+export function initNavigation() {
const showPage = (pageId: string) => {
window.currentPageId = pageId;
diff --git a/src/client/components/DesktopNavBar.ts b/src/client/components/DesktopNavBar.ts
index 5786c8057..69dc271c1 100644
--- a/src/client/components/DesktopNavBar.ts
+++ b/src/client/components/DesktopNavBar.ts
@@ -148,14 +148,14 @@ export class DesktopNavBar extends LitElement {