// App Store platforms of one app record. An app can have an iOS and a macOS (tvOS, visionOS) version at the same time, // each with its own state and its own review submission (pitfall 79): every version lookup filters by platform, and a // command that needs one version picks the platform that has it, or asks for --platform when several do. import type { Asc, J } from './asc'; export type AscPlatform = 'IOS' | 'MAC_OS' | 'TV_OS' | 'VISION_OS'; export const ASC_PLATFORMS: AscPlatform[] = ['IOS', 'MAC_OS', 'TV_OS', 'VISION_OS']; const FLAG: Record = { ios: 'IOS', macos: 'MAC_OS', tvos: 'TV_OS', visionos: 'VISION_OS' }; const LABEL: Record = { IOS: 'iOS', MAC_OS: 'macOS', TV_OS: 'tvOS', VISION_OS: 'visionOS' }; /** The console's URL segment of a platform's version page. */ export const CONSOLE_SEGMENT: Record = { IOS: 'ios', MAC_OS: 'macos', TV_OS: 'appletvos', VISION_OS: 'visionos' }; /** --platform ios|macos|tvos|visionos (also the API names IOS, MAC_OS, ...) -> the API's platform; undefined when not * given; throws on anything else. Pure. */ export function parsePlatform(v: string | undefined): AscPlatform | undefined { if (v == null || v === '') return undefined; const k = v.toLowerCase().replace(/[_\s-]/g, ''); const p = FLAG[k] ?? FLAG[k.replace(/^appletv/, 'tv')]; if (!p) throw new Error(`--platform: ios | macos | tvos | visionos (got "${v}")`); return p; } /** "iOS", "macOS", ... for any platform string (unknown ones as they are). Pure. */ export const platformLabel = (p: string | undefined | null): string => LABEL[(p ?? 'IOS') as AscPlatform] ?? String(p); export const platformOf = (v: J): AscPlatform => (v?.attributes?.platform ?? 'IOS') as AscPlatform; export const stateOf = (v: J): string => v?.attributes?.appVersionState ?? v?.attributes?.appStoreState ?? ''; /** "0.8.7 (macOS)": a version as people read it. */ export const versionLabel = (v: J): string => `${v?.attributes?.versionString ?? '?'} (${platformLabel(platformOf(v))})`; /** The appStoreVersions list path of one platform. */ export const versionsPath = (appId: string, platform: AscPlatform, limit = 50): string => `/v1/apps/${appId}/appStoreVersions?filter[platform]=${platform}&limit=${limit}`; /** * The platform a command works on. `flag` (--platform) wins. Else the platforms that have a version `needs` accepts: * one -> that one; several -> an error naming them all (ask for --platform); none -> the only platform the app has * versions on, else iOS. Pure. */ export function pickPlatform(versions: J[], needs: ((v: J) => boolean) | ((v: J) => boolean)[], flag: AscPlatform | undefined, what: string): AscPlatform { if (flag) return flag; // several tests: the first one any version passes decides (e.g. "this version string, editable", then "editable") if (Array.isArray(needs)) { for (const n of needs) if (versions.some(n)) return pickPlatform(versions, n, undefined, what); return pickPlatform(versions, () => false, undefined, what); } const hits = [...new Set(versions.filter(needs).map(platformOf))]; if (hits.length === 1) return hits[0]!; if (hits.length > 1) { const which = versions.filter(needs).map(v => `${versionLabel(v)} ${stateOf(v)}`).join(', '); throw new Error(`${what} on more than one platform (${which}): run it again with --platform ${hits.map(p => Object.keys(FLAG).find(k => FLAG[k] === p)).join(' or --platform ')}`); } const all = [...new Set(versions.map(platformOf))]; return all.length === 1 ? all[0]! : 'IOS'; } /** Every App Store version of the app on every platform (only to pick the platform; the command then reads its * platform's list with filter[platform]). */ export async function allVersions(c: Asc, appId: string): Promise { return c.all(`/v1/apps/${appId}/appStoreVersions?filter[platform]=${ASC_PLATFORMS.join(',')}&limit=200`); }