// Typed wrappers around `GalaxyClient.executeCommand` for the user- // account command catalog. Each wrapper builds a FlatBuffers request // payload via the generated TS bindings, calls `executeCommand`, then // decodes the `AccountResponse` reply. Errors with a non-`ok` // `result_code` surface as a thrown `AccountError` carrying the // canonical backend code (`invalid_request`, `subject_not_found`, // `forbidden`, `conflict`, `internal_error`). import { Builder, ByteBuffer } from "flatbuffers"; import type { GalaxyClient } from "./galaxy-client"; import { AccountResponse, AccountView, ErrorResponse as FbsErrorResponse, GetMyAccountRequest, UpdateMyProfileRequest, UpdateMySettingsRequest, } from "../proto/galaxy/fbs/user"; const RESULT_CODE_OK = "ok"; export class AccountError extends Error { readonly code: string; readonly resultCode: string; constructor(resultCode: string, code: string, message: string) { super(message); this.name = "AccountError"; this.resultCode = resultCode; this.code = code; } } export interface Account { userId: string; email: string; userName: string; displayName: string; preferredLanguage: string; timeZone: string; declaredCountry: string; entitlement: AccountEntitlement; } // AccountEntitlement is the narrow view of the FBS EntitlementSnapshot // the UI currently consumes. `isPaid` gates lobby affordances tied to // the paid tier (F8-04b: private-games subpage + create-game button). // Other snapshot fields (plan code, expiry timestamps) are intentionally // omitted until a feature needs them. export interface AccountEntitlement { isPaid: boolean; } export async function getMyAccount(client: GalaxyClient): Promise { const builder = new Builder(32); GetMyAccountRequest.startGetMyAccountRequest(builder); builder.finish(GetMyAccountRequest.endGetMyAccountRequest(builder)); const payload = await execute(client, "user.account.get", builder.asUint8Array()); return decodeAccountResponse(payload); } export async function updateMyProfile( client: GalaxyClient, displayName: string, ): Promise { const builder = new Builder(128); const displayNameOff = builder.createString(displayName); UpdateMyProfileRequest.startUpdateMyProfileRequest(builder); UpdateMyProfileRequest.addDisplayName(builder, displayNameOff); builder.finish(UpdateMyProfileRequest.endUpdateMyProfileRequest(builder)); const payload = await execute(client, "user.profile.update", builder.asUint8Array()); return decodeAccountResponse(payload); } export async function updateMySettings( client: GalaxyClient, preferredLanguage: string, timeZone: string, ): Promise { const builder = new Builder(128); const preferredLanguageOff = builder.createString(preferredLanguage); const timeZoneOff = builder.createString(timeZone); UpdateMySettingsRequest.startUpdateMySettingsRequest(builder); UpdateMySettingsRequest.addPreferredLanguage(builder, preferredLanguageOff); UpdateMySettingsRequest.addTimeZone(builder, timeZoneOff); builder.finish(UpdateMySettingsRequest.endUpdateMySettingsRequest(builder)); const payload = await execute(client, "user.settings.update", builder.asUint8Array()); return decodeAccountResponse(payload); } async function execute( client: GalaxyClient, messageType: string, payloadBytes: Uint8Array, ): Promise { const result = await client.executeCommand(messageType, payloadBytes); if (result.resultCode !== RESULT_CODE_OK) { throw decodeAccountError(result.resultCode, result.payloadBytes); } return result.payloadBytes; } function decodeAccountError(resultCode: string, payload: Uint8Array): AccountError { let code = resultCode; let message = resultCode; try { const errorResponse = FbsErrorResponse.getRootAsErrorResponse(new ByteBuffer(payload)); const body = errorResponse.error(); if (body) { code = body.code() ?? resultCode; message = body.message() ?? resultCode; } } catch (_err) { // fall through with the raw result code } return new AccountError(resultCode, code, message); } function decodeAccountResponse(payload: Uint8Array): Account { if (payload.length === 0) { throw new AccountError("internal_error", "internal_error", "empty account response"); } const response = AccountResponse.getRootAsAccountResponse(new ByteBuffer(payload)); const view = response.account(); if (view === null) { throw new AccountError("internal_error", "internal_error", "account missing in response"); } return decodeAccountView(view); } // Exported for unit tests that build a synthetic AccountView via the // FBS bindings and assert the resulting Account shape. Runtime callers // reach the same decode path through `getMyAccount` / `updateMyProfile` // / `updateMySettings`. export function decodeAccountView(view: AccountView): Account { const entitlement = view.entitlement(); return { userId: view.userId() ?? "", email: view.email() ?? "", userName: view.userName() ?? "", displayName: view.displayName() ?? "", preferredLanguage: view.preferredLanguage() ?? "", timeZone: view.timeZone() ?? "", declaredCountry: view.declaredCountry() ?? "", entitlement: { isPaid: entitlement?.isPaid() ?? false, }, }; }