Files
OpenFrontIO/src/client/Api.ts
T
Evan 6c2e0d1528 Fix matchmaking double join bug (#3065)
## Description:

There were several issues with the matchmaking modal:

1. It was defined twice (once before login, and once after login), so
players would sometimes join the matchmaking queue twice.
2. When clicking away from the modal (not clicking the back button), the
"onClose" callback was not triggered. So if a person closed & reopened
the modal, they would join twice'
3. Cache the userMe response so it can be called multiple times

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
2026-01-30 15:27:34 -08:00

188 lines
4.7 KiB
TypeScript

import { z } from "zod";
import {
PlayerProfile,
PlayerProfileSchema,
UserMeResponse,
UserMeResponseSchema,
} from "../core/ApiSchemas";
import { AnalyticsRecord, AnalyticsRecordSchema } from "../core/Schemas";
import { getAuthHeader, logOut, userAuth } from "./Auth";
export async function fetchPlayerById(
playerId: string,
): Promise<PlayerProfile | false> {
try {
const userAuthResult = await userAuth();
if (!userAuthResult) return false;
const { jwt } = userAuthResult;
const url = `${getApiBase()}/player/${encodeURIComponent(playerId)}`;
const res = await fetch(url, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${jwt}`,
},
});
if (res.status !== 200) {
console.warn(
"fetchPlayerById: unexpected status",
res.status,
res.statusText,
);
return false;
}
const json = await res.json();
const parsed = PlayerProfileSchema.safeParse(json);
if (!parsed.success) {
console.warn("fetchPlayerById: Zod validation failed", parsed.error);
return false;
}
return parsed.data;
} catch (err) {
console.warn("fetchPlayerById: request failed", err);
return false;
}
}
let __userMe: Promise<UserMeResponse | false> | null = null;
export async function getUserMe(): Promise<UserMeResponse | false> {
if (__userMe !== null) {
return __userMe;
}
__userMe = (async () => {
try {
const userAuthResult = await userAuth();
if (!userAuthResult) return false;
const { jwt } = userAuthResult;
// Get the user object
const response = await fetch(getApiBase() + "/users/@me", {
headers: {
authorization: `Bearer ${jwt}`,
},
});
if (response.status === 401) {
await logOut();
return false;
}
if (response.status !== 200) return false;
const body = await response.json();
const result = UserMeResponseSchema.safeParse(body);
if (!result.success) {
const error = z.prettifyError(result.error);
console.error("Invalid response", error);
return false;
}
return result.data;
} catch (e) {
return false;
}
})();
return __userMe;
}
export async function createCheckoutSession(
priceId: string,
colorPaletteName: string | null,
): Promise<string | false> {
try {
const response = await fetch(
`${getApiBase()}/stripe/create-checkout-session`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: await getAuthHeader(),
},
body: JSON.stringify({
priceId: priceId,
hostname: window.location.origin,
colorPaletteName: colorPaletteName,
}),
},
);
if (!response.ok) {
console.error(
"createCheckoutSession: request failed",
response.status,
response.statusText,
);
return false;
}
const json = await response.json();
return json.url;
} catch (e) {
console.error("createCheckoutSession: request failed", e);
return false;
}
}
export function getApiBase() {
const domainname = getAudience();
if (domainname === "localhost") {
const apiDomain = process?.env?.API_DOMAIN;
if (apiDomain) {
return `https://${apiDomain}`;
}
return localStorage.getItem("apiHost") ?? "http://localhost:8787";
}
return `https://api.${domainname}`;
}
export function getAudience() {
const { hostname } = new URL(window.location.href);
const domainname = hostname.split(".").slice(-2).join(".");
return domainname;
}
// Check if the user's account is linked to a Discord or email account.
export function hasLinkedAccount(
userMeResponse: UserMeResponse | false,
): boolean {
return (
userMeResponse !== false &&
(userMeResponse.user?.discord !== undefined ||
userMeResponse.user?.email !== undefined)
);
}
export async function fetchGameById(
gameId: string,
): Promise<AnalyticsRecord | false> {
try {
const url = `${getApiBase()}/game/${encodeURIComponent(gameId)}`;
const res = await fetch(url, {
headers: {
Accept: "application/json",
},
});
if (res.status !== 200) {
console.warn(
"fetchGameById: unexpected status",
res.status,
res.statusText,
);
return false;
}
const json = await res.json();
const parsed = AnalyticsRecordSchema.safeParse(json);
if (!parsed.success) {
console.warn("fetchGameById: Zod validation failed", parsed.error);
return false;
}
return parsed.data;
} catch (err) {
console.warn("fetchGameById: request failed", err);
return false;
}
}