iPhone Duo screenshots (render, check, upload skip with console link while the API has no display type) and the medium display class as the required iPhone set, labels as in the console

This commit is contained in:
CRTkafa 2026-10-18 13:07:15 +07:00
parent 0b32df57f0
commit 3381673ae6
17 changed files with 163 additions and 16 deletions

View file

@ -622,6 +622,10 @@ export interface ShipConfig {
* pixel-exact art on 6.3" phones, or if Apple stops scaling. (Screenshot specifications, App Store Connect Help.)
*/
iphoneMedium?: boolean;
/** Also render and check iPhone Duo (foldable) screenshots: outer 1398 x 2034 and inner 2007 x 2853 (landscape too).
* Optional now, reportedly required from April 2027 for iOS 27.1 SDK builds. Not uploaded while the API has no
* display type for them (cli/shots/spec.ts IPHONE_DUO_DISPLAY). */
iphoneDuo?: boolean;
/** TestFlight beta groups, the tester CSV (in .storebound/) and Test Information (`storebound apple testflight`). */
testflight?: TfConfig;
/** Certificates, profiles and capabilities through the API (`storebound apple signing`), signing style of `apple build`. */

File diff suppressed because one or more lines are too long

View file

@ -3,6 +3,7 @@
import { consoleLink, consoleUrl } from './console-links';
import { type FixAction, fixAppleListing, fixPlayListing, fixReviewDetails } from './fix-actions';
import { iphoneDuoNote } from '../shots/spec';
export type Level = 'blocker' | 'warn' | 'ok';
@ -11,11 +12,16 @@ export type Level = 'blocker' | 'warn' | 'ok';
* (either 13" display type). The smaller sizes are scaled down from these. A live app with only the 6.5" set is fine
* (store-pitfalls 44). */
export const ACCEPTED_INSTEAD: Record<string, string[]> = {
// the console's required iPhone class (medium display): smaller sizes scale down from a larger set automatically
APP_IPHONE_61: ['APP_IPHONE_67', 'APP_IPHONE_65'],
APP_IPHONE_67: ['APP_IPHONE_65'],
APP_IPHONE_65: ['APP_IPHONE_67'],
APP_IPAD_PRO_3GEN_129: ['APP_IPAD_PRO_129'],
APP_IPAD_PRO_129: ['APP_IPAD_PRO_3GEN_129'],
};
/** Required class -> larger sets App Store Connect scales down into it ("View All Sizes in Media Manager"). */
const SCALES_DOWN: Record<string, string[]> = { APP_IPHONE_61: ['APP_IPHONE_67', 'APP_IPHONE_65'] };
const LABEL: Record<string, string> = { APP_IPHONE_61: 'iPhone with Dynamic Island (medium display)', APP_IPAD_PRO_3GEN_129: 'iPad 13" display' };
export interface Finding { store: 'apple' | 'play'; level: Level; code: string; msg: string; fix?: string;
/** Absolute deep link to the console page the message points to (desktop / MCP render it as an Open link). */
url?: string;
@ -58,6 +64,10 @@ export interface AppleSnapshot {
expectedLocales: string[];
locales: { locale: string; name?: string; subtitle?: string; description?: string; keywords?: string; screenshots: Record<string, number> }[];
requiredDisplayTypes: string[];
/** apple.iphoneDuo (iPhone Duo renders on) and whether the project has a play section, for the Duo note. */
iphoneDuo?: boolean;
play?: boolean;
today?: Date;
reviewDetail: { contactEmail?: string | null; contactPhone?: string | null; contactFirstName?: string | null; contactLastName?: string | null; demoAccountRequired?: boolean; demoAccountName?: string | null; notes?: string | null } | null;
/** Age rating declaration attributes (null = not readable). */
ageRating: Record<string, unknown> | null;
@ -212,7 +222,8 @@ export function appleRules(s: AppleSnapshot): Finding[] {
for (const t of s.requiredDisplayTypes) {
if (l.screenshots[t]) continue;
const alt = (ACCEPTED_INSTEAD[t] ?? []).find(x => l.screenshots[x]);
if (alt) add('warn', 'screenshots-size', `${l.locale}: no ${t} set; Apple accepts the ${alt} set (${l.screenshots[alt]} screenshots) and scales it for the other sizes. Recommended, not required: add ${t}`, 'storebound apple screenshots');
if (alt && SCALES_DOWN[t]?.includes(alt)) add('ok', 'screenshots-size', `${l.locale}: ${LABEL[t] ?? t} filled by scaling the ${alt} set down (${l.screenshots[alt]} screenshots)`);
else if (alt) add('warn', 'screenshots-size', `${l.locale}: no ${t} set; Apple accepts the ${alt} set (${l.screenshots[alt]} screenshots) and scales it for the other sizes. Recommended, not required: add ${t}`, 'storebound apple screenshots');
else add('blocker', 'screenshots', `${l.locale}: no ${t} screenshots${ACCEPTED_INSTEAD[t] ? ` (or ${ACCEPTED_INSTEAD[t]!.join(' / ')})` : ''}`, 'storebound apple screenshots');
}
}
@ -254,6 +265,9 @@ export function appleRules(s: AppleSnapshot): Finding[] {
add(urlOk(s.privacyUrl) ? 'ok' : 'blocker', 'privacy-url', `privacy policy ${s.privacyUrl.url}: ${urlMsg(s.privacyUrl)}`, 'privacy-policy skill');
if (s.profile) f.push(...profileRules('apple', s.profile, { reviewDetail: s.reviewDetail, descriptions: s.locales.map(l => ({ locale: l.locale, text: l.description ?? '' })) }));
f.push(...rendersFindings('apple', s.renders));
// iPhone Duo: optional until April 2027, so a passing note (never a blocker); a warning after that date
const duo = iphoneDuoNote({ apple: true, duo: !!s.iphoneDuo, play: s.play, today: s.today });
if (duo) add(duo.level === 'info' ? 'ok' : 'warn', 'screenshots-duo', duo.msg, 'apple.iphoneDuo: true, then storebound shots render');
return sortFindings(f);
}