From 40182295d3596b219d17d226b68dfa4557061010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 13 Oct 2024 10:14:44 +0200 Subject: [PATCH 1/2] wip --- .../src/pkpass/creation_failure_reason.dart | 45 +++++++++ passkit/lib/src/pkpass/pkpass.dart | 93 ++++++++++++++++++- 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 passkit/lib/src/pkpass/creation_failure_reason.dart diff --git a/passkit/lib/src/pkpass/creation_failure_reason.dart b/passkit/lib/src/pkpass/creation_failure_reason.dart new file mode 100644 index 0000000..7400bac --- /dev/null +++ b/passkit/lib/src/pkpass/creation_failure_reason.dart @@ -0,0 +1,45 @@ +import 'package:passkit/src/pkpass/pass_data.dart'; +import 'package:passkit/src/pkpass/pkpass.dart'; + +class CreationFailureException implements Exception { + const CreationFailureException(this.failures); + + final List failures; + + @override + String toString() => 'CreationFailureException(failures: $failures)'; +} + +enum CreationFailureReason { + /// [PkPass.icon] is missing + missingIconImage, + + /// [PkPass.logo] is missing + missingLogoImage, + + /// [PkPass.background] is set, but it should not be set + superfluousBackgroundImage, + + /// [PkPass.thumbnail] is set, but it should not be set + superfluousThumbnailImage, + + /// When thrown for a PkPass: + // TODO(any): Describe the problem + /// When thrown for a PkOrder: + // TODO(any): Describe the problem + certificateIdentifierMistmatch, + + /// When thrown for a PkPass: + // TODO(any): Describe the problem + /// When thrown for a PkOrder: + // TODO(any): Describe the problem + certificateTeamIdentifierMismatch, + + /// Indicates that a language is not completely translated, thus missing one + /// or more translated strings compared to the other translated languages. + incompleteTranslation, + + /// One of [PassData.coupon], [PassData.generic], [PassData.eventTicket], + /// [PassData.storeCard] or [PassData.boardingPass] must be set. + undefinedPassType, +} diff --git a/passkit/lib/src/pkpass/pkpass.dart b/passkit/lib/src/pkpass/pkpass.dart index 090db2b..3c1302c 100644 --- a/passkit/lib/src/pkpass/pkpass.dart +++ b/passkit/lib/src/pkpass/pkpass.dart @@ -6,6 +6,7 @@ import 'package:passkit/src/archive_file_extension.dart'; import 'package:passkit/src/crypto/signature_verification.dart'; import 'package:passkit/src/crypto/write_signature.dart'; import 'package:passkit/src/pk_image.dart'; +import 'package:passkit/src/pkpass/creation_failure_reason.dart'; import 'package:passkit/src/pkpass/exceptions.dart'; import 'package:passkit/src/pkpass/pass_data.dart'; import 'package:passkit/src/pkpass/pass_type.dart'; @@ -257,6 +258,8 @@ class PkPass { /// Apple's documentation [here](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/Creating.html) /// explains which fields to set for which type of pass. /// + /// Throws a [CreationFailureException] if something is off. + /// /// Remarks: /// - Image sizes aren't checked, which means it's possible to create passes /// that look odd and wrong in Apple wallet or [passkit_ui](https://pub.dev/packages/passkit_ui) @@ -265,6 +268,18 @@ class PkPass { required String? privateKeyPem, X509? overrideWwdrCert, }) { + if (!(certificatePem == null && privateKeyPem != null)) { + throw ArgumentError( + 'You must either set certificatePem and privateKeyPem or none of them', + ); + } + + final failureReasons = []; + final passTypeIssue = _validatePassType(); + if (passTypeIssue != null) { + failureReasons.add(passTypeIssue); + } + final archive = Archive(); final passContent = utf8JsonEncode(pass.toJson()); @@ -285,6 +300,9 @@ class PkPass { archive.addFile(personalizationFile); } + failureReasons.addAll(_validateCorrectImages()); + + // TODO(any): Validate that each image is correctly localized logo?.writeToArchive(archive, 'logo'); background?.writeToArchive(archive, 'background'); icon?.writeToArchive(archive, 'icon'); @@ -295,7 +313,15 @@ class PkPass { final translationEntries = languageData?.entries; if (translationEntries != null && translationEntries.isNotEmpty) { - // TODO(any): Ensure every translation file has the same amount of key value pairs. + int translationCount = translationEntries.first.value.length; + for (final entry in translationEntries) { + if (entry.value.length != translationCount) { + failureReasons.add(CreationFailureReason.incompleteTranslation); + // After seeing one incomplete translation, there's no need to check + // for more incomplete translations. + break; + } + } for (final entry in translationEntries) { final name = '${entry.key}.lproj/pass.strings'; @@ -307,7 +333,7 @@ class PkPass { final manifestFile = archive.createManifest(); - if (certificatePem != null && privateKeyPem != null) { + if (certificatePem != null) { final signature = writeSignature( certificatePem, privateKeyPem, @@ -326,6 +352,10 @@ class PkPass { archive.addFile(signatureFile); } + if (failureReasons.isNotEmpty) { + throw CreationFailureException(failureReasons); + } + final pkpass = ZipEncoder().encode(archive); return pkpass == null ? null : Uint8List.fromList(pkpass); } @@ -359,6 +389,65 @@ class PkPass { sourceData: sourceData ?? this.sourceData, ); } + + // See https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/Creating.html#//apple_ref/doc/uid/TP40012195-CH4-SW1 + List _validateCorrectImages() { + final missingImageFailures = []; + // Every pass should have an icon and a logo + if (icon == null) { + missingImageFailures.add(CreationFailureReason.missingIconImage); + } + if (logo == null) { + missingImageFailures.add(CreationFailureReason.missingLogoImage); + } + switch (type) { + case PassType.boardingPass: + // Since the footer is optional, there's no need to check for its presence. + // While superfluous images are bad, it's nothing that breaks passes AFAIK. + // Thus, validation for that can be added later if needed. + return missingImageFailures; + case PassType.coupon: + case PassType.generic: + // Since the strip is optional, there's no need to check for its presence. + // While superfluous images are bad, it's nothing that breaks passes AFAIK. + // Thus, validation for that can be added later if needed. + return missingImageFailures; + case PassType.storeCard: + // Since the thumbnail is optional, there's no need to check for its presence. + // While superfluous images are bad, it's nothing that breaks passes AFAIK. + // Thus, validation for that can be added later if needed. + return missingImageFailures; + case PassType.eventTicket: + // An event ticket can display logo, strip, background, or thumbnail images. + // However, if you supply a strip image, don’t include a background or thumbnail image. + // https://developer.apple.com/design/human-interface-guidelines/wallet#Event-tickets + if (strip != null) { + if (background != null) { + missingImageFailures + .add(CreationFailureReason.superfluousBackgroundImage); + } + if (thumbnail != null) { + missingImageFailures + .add(CreationFailureReason.superfluousThumbnailImage); + } + } + + return missingImageFailures; + } + } + + CreationFailureReason? _validatePassType() { + final isPassTypeKnown = (pass.coupon ?? + pass.generic ?? + pass.boardingPass ?? + pass.eventTicket ?? + pass.storeCard) != + null; + if (!isPassTypeKnown) { + return CreationFailureReason.undefinedPassType; + } + return null; + } } // This is intentionally not exposed to keep this an implementation detail. From c5f8e46557f63ee541d5a5feb6e0a4717d71daaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Thu, 17 Oct 2024 19:46:10 +0200 Subject: [PATCH 2/2] changelog rewording --- passkit/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passkit/CHANGELOG.md b/passkit/CHANGELOG.md index 4982da2..15140f7 100644 --- a/passkit/CHANGELOG.md +++ b/passkit/CHANGELOG.md @@ -1,6 +1,6 @@ ## Unreleased -- No longer mark `PkPass.write()` as experimental +- `PkPass.write()` is no longer experimental - Add webservice support for orders - Add support for readong images of orders - Add support for creating order files