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

@ -11,6 +11,9 @@ export const SIZES: Record<string, { w: number; h: number; device?: 'tablet'; fe
ios65: { w: 1284, h: 2778 },
'iphone-medium': { w: 1206, h: 2622 },
ipad13: { w: 2064, h: 2752, device: 'tablet' },
// iPhone Duo (foldable), only with apple.iphoneDuo: near-tablet aspect, so the tablet frames fill it
'iphone-duo-outer': { w: 1398, h: 2034, device: 'tablet' },
'iphone-duo-inner': { w: 2007, h: 2853, device: 'tablet' },
play: { w: 1080, h: 1920 },
tab7: { w: 1080, h: 1920, device: 'tablet' },
tab10: { w: 1440, h: 2560, device: 'tablet' },
@ -120,13 +123,14 @@ export function frameDims(config: Pick<ShipConfig, 'apple' | 'shots'>): Record<'
export const PLAY_TABLET_SIZES: readonly string[] = ['tab7', 'tab10'];
/** Size keys this project renders by default: Apple sizes only with an apple section (iPad only with apple.ipad, the
* 6.3" iPhone only with apple.iphoneMedium),
* 6.3" iPhone only with apple.iphoneMedium, the iPhone Duo sizes only with apple.iphoneDuo),
* Play sizes only with a play section (both when neither is configured); the Play tablet sizes only when `tablets`
* (resolved play.tablets, see resolvedTablets in report.ts; unset here = play.tablets ?? true). */
export function defaultSizes(config: Pick<ShipConfig, 'apple' | 'play'>, tablets?: boolean): string[] {
const apple = !!config.apple || !config.play, play = !!config.play || !config.apple;
const tabs = tablets ?? config.play?.tablets ?? true;
return Object.keys(SIZES).filter(k => (k === 'ipad13' ? apple && !!config.apple?.ipad : k === 'iphone-medium' ? apple && !!config.apple?.iphoneMedium
: k.startsWith('iphone-duo') ? apple && !!config.apple?.iphoneDuo
: k.startsWith('ios') ? apple : PLAY_TABLET_SIZES.includes(k) ? play && tabs : play));
}

View file

@ -40,10 +40,14 @@ export const IMAGE_SPECS: Record<string, ImageSpec> = {
// 6.3" iPhone with Dynamic Island, medium display (iPhone 14 Pro .. 18 Pro): optional here (apple.iphoneMedium). Apple
// scales the 6.5" (Face ID large) set down for it when it is missing. APP_IPHONE_61 is inferred: the enum has no
// descriptions and 61 is the only medium iPhone bucket (NOT VERIFIED against an upload).
'iphone-medium': { store: 'apple', type: 'APP_IPHONE_61', label: 'App Store iPhone 6.3"', sizes: [[1206, 2622], [1179, 2556]], min: 1, max: 10, maxBytes: null, alpha: false },
'iphone-medium': { store: 'apple', type: 'APP_IPHONE_61', label: 'iPhone with Dynamic Island (medium display)', sizes: [[1206, 2622], [1179, 2556]], min: 1, max: 10, maxBytes: null, alpha: false },
ios65: { store: 'apple', type: 'APP_IPHONE_65', label: 'App Store iPhone 6.5"', sizes: [[1284, 2778], [1242, 2688]], min: 1, max: 10, maxBytes: null, alpha: false },
// 13" iPad display (iPad Pro M4/M5, iPad Air M2+): required when the app runs on iPad. API display type keeps the old name.
ipad13: { store: 'apple', type: 'APP_IPAD_PRO_3GEN_129', label: 'App Store iPad 13"', sizes: [[2064, 2752], [2048, 2732]], min: 1, max: 10, maxBytes: null, alpha: false },
ipad13: { store: 'apple', type: 'APP_IPAD_PRO_3GEN_129', label: 'iPad 13" display', sizes: [[2064, 2752], [2048, 2732]], min: 1, max: 10, maxBytes: null, alpha: false },
// iPhone Duo (foldable, Screenshot specifications, October 2026): outer and inner display, portrait or landscape,
// optional (apple.iphoneDuo). `type` is not an ASC display type: see IPHONE_DUO_DISPLAY. No app preview sizes yet.
'iphone-duo-outer': { store: 'apple', type: 'IPHONE_DUO_OUTER', label: 'iPhone Duo (outer display)', sizes: [[1398, 2034]], min: 1, max: 10, maxBytes: null, alpha: false },
'iphone-duo-inner': { store: 'apple', type: 'IPHONE_DUO_INNER', label: 'iPhone Duo (inner display)', sizes: [[2007, 2853]], min: 1, max: 10, maxBytes: null, alpha: false },
play: { store: 'play', type: 'phoneScreenshots', label: 'Play phone', sizes: [[1080, 1920]], min: 2, max: 8, maxBytes: 8 * MB, alpha: false },
tab7: { store: 'play', type: 'sevenInchScreenshots', label: 'Play 7" tablet', sizes: [[1080, 1920]], min: 0, max: 8, maxBytes: 8 * MB, alpha: false },
tab10: { store: 'play', type: 'tenInchScreenshots', label: 'Play 10" tablet', sizes: [[1440, 2560]], min: 0, max: 8, maxBytes: 8 * MB, alpha: false },
@ -51,6 +55,23 @@ export const IMAGE_SPECS: Record<string, ImageSpec> = {
feature: { store: 'play', type: 'featureGraphic', label: 'Play feature graphic', sizes: [[1024, 500]], min: 1, max: 1, maxBytes: 15 * MB, alpha: false },
};
/** ASC screenshotDisplayType per iPhone Duo size key. null = NOT IN THE API YET: ScreenshotDisplayType in App Store
* Connect API 4.5.1 (spec checked 2026-10-18) has no Duo value (only AppAssetLibraryDisplayClass lists IPHONE_DUO).
* When Apple adds it: regenerate (bun tools/gen-api.ts) and put the exact enum names here; uploads then include them. */
export const IPHONE_DUO_DISPLAY: Record<string, string | null> = { 'iphone-duo-outer': null, 'iphone-duo-inner': null };
export const IPHONE_DUO_NOT_IN_API = 'App Store Connect does not take iPhone Duo screenshots through the API yet; storebound keeps them in store/renders';
/** Reportedly required from this date for apps built with the iOS 27.1 SDK or later. */
export const IPHONE_DUO_REQUIRED_FROM = '2027-04-01';
const PLAY_FOLDABLE = 'Google Play has no foldable slot: foldables show the 7" / 10" tablet screenshots (tab7, tab10, play.tablets)';
/** check / validate note for an iPhone app without iPhone Duo screenshots: info before IPHONE_DUO_REQUIRED_FROM, a
* warning after (never an error: the rule depends on the SDK the build uses). null when Duo renders exist or no iPhone app. */
export function iphoneDuoNote(o: { apple: boolean; duo: boolean; play?: boolean; today?: Date }): { level: 'info' | 'warn'; msg: string } | null {
if (!o.apple || o.duo) return null;
const late = (o.today ?? new Date()).toISOString().slice(0, 10) >= IPHONE_DUO_REQUIRED_FROM;
return { level: late ? 'warn' : 'info', msg: `no iPhone Duo screenshots (outer 1398x2034, inner 2007x2853; apple.iphoneDuo): optional now, required from April 2027 for iOS 27.1 SDK builds${o.play ? `. ${PLAY_FOLDABLE}` : ''}` };
}
/** Play screenshots in general (any size the store accepts): 320..3840 px per side, long side at most 2x the short. */
export function playScreenshotOk(w: number, h: number): boolean {
const lo = Math.min(w, h), hi = Math.max(w, h);