testAssetLinksFile static method

Future<VerificationResult> testAssetLinksFile(
  1. String domain,
  2. String? packageName,
  3. List<String>? sha256Fingerprints
)

Test Android Asset Links JSON file

Implementation

static Future<VerificationResult> testAssetLinksFile(
  String domain,
  String? packageName,
  List<String>? sha256Fingerprints,
) async {
  final url = Uri.parse('https://$domain/.well-known/assetlinks.json');

  try {
    final response = await http.get(
      url,
      headers: {'Accept': 'application/json'},
    );

    // Check status code
    if (response.statusCode != 200) {
      return VerificationResult(
        checkName: 'Asset Links File Accessibility',
        status: VerificationStatus.error,
        message: 'Asset Links file returned status ${response.statusCode}',
        fixSuggestion:
            'Ensure the Asset Links file is accessible at https://$domain/.well-known/assetlinks.json',
        details: {'statusCode': response.statusCode, 'url': url.toString()},
      );
    }

    // Check Content-Type
    final contentType = response.headers['content-type'] ?? '';
    if (!contentType.contains('application/json')) {
      return VerificationResult(
        checkName: 'Asset Links File Content-Type',
        status: VerificationStatus.warning,
        message:
            'Asset Links file Content-Type is "$contentType" (expected application/json)',
        fixSuggestion:
            'Ensure the server returns Content-Type: application/json for the Asset Links file',
        details: {'contentType': contentType},
      );
    }

    // Parse JSON
    List<dynamic> json;
    try {
      json = jsonDecode(response.body) as List<dynamic>;
    } catch (e) {
      return VerificationResult(
        checkName: 'Asset Links File JSON Validity',
        status: VerificationStatus.error,
        message: 'Asset Links file is not valid JSON array: $e',
        fixSuggestion: 'Ensure the Asset Links file is a valid JSON array',
      );
    }

    if (json.isEmpty) {
      return VerificationResult(
        checkName: 'Asset Links File Structure',
        status: VerificationStatus.error,
        message: 'Asset Links file is an empty array',
        fixSuggestion:
            'Ensure the Asset Links file contains at least one entry',
      );
    }

    // Check if package name and fingerprints match
    if (packageName != null &&
        sha256Fingerprints != null &&
        sha256Fingerprints.isNotEmpty) {
      bool foundMatch = false;

      for (final entry in json) {
        if (entry is Map<String, dynamic>) {
          final relation = entry['relation'] as List<dynamic>?;
          final target = entry['target'] as Map<String, dynamic>?;

          if (relation != null &&
              relation.contains(
                'delegate_permission/common.handle_all_urls',
              ) &&
              target != null) {
            final targetNamespace = target['namespace'] as String?;
            final targetPackageName = target['package_name'] as String?;
            final targetFingerprints =
                target['sha256_cert_fingerprints'] as List<dynamic>?;

            if (targetNamespace == 'android_app' &&
                targetPackageName == packageName &&
                targetFingerprints != null) {
              // Check if at least one fingerprint matches
              final entryFingerprints = targetFingerprints
                  .map((f) => f.toString().toUpperCase())
                  .toList();
              final expectedFingerprints = sha256Fingerprints
                  .map((f) => f.toUpperCase())
                  .toList();

              for (final expected in expectedFingerprints) {
                if (entryFingerprints.contains(expected)) {
                  foundMatch = true;
                  break;
                }
              }

              if (foundMatch) break;
            }
          }
        }
      }

      if (!foundMatch) {
        return VerificationResult(
          checkName: 'Asset Links File Package Match',
          status: VerificationStatus.error,
          message:
              'Asset Links file does not contain matching entry for package "$packageName"',
          fixSuggestion:
              'Ensure the Asset Links file contains an entry with:\n'
              '  - package_name: "$packageName"\n'
              '  - sha256_cert_fingerprints: [${sha256Fingerprints.join(", ")}]',
          details: {
            'packageName': packageName,
            'fingerprints': sha256Fingerprints,
          },
        );
      }
    }

    return VerificationResult(
      checkName: 'Asset Links File Verification',
      status: VerificationStatus.success,
      message: 'Asset Links file is accessible and valid',
      details: {'url': url.toString(), 'domain': domain},
    );
  } catch (e) {
    return VerificationResult(
      checkName: 'Asset Links File Accessibility',
      status: VerificationStatus.error,
      message: 'Failed to fetch Asset Links file: $e',
      fixSuggestion:
          'Check network connectivity and ensure the domain is correctly configured',
    );
  }
}