checkAppStoreTree function
Loads the App Store metadata tree at path and reports what it refuses.
The loading is the check: loadMetadata validates text limits, URL
schemes, category ids, screenshot dimensions and — the one that fires most
often in practice — the alpha channel every simulator screen capture
carries. Anything it throws becomes a problem here rather than an exception,
so a caller sees it alongside whatever else is wrong.
requireScreenshotTypes covers the case the loader cannot know about on its
own: a universal app declaring TARGETED_DEVICE_FAMILY = "1,2" must carry an
iPad set as well as an iPhone one, and Apple refuses the submission if it
does not. Which types are required is a property of the app, so the consumer
names them.
requirePreviewFrames is the same kind of requirement about a different
field: every preview must name its poster frame rather than inheriting
Apple's five-second default.
Here rather than in the loader, and that is the whole design decision.
The tree's standing rule is present means owned — a file that exists
replaces what App Store Connect holds, one that does not is left alone — so
a missing .timecode means "leave the poster Apple has", which is the right
answer for a project that set one in the console and does not want it
reasserted. Making the sidecar mandatory in loadMetadata would make that
state unreachable for every consumer, to serve a policy only some of them
have.
But the policy is a good one and the argument for it is strong: Apple's
default is invisible everywhere except a search result, and a preview
freezes with the version, so a poster that quietly shipped wrong cannot be
corrected without a new submission. A flag here is how this package already
says "the store permits it and this project does not" — it is what
requireScreenshotTypes is — and it puts the requirement in the consumer's
test suite, where it fails on the push that introduces it.
Implementation
List<ReleaseProblem> checkAppStoreTree(
String path, {
Set<String> requireScreenshotTypes = const {},
Set<String> requireLocales = const {},
bool requirePreviewFrames = false,
}) {
final AppStoreMetadata metadata;
try {
metadata = loadMetadata(path);
} on MetadataException catch (e) {
return [ReleaseProblem(path, e.message)];
}
final problems = <ReleaseProblem>[];
if (metadata.locales.isEmpty) {
problems.add(ReleaseProblem(path, 'no locales — nothing would publish'));
return problems;
}
for (final locale in requireLocales) {
if (!metadata.locales.any((l) => l.locale == locale)) {
problems.add(
ReleaseProblem(path, 'no listing for required locale $locale'),
);
}
}
for (final locale in metadata.locales) {
if (requireLocales.isNotEmpty && !requireLocales.contains(locale.locale)) {
continue;
}
for (final type in requireScreenshotTypes) {
final shots = locale.screenshots[type];
if (shots == null || shots.isEmpty) {
problems.add(
ReleaseProblem(
'$path → ${locale.locale}',
'no $type screenshots, which this app is required to carry',
),
);
}
}
if (requirePreviewFrames) {
for (final type in locale.previews.entries) {
for (final preview in type.value) {
if (preview.frameTimeCode != null) {
continue;
}
final name = preview.file.uri.pathSegments.last;
problems.add(
ReleaseProblem(
'$path → ${locale.locale}',
'previews/${type.key}/$name names no poster frame, so Apple '
'would pose it at $defaultPreviewFrameTimeCode.\n'
' Write the frame to '
'previews/${type.key}/$name$previewTimeCodeSuffix — a '
'preview freezes with the version, so a default that ships '
'by accident needs a new submission to correct.',
),
);
}
}
}
}
return problems;
}