apple: macOS (tvOS, visionOS) versions are no longer invisible: every version and review submission lookup is per platform; status, listing, listing pull, resubmit, submit, release and screenshots take --platform and otherwise pick the platform that has the version they need (refuse naming both when several do); status, validate, release status, the report and the desktop label versions with their platform; MCP and owner actions get platform; pitfall 77

This commit is contained in:
CRTkafa 2026-10-20 03:00:05 +07:00
parent 1736300784
commit 47dfd372fe
30 changed files with 423 additions and 127 deletions

View file

@ -18,7 +18,12 @@ export type SubmissionKind = 'waiting' | 'inReview' | 'unresolved' | 'draft' | '
export interface SubmissionItem { id: string; kind: string; label: string; state: string; rejected: boolean }
export interface SubmissionView { id: string; state: string; kind: SubmissionKind; date: string | null; items: SubmissionItem[] }
export interface BuildChoice { id: string; number: string; version: string | null; state: string; uploaded: string | null; valid: boolean; attachedTo: string | null }
export interface AppleVersion { id: string; version: string; state: string; build: string | null }
export interface AppleVersion { id: string; version: string; state: string; build: string | null; platform: string }
const PLATFORM_LABEL: Record<string, string> = { IOS: 'iOS', MAC_OS: 'macOS', TV_OS: 'tvOS', VISION_OS: 'visionOS' };
/** "iOS", "macOS", ... of an App Store platform (IOS when the data has none). */
export const platformLabel = (p: string | undefined): string => PLATFORM_LABEL[p || 'IOS'] ?? p ?? 'iOS';
/** The CLI's --platform value of an App Store platform. */
export const platformFlag = (p: string | undefined): string => platformLabel(p).toLowerCase();
export interface AppleSubmitState {
submissions: SubmissionView[];
/** The first submission back from App Review (UNRESOLVED_ISSUES), with its items. */
@ -40,11 +45,11 @@ export function appleSubmitState(raw: unknown): AppleSubmitState {
const d = obj(raw) as AppleStatusData & J;
const versions: AppleVersion[] = arr(d.versions).map(v => {
const o = obj(v), a = obj(o.attributes);
return { id: str(o.id), version: str(a.versionString), state: str(a.appVersionState) || str(a.appStoreState), build: str(obj(obj(obj(o.relationships).build).data).id) || null };
return { id: str(o.id), version: str(a.versionString), state: str(a.appVersionState) || str(a.appStoreState), build: str(obj(obj(obj(o.relationships).build).data).id) || null, platform: str(a.platform) || 'IOS' };
}).filter(v => v.id && v.version);
const label = (kind: string, versionId: string): string => {
const v = versions.find(x => x.id === versionId);
return v && kind === 'appStoreVersion' ? `app version ${v.version}` : `${ITEM_NAME[kind] ?? kind} ${versionId}`;
return v && kind === 'appStoreVersion' ? `app version ${v.version} (${platformLabel(v.platform)})` : `${ITEM_NAME[kind] ?? kind} ${versionId}`;
};
const empty = new Set(arr(d.emptyDrafts).map(str));
const unresolvedItems = new Map(arr(d.unresolved).map(u => [str(obj(u).id), arr(obj(u).items)] as const));
@ -67,7 +72,7 @@ export function appleSubmitState(raw: unknown): AppleSubmitState {
const unresolved = submissions.find(s => s.kind === 'unresolved') ?? null;
const busy = submissions.find(s => s.kind === 'waiting' || s.kind === 'inReview') ?? null;
const held = new Set(unresolved?.items.filter(i => i.kind === 'appStoreVersion').map(i => i.label) ?? []);
const editable = versions.find(v => held.has(`app version ${v.version}`)) ?? versions.find(v => SUBMITTABLE_APP.includes(v.state)) ?? null;
const editable = versions.find(v => held.has(`app version ${v.version} (${platformLabel(v.platform)})`)) ?? versions.find(v => SUBMITTABLE_APP.includes(v.state)) ?? null;
return { submissions, unresolved, busy, builds, editable, versions };
}

View file

@ -10,7 +10,7 @@ import { useState } from 'react';
import { api, type Project } from '../../api';
import { type Key, t } from '../../i18n';
import { ago, date } from '../../lib/format';
import { type AppleStatusData, appleSubmitState, type PlayTrackData, playSubmitState, type SubmissionKind } from '../../lib/submit';
import { type AppleStatusData, appleSubmitState, platformFlag, platformLabel, type PlayTrackData, playSubmitState, type SubmissionKind } from '../../lib/submit';
import type { UnifiedApp } from '../../lib/identity';
import { useData, useRead } from '../../store';
import { ConsolePlanButton } from '../../ui/ConsolePlan';
@ -35,9 +35,9 @@ export function AppleSubmitCard({ app, project }: { app: UnifiedApp; project: Pr
const summary = d.statusOf(project)?.summary;
const iaps = summary?.catalog.ios.length ?? 0;
const picked = s?.builds.find(b => b.number === build && b.valid) ?? null;
const version = s?.unresolved ? s.versions.find(v => s.unresolved!.items.some(i => i.label === `app version ${v.version}`)) ?? s.editable : s?.editable ?? null;
const version = s?.unresolved ? s.versions.find(v => s.unresolved!.items.some(i => i.label === `app version ${v.version} (${platformLabel(v.platform)})`)) ?? s.editable : s?.editable ?? null;
const rejectedIap = s?.unresolved?.items.some(i => i.rejected && i.kind !== 'appStoreVersion') ?? false;
const args = { build, ...(version ? { version: version.version } : {}), ...(resolveIaps ? { resolve_iaps: true } : {}) };
const args = { build, ...(version ? { version: version.version, platform: platformFlag(version.platform) } : {}), ...(resolveIaps ? { resolve_iaps: true } : {}) };
const refresh = () => { r.refresh(); };
return (
<Card title={t('sub.appleTitle')} icon="rocket" sub={<span className="row" style={{ gap: 6 }}><StoreGlyph store="ios" className="ic13" />{r.data ? (r.fromCache ? t('common.cachedFrom', { when: ago(r.at) }) : t('common.readWhen', { when: ago(r.at) })) : r.loading ? t('read.waiting') : t('sub.appleSub')}</span>}
@ -90,7 +90,7 @@ export function AppleSubmitCard({ app, project }: { app: UnifiedApp; project: Pr
</label>
<div className="stack small" style={{ gap: 4 }}>
<span className="strong">{t('sub.version')}</span>
<span>{version ? `${version.version} · ${version.state}` : <Chip tone="warn" icon="question">{t('sub.noVersion')}</Chip>}</span>
<span>{version ? `${version.version} · ${platformLabel(version.platform)} · ${version.state}` : <Chip tone="warn" icon="question">{t('sub.noVersion')}</Chip>}</span>
</div>
</div>
<span id="sub-build-hint" className="small muted">{s.builds.length ? t('sub.buildHint') : t('sub.noBuilds')}</span>

View file

@ -20,7 +20,7 @@ describe('App Store submission state', () => {
test('UNRESOLVED_ISSUES: its items with REJECTED marked, the held version is the one to resubmit', () => {
const s = appleSubmitState(APPLE_STATUS_UNRESOLVED);
expect(s.unresolved).toMatchObject({ id: 'rs-back-1', kind: 'unresolved' });
expect(s.unresolved!.items.map(i => [i.label, i.state, i.rejected])).toEqual([['app version 2.4.0', 'REJECTED', true], ['in-app purchase iapv-coins', 'READY_FOR_REVIEW', false]]);
expect(s.unresolved!.items.map(i => [i.label, i.state, i.rejected])).toEqual([['app version 2.4.0 (iOS)', 'REJECTED', true], ['in-app purchase iapv-coins', 'READY_FOR_REVIEW', false]]);
expect(s.editable).toMatchObject({ version: '2.4.0', state: 'REJECTED' });
expect(s.busy).toBeNull();
});