checkAppStoreTree function

List<ReleaseProblem> checkAppStoreTree(
  1. String path, {
  2. Set<String> requireScreenshotTypes = const {},
  3. Set<String> requireLocales = const {},
  4. bool requirePreviewFrames = false,
})

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;
}