Game Center switch before submit: apple submit / resubmit and signing ensure create gameCenterDetails and gameCenterAppVersions when Game Center is in use (config, entitlements or the built IPA), shown in the dry run; validate blocks on either missing; fake 409 BUILD_INDICATES_GAME_CENTER_ENABLED until both exist; pitfall 89
This commit is contained in:
parent
dd9e059036
commit
163e666834
9 changed files with 152 additions and 26 deletions
|
|
@ -5,7 +5,7 @@
|
|||
// find-then-create (createOnce: a conflict or an unclear answer reads the store again, never a second create) or a
|
||||
// PATCH of the fields that differ, so a re-run writes nothing. Nothing is deleted or archived; nothing is submitted
|
||||
// (leaderboards and achievements go to review with the next app version, the owner's Submit for review).
|
||||
import { findBundleId, readProjectEntitlements } from './signing';
|
||||
import { builtIpaEntitlements, findBundleId, readProjectEntitlements } from './signing';
|
||||
import { consoleLink } from '../lib/console-links';
|
||||
import type { Ctx } from '../lib/ctx';
|
||||
import { confirm, need } from '../lib/ctx';
|
||||
|
|
@ -90,21 +90,8 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
console.log(dryPlan('apple game-center (to write)', rows).replace(/^dry run: /, '').replace(/; nothing written/, ''));
|
||||
if (!(await confirm(ctx, 'Write these Game Center changes to App Store Connect? (nothing is submitted; they go to review with the next version)'))) return;
|
||||
|
||||
let detail = s.detail;
|
||||
if (!detail) {
|
||||
const r = await createOnce({ what: 'Game Center detail', tag: TAG, unique: true,
|
||||
create: async () => (await c.asc('POST', '/v1/gameCenterDetails', { data: { type: 'gameCenterDetails', relationships: { app: { data: { type: 'apps', id: appId } } } } })).data.id as string,
|
||||
find: async () => (await c.maybe(`/v1/apps/${appId}/gameCenterDetail`))?.data?.id ?? null });
|
||||
detail = r.value;
|
||||
log(TAG, `Game Center switched on for the app (${detail})`);
|
||||
}
|
||||
if (s.version && !s.version.enabled) {
|
||||
const vid = s.version.id;
|
||||
await createOnce({ what: `Game Center for version ${s.version.versionString}`, tag: TAG, unique: true,
|
||||
create: async () => (await c.asc('POST', '/v1/gameCenterAppVersions', { data: { type: 'gameCenterAppVersions', relationships: { appStoreVersion: { data: { type: 'appStoreVersions', id: vid } } } } })).data.id as string,
|
||||
find: async () => (await c.maybe(`/v1/appStoreVersions/${vid}/gameCenterAppVersion`))?.data?.id ?? null });
|
||||
log(TAG, `Game Center switched on for version ${s.version.versionString}`);
|
||||
}
|
||||
const sw = await ensureGcSwitch(c, { appId, detail: s.detail, version: s.version });
|
||||
const detail = sw.detail;
|
||||
for (const p of plan) {
|
||||
const type = p.kind === 'leaderboard' ? 'gameCenterLeaderboards' as const : 'gameCenterAchievements' as const;
|
||||
const locType = p.kind === 'leaderboard' ? 'gameCenterLeaderboardLocalizations' : 'gameCenterAchievementLocalizations';
|
||||
|
|
@ -139,3 +126,49 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (!after.detail || left.length) warn(TAG, `read back: ${!after.detail ? 'no Game Center detail yet; ' : ''}${left.map(p => `${p.kind} ${p.id}`).join(', ') || ''} not listed as written yet (App Store Connect lists lag after a create): run storebound apple game-center status in a minute`);
|
||||
done(TAG, `Game Center: ${after.leaderboards.length} leaderboard(s), ${after.achievements.length} achievement(s); images and review go with the next version (${consoleLink('asc.version')})`);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ the Game Center switch (submit, signing ensure)
|
||||
// Seen live on a first submission: a build with com.apple.developer.game-center and an app version without Game
|
||||
// Center answer 409 STATE_ERROR.BUILD_INDICATES_GAME_CENTER_ENABLED ("appStoreVersion is missing
|
||||
// gameCenterConfigurations relationship"). What passes: POST /v1/gameCenterDetails {app} when the app has none, then
|
||||
// POST /v1/gameCenterAppVersions {appStoreVersion} (both verified in specs/app-store-connect.oas.json).
|
||||
|
||||
export interface GcSwitch { appId: string; detail: string | null; version: { id: string; versionString: string; enabled: boolean } | null }
|
||||
|
||||
export async function readGcSwitch(c: Asc, appId: string, version: J | null): Promise<GcSwitch> {
|
||||
const detail = (await c.maybe(`/v1/apps/${appId}/gameCenterDetail`))?.data?.id ?? null;
|
||||
return { appId, detail, version: version ? { id: version.id, versionString: String(version.attributes?.versionString ?? ''), enabled: !!(await c.maybe(`/v1/appStoreVersions/${version.id}/gameCenterAppVersion`))?.data } : null };
|
||||
}
|
||||
|
||||
/** The writes the switch still needs, as request lines (empty: on). */
|
||||
export function gcSwitchPlan(s: GcSwitch): string[] {
|
||||
return [...(s.detail ? [] : [`POST /v1/gameCenterDetails {app=${s.appId}}`]),
|
||||
...(s.version && !s.version.enabled ? [`POST /v1/gameCenterAppVersions {appStoreVersion=${s.version.id}} # version ${s.version.versionString}`] : [])];
|
||||
}
|
||||
|
||||
/** Creates what gcSwitchPlan names (find-then-create, never twice). */
|
||||
export async function ensureGcSwitch(c: Asc, s: GcSwitch, tag = TAG): Promise<GcSwitch> {
|
||||
let detail = s.detail;
|
||||
if (!detail) {
|
||||
detail = (await createOnce({ what: 'Game Center detail', tag, unique: true,
|
||||
create: async () => (await c.asc('POST', '/v1/gameCenterDetails', { data: { type: 'gameCenterDetails', relationships: { app: { data: { type: 'apps', id: s.appId } } } } })).data.id as string,
|
||||
find: async () => (await c.maybe(`/v1/apps/${s.appId}/gameCenterDetail`))?.data?.id ?? null })).value;
|
||||
log(tag, `Game Center switched on for the app (${detail})`);
|
||||
}
|
||||
const v = s.version;
|
||||
if (v && !v.enabled) {
|
||||
await createOnce({ what: `Game Center for version ${v.versionString}`, tag, unique: true,
|
||||
create: async () => (await c.asc('POST', '/v1/gameCenterAppVersions', { data: { type: 'gameCenterAppVersions', relationships: { appStoreVersion: { data: { type: 'appStoreVersions', id: v.id } } } } })).data.id as string,
|
||||
find: async () => (await c.maybe(`/v1/appStoreVersions/${v.id}/gameCenterAppVersion`))?.data?.id ?? null });
|
||||
log(tag, `Game Center switched on for version ${v.versionString}`);
|
||||
}
|
||||
return { ...s, detail, version: v ? { ...v, enabled: true } : null };
|
||||
}
|
||||
|
||||
/** Game Center is in use when the config says so, the iOS project has the entitlement, or the newest local IPA does. */
|
||||
export function gameCenterInUse(config: Ctx['loaded']['config'], root: string): { inUse: boolean; why: string | null } {
|
||||
if (usesGameCenter(config)) return { inUse: true, why: 'games.gameCenter' };
|
||||
try { const p = readProjectEntitlements(root)?.find(e => e.caps.some(x => x.cap === 'GAME_CENTER')); if (p) return { inUse: true, why: `${p.file} has com.apple.developer.game-center` }; } catch { /* unreadable */ }
|
||||
try { const b = builtIpaEntitlements(root, config); if (b?.caps.some(x => x.cap === 'GAME_CENTER')) return { inUse: true, why: `build ${b.number} (${b.file}) has com.apple.developer.game-center` }; } catch { /* no IPA */ }
|
||||
return { inUse: false, why: null };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue