90 lines
10 KiB
TypeScript
90 lines
10 KiB
TypeScript
// `storebound console plan play-games-services`: the Play Games Services setup has no API (the games project, its
|
|
// credentials, publishing). Only the leaderboards and achievements do (`storebound play games`). Agent-first like the
|
|
// other Play plans: the agent drives Play Console and Google Cloud console with its own browser tool, stops before
|
|
// Publish (the owner's button), then `console verify play-games-services --from-text -`. Pure.
|
|
// Modeled on Google's help ("Set up Play Games Services", "Create a credential", "Publish your game's changes"),
|
|
// not yet verified on a real console: the page paths and labels are where to start, the agent adapts.
|
|
// App signing SHA-1: the Play Developer API has no read of the app signing certificate SHA-1 (generatedapks.list only
|
|
// gives certificateSha256Hash), so the plan points at Play Console > App integrity > Play app signing.
|
|
import type { ShipConfig } from './config';
|
|
import { consoleUrl, PLAY_CONSOLE } from './console-links';
|
|
import { NEVER_PRESS, PLAY_NEVER_PRESS } from './console-forms';
|
|
import type { PlanAnswer, PlanIds } from './console-plan';
|
|
import { type Part, PLAY_MATERIAL_FACT } from './console-play-plans';
|
|
import type { VerifyRow } from './console-verify';
|
|
import { onStore } from './game-services';
|
|
|
|
export const GOOGLE_CLOUD_CREDENTIALS = 'https://console.cloud.google.com/apis/credentials';
|
|
export const GOOGLE_CLOUD_CONSENT = 'https://console.cloud.google.com/apis/credentials/consent';
|
|
export const PGS_PUBLISH = 'Publish';
|
|
|
|
const ids = (c: ShipConfig, p: PlanIds) => ({ playDeveloperId: p.playDeveloperId ?? c.play?.developerId, playAppId: p.playAppId ?? c.play?.appId });
|
|
|
|
export function playGamesPart(config: ShipConfig, p: PlanIds): Part {
|
|
const pkg = config.play?.packageName ?? '(play.packageName)';
|
|
const i = ids(config, p);
|
|
const exact = !!(i.playDeveloperId && i.playAppId);
|
|
const setup = consoleUrl('play.games-services', i), signing = consoleUrl('play.app-signing', i);
|
|
const g = config.games;
|
|
const pid = g?.playGames?.appId;
|
|
const ach = (g?.achievements ?? []).filter(a => onStore(a, 'play')).length;
|
|
const lb = (g?.leaderboards ?? []).filter(l => onStore(l, 'play')).length;
|
|
const a = (id: string, question: string, value: string | null, extra: Partial<PlanAnswer> = {}): PlanAnswer =>
|
|
({ id, question, value, text: value ?? '(you decide)', status: value === null ? 'ask-owner' : 'certain', source: 'storebound.config.ts', ...extra });
|
|
const answers: PlanAnswer[] = [
|
|
a('project', 'Games project', pid ? `the existing project ${pid}` : 'a new one ("No, my game doesn\'t use Google APIs")', { where: 'Setup: the first question', control: 'radio', action: 'pick' }),
|
|
a('displayName', 'Display name', config.play?.name ?? config.app.name, { where: 'Properties', control: 'text', action: 'type' }),
|
|
a('package', 'Android credential: package name', pkg, { where: 'Credentials > Add credential > Android', control: 'select', action: 'choose' }),
|
|
a('sha1', 'OAuth client SHA-1', 'the App signing key certificate SHA-1 from Play app signing (not the upload key)', { where: `Google Cloud > Credentials > OAuth client ID (Android): ${signing}`, control: 'text', action: 'type' }),
|
|
a('webClient', 'Web OAuth client (server auth codes)', null, { note: 'Only when a game server exchanges server auth codes (requestServerSideAccess). Ask the owner; with no server, skip it.', where: 'Credentials > Add credential > Game server' }),
|
|
];
|
|
return {
|
|
title: 'Play Console + Google Cloud: Play Games Services setup (games project, credentials, publish)',
|
|
start: { url: setup, deepLink: exact, reach: exact ? [] : ['Pick the app (set play.developerId and play.appId for a deep link)', 'Grow users > Play Games Services > Setup and management > Configuration'] },
|
|
steps: [
|
|
{ n: 1, text: `Open ${setup}. No games project yet: answer "No, my game doesn't use Google APIs" and press Create (a draft project; nothing is public). With games.playGames.appId set, check the page shows that project id.`, click: ['Create'], answers: ['project'] },
|
|
{ n: 2, text: 'Properties: Edit properties > display name (and the description / graphics the owner gave) > Save as draft.', click: ['Edit properties', 'Save as draft'], answers: ['displayName'] },
|
|
{ n: 3, text: `Google Cloud OAuth consent screen (once per Cloud project): ${GOOGLE_CLOUD_CONSENT}. If it is not configured, set it up as External with the app name and the support email; never press "Publish app" there without the owner.` },
|
|
{ n: 4, text: `App signing SHA-1: open ${signing} and copy "SHA-1 certificate fingerprint" of the App signing key certificate (Play re-signs installs with it). The upload key's SHA-1 is only for builds you install yourself (an optional second credential).` },
|
|
{ n: 5, text: `Credentials > Add credential > Android: pick ${pkg}; it links to an OAuth client in Google Cloud. Create it at ${GOOGLE_CLOUD_CREDENTIALS} > Create credentials > OAuth client ID > Android (package ${pkg}, the SHA-1 of step 4) > Create, then back in Play Console pick that client > Save changes.`, click: ['Add credential', 'Save changes'], answers: ['package', 'sha1'] },
|
|
{ n: 6, text: `Only with a game server (ask-owner): Add credential > Game server: create a Web application OAuth client at ${GOOGLE_CLOUD_CREDENTIALS}, pick it, Save changes. Its client id goes into the game's requestServerSideAccess; the client secret stays on the server (never in chat or files here).`, answers: ['webClient'] },
|
|
{ n: 7, text: 'Copy the project id (Configuration page, "Project ID" / Get resources): write it to games.playGames.appId, and put the resources XML into android/app/src/main/res/values/games-ids.xml. `storebound check` then compares it with the manifest\'s com.google.android.gms.games.APP_ID.' },
|
|
{ n: 8, text: `Leaderboards (${lb}) and achievements (${ach}) come from the config: run \`storebound play games --dry\`, then without --dry (the Configuration API; drafts only). If it answers 403 SERVICE_DISABLED / accessNotConfigured, the Google Play Game Services Publishing API is off in the service account's Cloud project: open https://console.cloud.google.com/apis/library/gamesconfiguration.googleapis.com?project=<project id of the service account key> (the error names the project), press Enable, wait a minute, run it again. Play needs at least 5 achievements before it publishes the games project.` },
|
|
{ n: 9, text: 'Testers: Play Games Services > Testers lists who can sign in before publishing (the owner adds them; storebound never invites testers).' },
|
|
{ n: 10, text: `Stop before "${PGS_PUBLISH}" (Review and publish): the owner presses it. Then read the page text and pipe it to \`storebound console verify play-games-services --from-text -\`.` },
|
|
],
|
|
answers,
|
|
final: PGS_PUBLISH,
|
|
neverPress: [...new Set([PGS_PUBLISH, 'Publish changes', 'Publish app', 'Delete', 'Unlink', ...PLAY_NEVER_PRESS, ...NEVER_PRESS])],
|
|
allowed: [
|
|
{ button: 'Create', class: 'navigation', where: 'Setup: creates the draft games project (not public)' },
|
|
{ button: 'Edit properties / Add credential / Create credentials', class: 'navigation', where: 'Play Console and Google Cloud console' },
|
|
{ button: 'Save as draft / Save changes', class: 'draft-save', where: 'Play Games Services pages: drafts until the owner publishes' },
|
|
{ button: 'Create (OAuth client ID)', class: 'draft-save', where: `${GOOGLE_CLOUD_CREDENTIALS}: an OAuth client (private to the Cloud project)` },
|
|
],
|
|
uiFacts: [
|
|
'Modeled on Google\'s Play Games Services help (not verified on a real console yet): Grow users > Play Games Services > Setup and management > Configuration; Credentials and Properties are sections of that page; Review and publish has the Publish button.',
|
|
'The Android credential needs the SHA-1 of the key that signs what players install: with Play app signing that is the App signing key certificate (Test and release > App integrity > Play app signing), not the upload key. The Play Developer API gives only SHA-256 hashes (generatedapks.list), so the SHA-1 is read from that page.',
|
|
'A games project links one Google Cloud project; linking a different one later is not possible from the page: check the project id before Create.',
|
|
PLAY_MATERIAL_FACT,
|
|
`Absolute pages: ${PLAY_CONSOLE} (Play Console), ${GOOGLE_CLOUD_CREDENTIALS} (OAuth clients), ${GOOGLE_CLOUD_CONSENT} (consent screen).`,
|
|
],
|
|
verify: { command: 'storebound console verify play-games-services --from-text -', source: 'page-text', capture: 'document.querySelector("main")?.innerText ?? document.body.innerText',
|
|
expect: ['The Configuration page shows the project id of games.playGames.appId', `An Android credential for ${pkg}`, 'The status is ready to publish or published'] },
|
|
guidance: ['Open: Grow users > Play Games Services > Setup and management > Configuration', `Then: ${GOOGLE_CLOUD_CREDENTIALS} for the OAuth clients`],
|
|
};
|
|
}
|
|
|
|
/** Page text of the Configuration page vs the config. */
|
|
export function verifyPlayGames(config: ShipConfig, text: string): VerifyRow[] {
|
|
const rows: VerifyRow[] = [];
|
|
const pid = config.games?.playGames?.appId;
|
|
const pkg = config.play?.packageName;
|
|
if (pid) rows.push({ item: 'project id', expected: pid, seen: text.includes(pid) ? pid : '(not on the page)', status: text.includes(pid) ? 'match' : 'mismatch' });
|
|
else rows.push({ item: 'project id', expected: 'games.playGames.appId', seen: (/\b\d{10,14}\b/.exec(text) ?? ['?'])[0], status: 'ask-owner', note: 'set games.playGames.appId to the project id shown on the page' });
|
|
if (pkg) rows.push({ item: 'Android credential', expected: pkg, seen: text.includes(pkg) ? pkg : '(no credential with the package)', status: text.includes(pkg) ? 'match' : 'mismatch' });
|
|
const published = /\bPublished\b/i.test(text) && !/not published|unpublished/i.test(text);
|
|
const ready = /ready to publish|ready for publishing/i.test(text);
|
|
rows.push({ item: 'status', expected: 'ready to publish or published', seen: published ? 'published' : ready ? 'ready to publish' : '(neither)', status: published || ready ? 'match' : 'mismatch', ...(ready && !published ? { note: 'the owner presses Publish' } : {}) });
|
|
return rows;
|
|
}
|