// The device families of the app's last live iOS version, read from App Store Connect (cli/lib/device-families.ts compares // them with the project and the config). Signals, merged because none is documented as THE answer: // - the live version's build bundle: buildBundles.deviceProtocols and the device models of buildBundleFileSizes // - the screenshot sets of the live version: an APP_IPAD_* display type means the version was submitted for iPad // Live version = READY_FOR_SALE / READY_FOR_DISTRIBUTION; else the newest one that was on sale (REPLACED_WITH_NEW_VERSION). import { compareVersions } from '../lib/listing-plan'; import { liveFamilies, type LiveFamilies } from '../lib/device-families'; import type { Asc, J } from './asc'; import { type AscPlatform, versionsPath } from './platform'; const ON_SALE = ['READY_FOR_SALE', 'READY_FOR_DISTRIBUTION']; const WAS_ON_SALE = [...ON_SALE, 'REPLACED_WITH_NEW_VERSION']; /** The live (or last live) version among the rows of GET appStoreVersions, newest first. Pure. */ export function lastLiveVersion(rows: J[]): J | null { const state = (v: J): string => v.attributes.appVersionState ?? v.attributes.appStoreState; const pick = (states: string[]): J | null => rows.filter(v => states.includes(state(v))) .sort((a, b) => compareVersions(String(b.attributes.versionString), String(a.attributes.versionString)))[0] ?? null; return pick(ON_SALE) ?? pick(WAS_ON_SALE); } /** Reads the families of the last live version of the platform (iOS unless named); null when there is none or no signal names a family. */ export async function liveDeviceFamilies(c: Asc, appId: string, platform: AscPlatform = 'IOS'): Promise { const v = lastLiveVersion(await c.all(versionsPath(appId, platform, 20))); if (!v) return null; const signals: { name: string; values: string[] }[] = []; const b = (await c.maybe(`/v1/appStoreVersions/${v.id}/build`))?.data; if (b) { const full = await c.maybe(`/v1/builds/${b.id}?include=buildBundles`); const bundles = (full?.included ?? []).filter((x: J) => x.type === 'buildBundles' && (x.attributes?.bundleType ?? 'APP') === 'APP'); signals.push({ name: `build ${b.attributes?.version ?? b.id} deviceProtocols`, values: bundles.flatMap((x: J) => (x.attributes?.deviceProtocols ?? []) as string[]) }); const models: string[] = []; for (const x of bundles) for (const f of await c.all(`/v1/buildBundles/${x.id}/buildBundleFileSizes?limit=200`)) models.push(String(f.attributes?.deviceModel ?? '')); signals.push({ name: `build ${b.attributes?.version ?? b.id} device models`, values: models }); } const types: string[] = []; for (const l of (await c.all(`/v1/appStoreVersions/${v.id}/appStoreVersionLocalizations?limit=50`)).slice(0, 5)) for (const s of await c.all(`/v1/appStoreVersionLocalizations/${l.id}/appScreenshotSets?limit=50`)) types.push(String(s.attributes?.screenshotDisplayType ?? '')); signals.push({ name: 'screenshot sets', values: types }); return liveFamilies(String(v.attributes.versionString), String(v.attributes.appVersionState ?? v.attributes.appStoreState), signals); }