desktop: five sections by job (Release, Store page, Revenue, Players, Health), Game services view, sidebar by attention
- app view: 12 tabs become 5 sections, each with a header (what it is for, the one next step from state already read) and its sub views in a segmented control; 1-5 jump, Ctrl+Shift+[ ] cycle, Ctrl+K and the context menu list sections and sub views; old tab ids, section and section/sub links all resolve - Players > Game services (games only): both stores, one row per leaderboard / achievement with its languages and each store's state, Game Center locale map, Play API-disabled state with the console plan - games status --json: items, localeMap, playApiDisabled; inspect summary: games - sidebar: apps grouped by attention / in progress / live / not on a store, one status line each, studio filter, workspace as a footer icon row - boot check opens places like a person (section, then sub view)
This commit is contained in:
parent
98cd1d3c92
commit
0ef27cf2e4
22 changed files with 918 additions and 115 deletions
|
|
@ -7,7 +7,8 @@ import { ascClient } from './apple/asc';
|
|||
import { readProjectEntitlements } from './apple/signing';
|
||||
import type { Loaded } from './lib/config';
|
||||
import { consoleUrl } from './lib/console-links';
|
||||
import { type Finding, gamesPrivacyFindings, gcFindings, hasAndroidProject, planGameCenter, planPlayGames, pgsFindings, readPgsProject, usesPlayGames } from './lib/game-services';
|
||||
import { type Action, type Finding, gamesPrivacyFindings, gcFindings, hasAndroidProject, type ItemPlan, liveGcLocales, onStore, planGameCenter, planPlayGames, pgsFindings, readPgsProject, suggestLocaleMap, usesPlayGames } from './lib/game-services';
|
||||
import { parseGoogleError } from './lib/google-errors';
|
||||
import type { Finding as VFinding } from './lib/validate-rules';
|
||||
import { gamesConfigClient, playDefaultLocale, readPlayGames } from './play/games';
|
||||
|
||||
|
|
@ -18,6 +19,33 @@ export interface GameServicesStatus {
|
|||
config: { leaderboards: number; achievements: number }; changes: string[]; error?: string };
|
||||
findings: (Finding & { store: 'apple' | 'play' })[];
|
||||
links: { label: string; url: string }[];
|
||||
/** One row per configured leaderboard / achievement: its texts and where each store stands against the config
|
||||
* (create / update / keep; null = not on that store or that store was not read). */
|
||||
items?: GameItemRow[];
|
||||
/** Game Center locale map: the configured entries and the suggestions from the live locales. */
|
||||
localeMap?: { configured: Record<string, string>; suggested: { id: string; from: string; to: string }[] };
|
||||
/** Play: the Games Configuration API is off in the service account's Cloud project. */
|
||||
playApiDisabled?: { service: string; title: string; url: string } | null;
|
||||
}
|
||||
|
||||
export interface GameItemRow {
|
||||
kind: 'leaderboard' | 'achievement'; id: string; name: string; locales: string[]; points?: number; hidden?: boolean;
|
||||
ios: boolean; android: boolean;
|
||||
apple: { action: Action; changed: string[]; locales: { locale: string; action: Action }[] } | null;
|
||||
play: { action: Action; changed: string[] } | null;
|
||||
}
|
||||
|
||||
/** The per-item rows of the status (pure; plans are null when that store was not read). */
|
||||
export function gameItemRows(g: NonNullable<Loaded['config']['games']>, applePlan: ItemPlan[] | null, playPlan: ItemPlan[] | null): GameItemRow[] {
|
||||
const find = (plan: ItemPlan[] | null, kind: ItemPlan['kind'], id: string) => plan?.find(p => p.kind === kind && p.id === id) ?? null;
|
||||
const row = (kind: ItemPlan['kind'], x: { id: string; locales: Record<string, { name: string }>; platforms?: ('ios' | 'android')[] }, extra: Partial<GameItemRow>): GameItemRow => {
|
||||
const a = find(applePlan, kind, x.id), p = find(playPlan, kind, x.id);
|
||||
return { kind, id: x.id, name: Object.values(x.locales)[0]?.name ?? x.id, locales: Object.keys(x.locales), ...extra,
|
||||
ios: onStore(x, 'apple'), android: onStore(x, 'play'),
|
||||
apple: a ? { action: a.action, changed: a.changed, locales: a.locales.map(l => ({ locale: l.locale, action: l.action })) } : null,
|
||||
play: p ? { action: p.action, changed: p.changed } : null };
|
||||
};
|
||||
return [...(g.leaderboards ?? []).map(l => row('leaderboard', l, {})), ...(g.achievements ?? []).map(a => row('achievement', a, { points: a.points, hidden: !!a.hidden }))];
|
||||
}
|
||||
|
||||
/** Offline findings (check, scan-like): entitlement, APP_ID resource, declarations reminders. */
|
||||
|
|
@ -37,7 +65,8 @@ export async function gameServicesStatus(loaded: Loaded, o: { apple?: boolean; p
|
|||
const { config, root } = loaded;
|
||||
const g = config.games ?? {};
|
||||
const findings = localGameFindings(loaded);
|
||||
const out: GameServicesStatus = { apple: null, play: null, findings, links: [] };
|
||||
const out: GameServicesStatus = { apple: null, play: null, findings, links: [], localeMap: { configured: { ...(g.localeMap ?? {}) }, suggested: [] }, playApiDisabled: null };
|
||||
let applePlan: ItemPlan[] | null = null, playPlan: ItemPlan[] | null = null;
|
||||
const count = (store: 'apple' | 'play') => ({ leaderboards: (g.leaderboards ?? []).filter(x => !x.platforms || x.platforms.includes(store === 'apple' ? 'ios' : 'android')).length,
|
||||
achievements: (g.achievements ?? []).filter(x => !x.platforms || x.platforms.includes(store === 'apple' ? 'ios' : 'android')).length });
|
||||
const gcUse = config.apple ? gameCenterInUse(config, root) : { inUse: false, why: null };
|
||||
|
|
@ -46,8 +75,10 @@ export async function gameServicesStatus(loaded: Loaded, o: { apple?: boolean; p
|
|||
try {
|
||||
const c = await ascClient(root, config.apple.bundleId, config.apple.appId);
|
||||
const s = await readGameCenter(c, config.apple.bundleId);
|
||||
applePlan = planGameCenter(g, config.languages, s, g.localeMap);
|
||||
out.localeMap!.suggested = suggestLocaleMap(config.languages, liveGcLocales(s), g.localeMap ?? {});
|
||||
out.apple = { capability: s.capability, enabled: !!s.detail, version: s.version?.versionString ?? null, versionEnabled: s.version ? s.version.enabled : null,
|
||||
leaderboards: s.leaderboards.length, achievements: s.achievements.length, config: count('apple'), changes: changesOf(planGameCenter(g, config.languages, s, g.localeMap)), why: gcUse.why ?? undefined };
|
||||
leaderboards: s.leaderboards.length, achievements: s.achievements.length, config: count('apple'), changes: changesOf(applePlan), why: gcUse.why ?? undefined };
|
||||
} catch (e) { out.apple = { capability: null, enabled: false, version: null, versionEnabled: null, leaderboards: 0, achievements: 0, config: count('apple'), changes: [], error: (e as Error).message.slice(0, 300) }; }
|
||||
}
|
||||
if (config.play && usesPlayGames(config) && o.play !== false) {
|
||||
|
|
@ -58,9 +89,14 @@ export async function gameServicesStatus(loaded: Loaded, o: { apple?: boolean; p
|
|||
if (!appId) out.play = { ...base, leaderboards: 0, achievements: 0, changes: [], error: 'no games project id yet (storebound console plan play-games-services)' };
|
||||
else try {
|
||||
const have = await readPlayGames(gamesConfigClient(root), appId);
|
||||
out.play = { ...base, leaderboards: have.leaderboards.length, achievements: have.achievements.length, changes: changesOf(planPlayGames(g, config.languages, playDefaultLocale(config), have)) };
|
||||
} catch (e) { out.play = { ...base, leaderboards: 0, achievements: 0, changes: [], error: (e as Error).message.slice(0, 300) }; }
|
||||
playPlan = planPlayGames(g, config.languages, playDefaultLocale(config), have);
|
||||
out.play = { ...base, leaderboards: have.leaderboards.length, achievements: have.achievements.length, changes: changesOf(playPlan) };
|
||||
} catch (e) {
|
||||
const f = parseGoogleError((e as Error).message);
|
||||
if (f?.kind === 'api-disabled') out.playApiDisabled = { service: f.service, title: f.serviceTitle, url: f.activationUrl };
|
||||
out.play = { ...base, leaderboards: 0, achievements: 0, changes: [], error: (e as Error).message.slice(0, 300) }; }
|
||||
}
|
||||
out.items = gameItemRows(g, applePlan, playPlan);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export function summarize(ctx: Ctx): ProjectSummary {
|
|||
variants: Object.keys(config.shots?.variants ?? {}),
|
||||
playReports: !!config.play?.reportsBucket,
|
||||
playRelease: config.play ? { managedPublishing: config.play.managedPublishing ?? null, countries: config.play.countries?.length ?? null, category: playCategoryOf(config)?.category ?? null } : null,
|
||||
games: config.games ? { gameCenter: config.games.gameCenter === true, playGames: !!config.games.playGames, leaderboards: config.games.leaderboards?.length ?? 0, achievements: config.games.achievements?.length ?? 0 } : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ export interface ProjectSummary {
|
|||
/** Play production settings from the config: play.managedPublishing (null: not set, Play's default off), the number
|
||||
* of play.countries (null: all countries / regions) and the App category (play.category or the App Store match). */
|
||||
playRelease?: { managedPublishing: boolean | null; countries: number | null; category: string | null } | null;
|
||||
/** The games section of the config (Game Center / Play Games Services and the item counts), null without one. */
|
||||
games?: { gameCenter: boolean; playGames: boolean; leaderboards: number; achievements: number } | null;
|
||||
}
|
||||
|
||||
// ---- App-centric model (one app = one product on up to two platforms; a workspace holds many apps) ----
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue