desktop: Game services card in App info (Game Center and Play Games Services status, ids, counts vs config, pending changes, sync buttons through the confirm-token write, PGS setup plan, console links); en + tr strings; dev fixture
This commit is contained in:
parent
26f0723641
commit
22286c7e02
6 changed files with 135 additions and 2 deletions
|
|
@ -423,6 +423,13 @@ const TOOLS = [
|
|||
];
|
||||
let chromeInstalled = false;
|
||||
|
||||
// games status (cli/games-status.ts) of the game demo: Game Center on, one leaderboard to create; PGS project set
|
||||
const GAMES_PRISM = {
|
||||
apple: { capability: true, enabled: true, version: '1.2.0', versionEnabled: true, leaderboards: 1, achievements: 5, config: { leaderboards: 2, achievements: 5 }, changes: ['leaderboard com.example.prismpuddle.daily: create'] },
|
||||
play: { appId: '123456789012', resource: '123456789012 (android/app/src/main/res/values/games-ids.xml)', leaderboards: 2, achievements: 5, config: { leaderboards: 2, achievements: 5 }, changes: [] },
|
||||
findings: [{ store: 'apple', level: 'ok', id: 'gc.entitlement', msg: 'com.apple.developer.game-center in ios/App/App/App.entitlements' }, { store: 'play', level: 'ok', id: 'pgs.app-id', msg: 'games_APP_ID 123456789012 matches the config' }],
|
||||
links: [{ label: 'Play Console: Play Games Services configuration', url: 'https://play.google.com/console/u/0/developers/4444/app/9876/play-games-services/setup' }],
|
||||
};
|
||||
const ok = (method: string, data: unknown): ToolResult => ({ method, ok: true, exitCode: 0, dryRun: false, command: `storebound ${method.replace('.', ' ')} --json`, output: '', data });
|
||||
|
||||
const STORE_REVIEWS: Review[] = [
|
||||
|
|
@ -491,6 +498,7 @@ function read(method: string, id: string, args: Record<string, unknown> = {}): T
|
|||
// send for review: Prism Puddle is ready to submit, Tidy Lists is back from App Review (UNRESOLVED_ISSUES)
|
||||
case 'apple.status': return ok(method, id === 'p-tidy' ? APPLE_STATUS_UNRESOLVED : APPLE_STATUS_READY);
|
||||
case 'play.status': return ok(method, PLAY_STATUS);
|
||||
case 'games.status': return ok(method, id === 'p-prism' ? GAMES_PRISM : { apple: null, play: null, findings: [], links: [] });
|
||||
case 'console.plan': return ok(method, consolePlan(String(args.form)));
|
||||
case 'console.verify': return ok(method, consoleVerify(String(args.form)));
|
||||
default:
|
||||
|
|
@ -520,7 +528,7 @@ const CONSOLE_FILL: Record<ConsoleForm, ConsoleFillResult> = {
|
|||
};
|
||||
|
||||
const CACHED_AGO: Record<string, number> = { health: 60 * 3, 'reviews.list': 45, 'apple.testflight_status': 60 * 5, 'catalog.diff': 60 * 26, validate: 60 * 2, 'prices.current': 60 * 4, 'apple.ppo_status': 60 * 6 };
|
||||
const NETWORK = new Set(['health', 'health.issue', 'reviews.list', 'apple.testflight_status', 'catalog.diff', 'validate', 'report', 'prices.current', 'prices.diff', 'apple.ppo_status', 'apple.status', 'play.status', 'console.verify']);
|
||||
const NETWORK = new Set(['games.status', 'health', 'health.issue', 'reviews.list', 'apple.testflight_status', 'catalog.diff', 'validate', 'report', 'prices.current', 'prices.diff', 'apple.ppo_status', 'apple.status', 'play.status', 'console.verify']);
|
||||
|
||||
async function progressLines(job: string | null, lines: string[]) {
|
||||
for (const line of lines) { await sleep(260); if (job) emit('sidecar-progress', { params: { requestId: 1, line, stream: 'stderr' }, job }); }
|
||||
|
|
|
|||
|
|
@ -1841,4 +1841,27 @@ export const en = {
|
|||
'tools.o.ads_change': 'Apple Ads bid, budget or pause',
|
||||
'tools.o.rc_sync': 'Sync the catalog to RevenueCat',
|
||||
'tools.o.shots_login': 'Sign in to the demo account for screenshots (by hand, Enter saves)',
|
||||
'cp.playGames': 'Play Games Services setup',
|
||||
'cp.playGamesText': 'Games project, Android credential with the app signing SHA-1, web OAuth client; you press Publish.',
|
||||
'gs.title': 'Game services',
|
||||
'gs.sub': 'Game Center and Play Games Services: switches, ids, leaderboards and achievements from the config.',
|
||||
'gs.gameCenter': 'Game Center',
|
||||
'gs.playGames': 'Play Games Services',
|
||||
'gs.enabled': 'On for the app',
|
||||
'gs.capability': 'App ID capability',
|
||||
'gs.version': 'Version {v}',
|
||||
'gs.items': 'Leaderboards / achievements',
|
||||
'gs.counts': '{lb} of {lbWant} leaderboards · {ach} of {achWant} achievements',
|
||||
'gs.pending': '{n} change(s) to write',
|
||||
'gs.projectId': 'Project id',
|
||||
'gs.resource': 'games_APP_ID resource',
|
||||
'gs.notSet': 'Not set',
|
||||
'gs.yes': 'Yes',
|
||||
'gs.no': 'No',
|
||||
'gs.unknown': 'Unknown',
|
||||
'gs.syncApple': 'Sync Game Center',
|
||||
'gs.syncPlay': 'Sync Play Games',
|
||||
'gs.syncHint': 'Dry run first, then you confirm; nothing is submitted or published.',
|
||||
'gs.synced': 'Game services in step',
|
||||
'gs.setup': 'Setup plan',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1843,4 +1843,27 @@ export const tr: Record<keyof typeof en, string> = {
|
|||
'tools.o.ads_change': 'Apple Ads teklif, bütçe veya duraklatma',
|
||||
'tools.o.rc_sync': 'Kataloğu RevenueCat ile eşitle',
|
||||
'tools.o.shots_login': 'Ekran görüntüleri için demo hesaba giriş (elle, Enter kaydeder)',
|
||||
'cp.playGames': 'Play Games Hizmetleri kurulumu',
|
||||
'cp.playGamesText': 'Oyun projesi, uygulama imzalama SHA-1 ile Android kimliği, web OAuth istemcisi; Yayınla düğmesine sen basarsın.',
|
||||
'gs.title': 'Oyun hizmetleri',
|
||||
'gs.sub': 'Game Center ve Play Games Hizmetleri: anahtarlar, kimlikler, yapılandırmadaki skor tabloları ve başarımlar.',
|
||||
'gs.gameCenter': 'Game Center',
|
||||
'gs.playGames': 'Play Games Hizmetleri',
|
||||
'gs.enabled': 'Uygulamada açık',
|
||||
'gs.capability': 'App ID yeteneği',
|
||||
'gs.version': 'Sürüm {v}',
|
||||
'gs.items': 'Skor tabloları / başarımlar',
|
||||
'gs.counts': '{lbWant} skor tablosundan {lb} · {achWant} başarımdan {ach}',
|
||||
'gs.pending': 'Yazılacak {n} değişiklik',
|
||||
'gs.projectId': 'Proje kimliği',
|
||||
'gs.resource': 'games_APP_ID kaynağı',
|
||||
'gs.notSet': 'Ayarlı değil',
|
||||
'gs.yes': 'Evet',
|
||||
'gs.no': 'Hayır',
|
||||
'gs.unknown': 'Bilinmiyor',
|
||||
'gs.syncApple': 'Game Center eşitle',
|
||||
'gs.syncPlay': 'Play Games eşitle',
|
||||
'gs.syncHint': 'Önce deneme çalıştırması, sonra sen onaylarsın; hiçbir şey gönderilmez ya da yayınlanmaz.',
|
||||
'gs.synced': 'Oyun hizmetleri eşit',
|
||||
'gs.setup': 'Kurulum planı',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { CONSOLE_FORM_NAME, ConsoleFillButton } from '../../ui/ConsoleFill';
|
|||
import { ConsolePlanButton, PLAN_FORMS } from '../../ui/ConsolePlan';
|
||||
import { Bar, Card, Chip, Empty, ErrorNotice, Notice, Skeleton } from '../../ui/kit';
|
||||
import { WriteButton } from '../../ui/WriteButton';
|
||||
import { GameServicesCard } from '../../ui/GameServicesCard';
|
||||
import { ascUrl } from '../AppView';
|
||||
|
||||
const AREA: Record<DeclAsk['area'], Key> = { dataSafety: 'ai.aDataSafety', appPrivacy: 'ai.aAppPrivacy', appInfo: 'ai.aAppInfo', encryption: 'ai.aEncryption', review: 'ai.aReview' };
|
||||
|
|
@ -237,6 +238,7 @@ export function AppInfoTab({ project }: { project: Project }) {
|
|||
</Card>
|
||||
</div>
|
||||
)}
|
||||
<GameServicesCard project={project} />
|
||||
<ConsoleSteps project={project} stores={d.stores} />
|
||||
|
||||
<p className="small muted">{t('ai.foot', { when: ago(r.at) })}</p>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { Chip, Notice } from './kit';
|
|||
/** The forms of `console plan` (cli/mcp-tools.ts CONSOLE_PLAN_FORMS). */
|
||||
export type PlanForm = 'apple-app-record' | 'apple-age-rating' | 'apple-app-privacy' | 'play-app-record' | 'play-content-rating' | 'play-target-audience'
|
||||
| 'play-store-settings' | 'play-app-content' | 'play-privacy-policy' | 'play-ads' | 'play-sign-in-details' | 'play-advertising-id' | 'play-government-apps'
|
||||
| 'play-financial-features' | 'play-health-apps' | 'play-production-countries' | 'play-production-release';
|
||||
| 'play-financial-features' | 'play-health-apps' | 'play-production-countries' | 'play-production-release' | 'play-games-services';
|
||||
/** `console plan <form> --json` (cli/lib/console-plan.ts ConsolePlan), the parts shown here. */
|
||||
export interface Plan {
|
||||
form: PlanForm; store: 'apple' | 'play'; title: string;
|
||||
|
|
@ -47,6 +47,7 @@ export const PLAN_FORMS: { form: PlanForm; store: 'apple' | 'play'; name: Key; t
|
|||
{ form: 'play-target-audience', store: 'play', name: 'cf.playTargetAudience', text: 'cf.playTargetAudienceText' },
|
||||
{ form: 'play-production-countries', store: 'play', name: 'cp.playCountries', text: 'cp.playCountriesText' },
|
||||
{ form: 'play-production-release', store: 'play', name: 'cp.playProductionRelease', text: 'cp.playProductionReleaseText' },
|
||||
{ form: 'play-games-services', store: 'play', name: 'cp.playGames', text: 'cp.playGamesText' },
|
||||
];
|
||||
const nameOf = (form: PlanForm): string => t(PLAN_FORMS.find(f => f.form === form)?.name ?? 'cp.title');
|
||||
/** Forms whose result `console verify` reads from the App Store Connect API (no page text needed). */
|
||||
|
|
|
|||
76
apps/desktop/src/ui/GameServicesCard.tsx
Normal file
76
apps/desktop/src/ui/GameServicesCard.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// Game services: Game Center (App Store) and Play Games Services (Google Play) side by side, from `games status --json`
|
||||
// (cli/games-status.ts): enabled, App ID capability, entitlement / APP_ID resource, ids, leaderboard and achievement
|
||||
// counts on the store vs the config, pending changes and findings. The sync buttons are the confirm-token writes
|
||||
// (dry run -> native dialog -> apply) of `apple game-center` and `play games`; the PGS setup is a console plan.
|
||||
import type { Project } from '../api';
|
||||
import { t } from '../i18n';
|
||||
import { useRead, useWrite } from '../store';
|
||||
import { ConsolePlanButton } from './ConsolePlan';
|
||||
import { StoreGlyph } from './icons';
|
||||
import { Card, Chip, ErrorNotice, Skeleton } from './kit';
|
||||
import { OpenLink } from './OpenLink';
|
||||
import { WriteButton } from './WriteButton';
|
||||
|
||||
interface Finding { store: 'apple' | 'play'; level: 'error' | 'warn' | 'ok'; id: string; msg: string; fix?: string }
|
||||
interface Counts { leaderboards: number; achievements: number }
|
||||
export interface GameServicesStatus {
|
||||
apple: null | { capability: boolean | null; enabled: boolean; version: string | null; versionEnabled: boolean | null; config: Counts; changes: string[]; error?: string } & Counts;
|
||||
play: null | { appId: string | null; resource: string | null; config: Counts; changes: string[]; error?: string } & Counts;
|
||||
findings: Finding[];
|
||||
links: { label: string; url: string }[];
|
||||
}
|
||||
|
||||
const tone = (l: Finding['level']) => (l === 'error' ? 'bad' : l === 'warn' ? 'warn' : 'ok');
|
||||
const yesNo = (v: boolean | null | undefined) => (v === null || v === undefined ? <Chip tone="neutral">{t('gs.unknown')}</Chip> : <Chip tone={v ? 'ok' : 'bad'}>{v ? t('gs.yes') : t('gs.no')}</Chip>);
|
||||
|
||||
export function GameServicesCard({ project }: { project: Project }) {
|
||||
const r = useRead<GameServicesStatus>(project.id, 'games.status', { network: true, title: t('gs.title') });
|
||||
const w = useWrite(project.id);
|
||||
if (!r.data) return r.error ? <ErrorNotice error={r.error} tone="warn" /> : <Card title={t('gs.title')} icon="sparkle"><Skeleton lines={3} block /></Card>;
|
||||
const s = r.data;
|
||||
if (!s.apple && !s.play && !s.findings.length) return null;
|
||||
const f = (store: 'apple' | 'play') => s.findings.filter(x => x.store === store);
|
||||
const count = (have: Counts, want: Counts) => t('gs.counts', { lb: have.leaderboards, lbWant: want.leaderboards, ach: have.achievements, achWant: want.achievements });
|
||||
return (
|
||||
<Card title={t('gs.title')} icon="sparkle" sub={t('gs.sub')}>
|
||||
<div className="grid g2" style={{ alignItems: 'start' }}>
|
||||
{s.apple && (
|
||||
<div className="stack" style={{ gap: 8 }}>
|
||||
<div className="row" style={{ gap: 6 }}><StoreGlyph store="ios" className="ic13" /><strong>{t('gs.gameCenter')}</strong></div>
|
||||
{s.apple.error ? <span className="small muted">{s.apple.error}</span> : (
|
||||
<dl className="kv">
|
||||
<dt>{t('gs.enabled')}</dt><dd>{yesNo(s.apple.enabled)}</dd>
|
||||
<dt>{t('gs.capability')}</dt><dd>{yesNo(s.apple.capability)}</dd>
|
||||
<dt>{t('gs.version', { v: s.apple.version ?? '-' })}</dt><dd>{yesNo(s.apple.versionEnabled)}</dd>
|
||||
<dt>{t('gs.items')}</dt><dd className="small">{count(s.apple, s.apple.config)}</dd>
|
||||
</dl>
|
||||
)}
|
||||
{f('apple').map(x => <Chip key={x.id} tone={tone(x.level)} title={x.fix}>{x.msg}</Chip>)}
|
||||
{!!s.apple.changes.length && <span className="small muted">{t('gs.pending', { n: s.apple.changes.length })}: {s.apple.changes.slice(0, 4).join('; ')}</span>}
|
||||
<div className="row" style={{ gap: 6 }}>
|
||||
<WriteButton run={w} primary icon="upload" w={{ method: 'apple.game_center', args: {}, title: t('gs.syncApple'), done: t('gs.synced'), after: r.refresh }} hint={t('gs.syncHint')}>{t('gs.syncApple')}</WriteButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{s.play && (
|
||||
<div className="stack" style={{ gap: 8 }}>
|
||||
<div className="row" style={{ gap: 6 }}><StoreGlyph store="android" className="ic13" /><strong>{t('gs.playGames')}</strong></div>
|
||||
<dl className="kv">
|
||||
<dt>{t('gs.projectId')}</dt><dd>{s.play.appId ? <span className="mono-id">{s.play.appId}</span> : <Chip tone="warn">{t('gs.notSet')}</Chip>}</dd>
|
||||
<dt>{t('gs.resource')}</dt><dd>{s.play.resource ? <span className="mono-id small">{s.play.resource}</span> : <Chip tone="warn">{t('gs.notSet')}</Chip>}</dd>
|
||||
{!s.play.error && <><dt>{t('gs.items')}</dt><dd className="small">{count(s.play, s.play.config)}</dd></>}
|
||||
</dl>
|
||||
{s.play.error && <span className="small muted">{s.play.error}</span>}
|
||||
{f('play').map(x => <Chip key={x.id} tone={tone(x.level)} title={x.fix}>{x.msg}</Chip>)}
|
||||
{!!s.play.changes.length && <span className="small muted">{t('gs.pending', { n: s.play.changes.length })}: {s.play.changes.slice(0, 4).join('; ')}</span>}
|
||||
<div className="row" style={{ gap: 6 }}>
|
||||
<WriteButton run={w} primary icon="upload" disabled={!s.play.appId} w={{ method: 'play.games', args: {}, title: t('gs.syncPlay'), done: t('gs.synced'), after: r.refresh }} hint={t('gs.syncHint')}>{t('gs.syncPlay')}</WriteButton>
|
||||
<ConsolePlanButton project={project} form="play-games-services" label={t('gs.setup')} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!!s.links.length && <div className="row small" style={{ gap: 12, marginTop: 10, flexWrap: 'wrap' }}>{s.links.map(l => <span key={l.url} className="row" style={{ gap: 4 }}>{l.label}<OpenLink url={l.url} /></span>)}</div>}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue