Gate public lobby listing on the canCreatePublicLobbies entitlement (#4620)

## What

The API now returns an explicit `canCreatePublicLobbies` boolean on
`/users/@me` (player) and on each subscription tier in `cosmetics.json`.
This PR gates public lobby listing on that entitlement instead of
inferring it from subscription status (`active`/`trialing`), and
advertises the perk on the store's subscription tiles.

## Changes

**Entitlement check**
- `UserMeResponseSchema` gains a required `canCreatePublicLobbies`
boolean (same pattern as `unlimitedRanked`)
- The worker's listing endpoint and `HostLobbyModal`'s Public toggle
both check the boolean directly; the Dev bypass is unchanged
- `hasActiveSubscription()` is removed — these were its only call sites
- The server error stays `subscription_required` (subscribing is still
how you get the entitlement; renaming would ripple through i18n for no
behavior change)

**Store**
- `SubscriptionSchema` (cosmetics catalog) gains a required
`canCreatePublicLobbies` boolean
- Subscription tiles show a "Create public lobbies" badge below
"Unlimited ranked" when the tier grants it (`cosmetics.public_lobbies`
added to en.json)

**Tests**
- Schema tests for both new fields (accepts true, rejects missing,
rejects non-boolean); `UserMeResponse` fixtures updated

## Deploy ordering

Both new schema fields are required, so this client must deploy after
the API serves them (same constraint as `unlimitedRanked` / infra#427).
The API already returns both fields.

## Verified

Typecheck, ESLint, and the affected test files pass (165 tests).
Confirmed the live payloads against a local API: `/users/@me` returns
`player.canCreatePublicLobbies` and both subscription tiers in
`cosmetics.json` carry the field.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Evan
2026-07-15 15:54:01 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 766fda991a
commit a639a6aa6b
10 changed files with 80 additions and 72 deletions
+3 -7
View File
@@ -120,6 +120,9 @@ export const UserMeResponseSchema = z.object({
// True when the player's active subscription tier exempts them from the
// free-ranked-play limits.
unlimitedRanked: z.boolean(),
// True when the player may list a custom lobby publicly. The API decides
// which subscriptions/grants confer this.
canCreatePublicLobbies: z.boolean(),
flares: z.string().array().optional(),
achievements: z.object({
singleplayerMap: z.array(SingleplayerMapAchievementSchema),
@@ -187,13 +190,6 @@ export type UserSubscription = NonNullable<
NonNullable<UserMeResponse["player"]["subscription"]>
>;
// Whether the player currently has subscriber entitlements (e.g. may list a
// private lobby publicly). Trialing counts; past_due/canceled do not.
export function hasActiveSubscription(userMe: UserMeResponse): boolean {
const status = userMe.player.subscription?.status;
return status === "active" || status === "trialing";
}
export const PlayerStatsLeafSchema = z.object({
wins: BigIntStringSchema,
losses: BigIntStringSchema,
+3
View File
@@ -361,6 +361,9 @@ export const SubscriptionSchema = CosmeticSchema.extend({
// Whether this tier exempts subscribers from the free-ranked-play limits
// (advertised on the store tile).
unlimitedRanked: z.boolean(),
// Whether this tier lets subscribers list custom lobbies publicly
// (advertised on the store tile).
canCreatePublicLobbies: z.boolean(),
});
// Schema for resources/cosmetics/cosmetics.json