diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml
index b019c35..23b18fc 100644
--- a/.idea/libraries/Dart_Packages.xml
+++ b/.idea/libraries/Dart_Packages.xml
@@ -37,6 +37,13 @@
+
+
+
+
+
+
+
@@ -54,7 +61,7 @@
-
+
@@ -219,6 +226,13 @@
+
+
+
+
+
+
+
@@ -374,9 +388,10 @@
+
-
+
@@ -400,6 +415,7 @@
+
diff --git a/README.md b/README.md
index 83a1218..f39884c 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,35 @@ A Dart native pkpass parsing library
- no platform specific dependencies - pure Dart
- parse any .pkpass file as blob
+- checksum verification
- extract metadata
- high level lookup for assets by locale and scale, with proper fallbacks
+- high level barcode API
+- No dependency on `dart:io` or `dart:ui` runs everywhere, from CLI to Flutter
+
+## Not supported (yet)
+
+Some parts of the PkPass specification are either not yet implemented, or not planned, such as:
+
+- `signature`: The detached PKCS #7 signature using Apple certificates of the manifest. Note: Checksums _are_ checked. -
+ Not planned, feel free to contribute.
+- `nfc`: Card payment information for Apple Pay. - Not planned, feel free to contribute.
+- `webService`: Information used to update passes using the web service. - Planned, feel free to contribute.
+
+## Dependencies and compatibility
+
+Any package should keep its dependencies as minimal as possible. Sometimes, there are specifications making this
+difficult. The PkPass spec unfortunately is a very complex one, requiring support of many standards and formats.
+
+The following dependencies are used to correctly parse the PkPass file into a relevant Dart representation.
+
+- [`pub:archive`](https://pub.dev/packages/archive): The PkPass file itself is a ZIP archive, used to parse the raw
+ bytes.
+- [`pub:barcode`](https://pub.dev/packages/barcode): Used to provide high-level access to barcode generation with the
+ proper encoding supported.
+- [`pub:crypto`](https://pub.dev/packages/crypto): Used for SHA1 signature verification as defined in the PkPass spec.
+- [`pub:intl`](https://pub.dev/packages/intl): Used for localization lookup of localizable resources like Strings or
+ assets.
## Getting started
diff --git a/example/pkpass_example.dart b/example/pkpass_example.dart
index c528792..8596a93 100644
--- a/example/pkpass_example.dart
+++ b/example/pkpass_example.dart
@@ -22,6 +22,9 @@ Future main(List args) async {
);
print('Logo image blob length: ${logo?.length}');
+ print('First barcode: ${pass.metadata.barcodes.firstOrNull?.message}');
+ print('Location: ${pass.metadata.locations.firstOrNull?.relevantText}');
+ print('Date: ${pass.metadata.relevantDate}');
return 0;
}
diff --git a/lib/src/models/barcode.dart b/lib/src/models/barcode.dart
new file mode 100644
index 0000000..37d3e19
--- /dev/null
+++ b/lib/src/models/barcode.dart
@@ -0,0 +1,89 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:barcode/barcode.dart';
+
+/// Information about a pass’s barcode.
+///
+/// Use with Flutter:
+///
+/// ```dart
+/// BarcodeWidget.fromBytes(
+/// barcode: Barcode.fromType(myPassBarcode.format),
+/// data: myPassBarcode.barcodeData,
+/// )
+/// ```
+///
+/// Please not the spec requires you to display the [altText] next to the
+/// barcode in case available.
+class PassBarcode {
+ /// the [Encoding] supported for [messageEncoding]. Can be expanded at runtime.
+ ///
+ /// Default values are
+ /// - iso-8859-1: [Latin1Codec]
+ /// - utf-8: [Utf8Codec]
+ static Map supportedCodecs = {
+ 'iso-8859-1': Latin1Codec(),
+ 'iso-8859': Latin1Codec(),
+ 'iso8859': Latin1Codec(),
+ 'utf-8': Utf8Codec(),
+ 'utf8': Utf8Codec(),
+ };
+
+ static const _allowedFormats = {
+ 'PKBarcodeFormatQR': BarcodeType.QrCode,
+ 'PKBarcodeFormatPDF417': BarcodeType.PDF417,
+ 'PKBarcodeFormatAztec': BarcodeType.Aztec,
+ 'PKBarcodeFormatCode128': BarcodeType.Code128,
+ };
+
+ /// Barcode format. For the barcode dictionary, you can use only the
+ /// following values: PKBarcodeFormatQR, PKBarcodeFormatPDF417, or
+ /// PKBarcodeFormatAztec. For dictionaries in the barcodes array, you may
+ /// also use PKBarcodeFormatCode128.
+ ///
+ /// The spec defined keys are converted into the corresponding [BarcodeType]
+ /// representations.
+ final BarcodeType format;
+
+ /// Message or payload to be displayed as a barcode.
+ ///
+ /// Do not use directly, use the encoded [barcodeData] instead.
+ final String message;
+
+ /// Text encoding that is used to convert the message from the string
+ /// representation to a data representation to render the barcode.
+ ///
+ /// The value is typically iso-8859-1, but you may use another encoding that
+ /// is supported by your barcode scanning infrastructure.
+ ///
+ /// Only supported values by this packages are:
+ /// - iso-8859-1
+ /// - utf-8
+ ///
+ /// Custom codecs can be provided as [Codec] in [PassBarcode.supportedCodecs].
+ final Encoding messageEncoding;
+
+ /// Text displayed near the barcode. For example, a human-readable version
+ /// of the barcode data in case the barcode doesn’t scan.
+ final String? altText;
+
+ const PassBarcode({
+ required this.format,
+ required this.message,
+ required this.messageEncoding,
+ required this.altText,
+ });
+
+ factory PassBarcode.fromJson(Map json) => PassBarcode(
+ format: _allowedFormats[json['format']]!,
+ message: json['message'] as String,
+ messageEncoding:
+ supportedCodecs[(json['messageEncoding'] as String).toLowerCase()]!,
+ altText: json['altText'] as String?,
+ );
+
+ /// Correctly encoded byte list to be displayed in the [barcode].
+ Uint8List get barcodeData =>
+ Uint8List.fromList(messageEncoding.encode(message));
+}
diff --git a/lib/src/models/beacon.dart b/lib/src/models/beacon.dart
new file mode 100644
index 0000000..18596b9
--- /dev/null
+++ b/lib/src/models/beacon.dart
@@ -0,0 +1,30 @@
+/// Information about a location beacon.
+class Beacon {
+ /// Unique identifier of a Bluetooth Low Energy location beacon.
+ final String proximityUUID;
+
+ /// Major identifier of a Bluetooth Low Energy location beacon.
+ final double? major;
+
+ /// Minor identifier of a Bluetooth Low Energy location beacon.
+ final double? minor;
+
+ /// Text displayed on the lock screen when the pass is currently relevant.
+ /// For example, a description of the nearby location such as
+ /// “Store nearby on 1st and Main.”
+ final String? relevantText;
+
+ const Beacon({
+ required this.proximityUUID,
+ this.major,
+ this.minor,
+ this.relevantText,
+ });
+
+ factory Beacon.fromJson(Map json) => Beacon(
+ proximityUUID: json['proximityUUID'] as String,
+ major: json['major'] as double?,
+ minor: json['minor'] as double?,
+ relevantText: json['relevantText'] as String?,
+ );
+}
diff --git a/lib/src/models/location.dart b/lib/src/models/location.dart
new file mode 100644
index 0000000..2ea31a2
--- /dev/null
+++ b/lib/src/models/location.dart
@@ -0,0 +1,30 @@
+/// Information about a location.
+class Location {
+ /// Latitude, in degrees, of the location.
+ final double latitude;
+
+ /// Longitude, in degrees, of the location.
+ final double longitude;
+
+ /// Altitude, in meters, of the location.
+ final double? altitude;
+
+ /// Text displayed on the lock screen when the pass is currently relevant.
+ /// For example, a description of the nearby location such as
+ /// “Store nearby on 1st and Main.”
+ final String? relevantText;
+
+ const Location({
+ required this.latitude,
+ required this.longitude,
+ this.altitude,
+ this.relevantText,
+ });
+
+ factory Location.fromJson(Map json) => Location(
+ latitude: json['latitude'] as double,
+ longitude: json['longitude'] as double,
+ altitude: json['altitude'] as double?,
+ relevantText: json['relevantText'] as String?,
+ );
+}
diff --git a/lib/src/models/pass.dart b/lib/src/models/pass.dart
index 7f84551..3a85025 100644
--- a/lib/src/models/pass.dart
+++ b/lib/src/models/pass.dart
@@ -1,15 +1,106 @@
+import 'package:pkpass/src/models/barcode.dart';
+import 'package:pkpass/src/utils/mabe_decode.dart';
+import 'beacon.dart';
+import 'location.dart';
+import 'pass_structure_dictionary.dart';
+import 'pass_web_service.dart';
+
+/// Information that is required for all passes.
class PassMetadata {
- final String? description;
+ /// Brief description of the pass, used by accessibility technologies.
+ ///
+ /// Don’t try to include all of the data on the pass in its description,
+ /// just include enough detail to distinguish passes of the same type.
+ final String description;
+
+ /// Version of the file format. The value must be 1.
final int formatVersion;
- final String? organizationName;
- final String? passTypeIdentifier;
- final String? serialNumber;
- final String? teamIdentifier;
- final bool? suppressStripShine;
- final EventTicket? eventTicket;
- final Barcode? barcode;
- final String? foregroundColor;
- final List? locations;
+
+ /// Display name of the organization that originated and signed the pass.
+ final String organizationName;
+
+ /// Pass type identifier, as issued by Apple. The value must correspond with
+ /// your signing certificate.
+ final String passTypeIdentifier;
+
+ /// Serial number that uniquely identifies the pass. No two passes with the
+ /// same pass type identifier may have the same serial number.
+ final String serialNumber;
+
+ /// Team identifier of the organization that originated and signed the pass,
+ /// as issued by Apple.
+ final String teamIdentifier;
+
+ /// A URL to be passed to the associated app when launching it.
+ final String? appLaunchURL;
+
+ /// Date and time when the pass expires.
+ final DateTime? expirationDate;
+
+ /// Indicates that the pass is void—for example, a one time use coupon that
+ /// has been redeemed. The default value is false.
+ final bool voided;
+
+ /// Beacons marking locations where the pass is relevant.
+ final List beacons;
+
+ /// Locations where the pass is relevant. For example, the location of your store.
+ final List locations;
+
+ /// Maximum distance in meters from a relevant latitude and longitude that
+ /// the pass is relevant. This number is compared to the pass’s default
+ /// distance and the smaller value is used.
+ final int? maxDistance;
+
+ /// Recommended for event tickets and boarding passes; otherwise optional.
+ /// Date and time when the pass becomes relevant. For example, the start
+ /// time of a movie.
+ final DateTime? relevantDate;
+
+ /// Information specific to a boarding pass.
+ final PassStructureDictionary? boardingPass;
+
+ /// Information specific to a coupon.
+ final PassStructureDictionary? coupon;
+
+ /// Information specific to an event ticket.
+ final PassStructureDictionary? eventTicket;
+
+ /// Information specific to a generic pass.
+ final PassStructureDictionary? generic;
+
+ /// Information specific to a store card.
+ final PassStructureDictionary? storeCard;
+
+ /// Information specific to the pass’s barcode.
+ /// The system uses the first valid barcode dictionary in the array.
+ /// Additional dictionaries can be added as fallbacks.
+ final List barcodes;
+
+ /// Background color of the pass.
+ final int? backgroundColor;
+
+ /// Foreground color of the pass.
+ final int? foregroundColor;
+
+ /// Optional for event tickets and boarding passes; otherwise not allowed.
+ /// Identifier used to group related passes. If a grouping identifier is
+ /// specified, passes with the same style, pass type identifier, and grouping
+ /// identifier are displayed as a group. Otherwise, passes are grouped
+ /// automatically.
+ ///
+ /// Use this to group passes that are tightly related, such as the boarding
+ /// passes for different connections of the same trip.
+ final String? groupingIdentifier;
+
+ /// Color of the label text.
+ final int? labelColor;
+
+ /// Text displayed next to the logo on the pass.
+ final String? logoText;
+
+ /// Information used to update passes using the web service.
+ final PassWebService? webService;
const PassMetadata({
required this.description,
@@ -18,308 +109,86 @@ class PassMetadata {
required this.passTypeIdentifier,
required this.serialNumber,
required this.teamIdentifier,
- required this.suppressStripShine,
- required this.eventTicket,
- required this.barcode,
- required this.foregroundColor,
- required this.locations,
+ this.appLaunchURL,
+ this.expirationDate,
+ this.voided = false,
+ this.beacons = const [],
+ this.locations = const [],
+ this.maxDistance,
+ this.relevantDate,
+ this.boardingPass,
+ this.coupon,
+ this.eventTicket,
+ this.generic,
+ this.storeCard,
+ this.barcodes = const [],
+ this.backgroundColor,
+ this.foregroundColor,
+ this.groupingIdentifier,
+ this.labelColor,
+ this.logoText,
+ this.webService,
});
factory PassMetadata.fromJson(Map json) => PassMetadata(
- description: json['description'] as String?,
+ description: json['description'] as String,
formatVersion: json['formatVersion'] as int,
- organizationName: json['organizationName'] as String?,
- passTypeIdentifier: json['passTypeIdentifier'] as String?,
- serialNumber: json['serialNumber'] as String?,
- teamIdentifier: json['teamIdentifier'] as String?,
- suppressStripShine: json['suppressStripShine'] as bool?,
- eventTicket: json['ventTicket'] == null
+ organizationName: json['organizationName'] as String,
+ passTypeIdentifier: json['passTypeIdentifier'] as String,
+ serialNumber: json['serialNumber'] as String,
+ teamIdentifier: json['teamIdentifier'] as String,
+ boardingPass: json['boardingPass'] == null
? null
- : EventTicket.fromJson(
- json['eventTicket'] as Map? ?? {},
+ : PassStructureDictionary.fromJson(
+ (json['boardingPass'] as Map).cast(),
),
- barcode: json['barcode'] == null
+ coupon: json['coupon'] == null
? null
- : Barcode.fromJson(json['barcode'] as Map? ?? {}),
- foregroundColor: json['foregroundColor'] as String?,
- locations: (json['locations'] as List)
+ : PassStructureDictionary.fromJson(
+ (json['coupon'] as Map).cast(),
+ ),
+ eventTicket: json['eventTicket'] == null
+ ? null
+ : PassStructureDictionary.fromJson(
+ (json['coupon'] as Map).cast(),
+ ),
+ generic: json['generic'] == null
+ ? null
+ : PassStructureDictionary.fromJson(
+ (json['generic'] as Map).cast(),
+ ),
+ storeCard: json['storeCard'] == null
+ ? null
+ : PassStructureDictionary.fromJson(
+ (json['storeCard'] as Map).cast(),
+ ),
+ barcodes: (json['barcodes'] as List? ??
+ [if (json['barcode'] != null) json['barcode']])
+ .map((i) => PassBarcode.fromJson(i))
+ .toList(),
+ locations: (json['locations'] as List? ?? [])
.map((i) => Location.fromJson(i))
.toList(),
- );
-
- Map toJson() => {
- 'description': description,
- 'formatVersion': formatVersion,
- 'organizationName': organizationName,
- 'passTypeIdentifier': passTypeIdentifier,
- 'serialNumber': serialNumber,
- 'teamIdentifier': teamIdentifier,
- 'suppressStripShine': suppressStripShine,
- 'eventTicket': eventTicket?.toJson(),
- 'barcode': barcode?.toJson(),
- 'foregroundColor': foregroundColor,
- 'locations': locations?.map((i) => i.toJson()).toList(),
- };
-}
-
-class EventTicket {
- final List headerFields;
- final List primaryFields;
- final List secondaryFields;
- final List backFields;
- final List auxiliaryFields;
-
- const EventTicket({
- required this.headerFields,
- required this.primaryFields,
- required this.secondaryFields,
- required this.backFields,
- required this.auxiliaryFields,
- });
-
- factory EventTicket.fromJson(Map json) => EventTicket(
- headerFields: (json['headerFields'] as List)
- .map((i) => HeaderField.fromJson(i))
- .toList(),
- primaryFields: (json['primaryFields'] as List)
- .map((i) => PrimaryField.fromJson(i))
- .toList(),
- secondaryFields: (json['secondaryFields'] as List)
- .map((i) => SecondaryField.fromJson(i))
- .toList(),
- backFields: (json['backFields'] as List)
- .map((i) => BackField.fromJson(i))
- .toList(),
- auxiliaryFields: (json['auxiliaryFields'] as List)
- .map((i) => AuxiliaryField.fromJson(i))
+ appLaunchURL: json['appLaunchURL'] as String?,
+ expirationDate:
+ MaybeDecode.maybeDateTime(json['expirationDate'] as String?),
+ voided: json['voided'] as bool? ?? false,
+ beacons: (json['beacons'] as List? ?? [])
+ .map((i) => Beacon.fromJson(i))
.toList(),
+ maxDistance: json['maxDistance'] as int?,
+ relevantDate:
+ MaybeDecode.maybeDateTime(json['relevantDate'] as String?),
+ backgroundColor:
+ MaybeDecode.maybeColor(json['backgroundColor'] as String?),
+ foregroundColor:
+ MaybeDecode.maybeColor(json['foregroundColor'] as String?),
+ groupingIdentifier: json['locoText'] as String?,
+ labelColor: MaybeDecode.maybeColor(json['labelColor'] as String?),
+ logoText: json['locoText'] as String?,
+ webService: PassWebService.maybe(
+ authenticationToken: json['authenticationToken'] as String?,
+ webServiceURL: json['webServiceURL'] as String?,
+ ),
);
-
- Map toJson() => {
- 'headerFields': headerFields.map((i) => i.toJson()).toList(),
- 'primaryFields': primaryFields.map((i) => i.toJson()).toList(),
- 'secondaryFields': secondaryFields.map((i) => i.toJson()).toList(),
- 'backFields': backFields.map((i) => i.toJson()).toList(),
- 'auxiliaryFields': auxiliaryFields.map((i) => i.toJson()).toList(),
- };
-}
-
-class HeaderField {
- final String? key;
- final String? value;
- final String? label;
- final String? changeMessage;
- final String? textAlignment;
-
- const HeaderField({
- required this.key,
- required this.value,
- required this.label,
- required this.changeMessage,
- required this.textAlignment,
- });
-
- factory HeaderField.fromJson(Map json) => HeaderField(
- key: json['key'] as String?,
- value: json['value'] as String?,
- label: json['label'] as String?,
- changeMessage: json['changeMessage'] as String?,
- textAlignment: json['textAlignment'] as String?,
- );
-
- Map toJson() => {
- 'key': key,
- 'value': value,
- 'label': label,
- 'changeMessage': changeMessage,
- 'textAlignment': textAlignment,
- };
-}
-
-class PrimaryField {
- final String? key;
- final String? value;
- final String? label;
- final String? changeMessage;
- final String? textAlignment;
-
- const PrimaryField({
- required this.key,
- required this.value,
- required this.label,
- required this.changeMessage,
- required this.textAlignment,
- });
-
- factory PrimaryField.fromJson(Map json) => PrimaryField(
- key: json['key'] as String?,
- value: json['value'] as String?,
- label: json['label'] as String?,
- changeMessage: json['changeMessage'] as String?,
- textAlignment: json['textAlignment'] as String?,
- );
-
- Map toJson() => {
- 'key': key,
- 'value': value,
- 'label': label,
- 'changeMessage': changeMessage,
- 'textAlignment': textAlignment,
- };
-}
-
-class SecondaryField {
- final String? key;
- final String? value;
- final String? label;
- final String? changeMessage;
- final String? textAlignment;
-
- const SecondaryField({
- required this.key,
- required this.value,
- required this.label,
- required this.changeMessage,
- required this.textAlignment,
- });
-
- factory SecondaryField.fromJson(Map json) => SecondaryField(
- key: json['key'] as String?,
- value: json['value'] as String?,
- label: json['label'] as String?,
- changeMessage: json['changeMessage'] as String?,
- textAlignment: json['textAlignment'] as String?,
- );
-
- Map toJson() => {
- 'key': key,
- 'value': value,
- 'label': label,
- 'changeMessage': changeMessage,
- 'textAlignment': textAlignment,
- };
-}
-
-class BackField {
- final String? key;
- final String? value;
- final String? label;
- final String? changeMessage;
- final String? textAlignment;
-
- const BackField({
- required this.key,
- required this.value,
- required this.label,
- required this.changeMessage,
- required this.textAlignment,
- });
-
- factory BackField.fromJson(Map json) => BackField(
- key: json['key'] as String?,
- value: json['value'] as String?,
- label: json['label'] as String?,
- changeMessage: json['changeMessage'] as String?,
- textAlignment: json['textAlignment'] as String?,
- );
-
- Map toJson() => {
- 'key': key,
- 'value': value,
- 'label': label,
- 'changeMessage': changeMessage,
- 'textAlignment': textAlignment,
- };
-}
-
-class AuxiliaryField {
- final String? key;
- final String? value;
- final String? label;
- final String? changeMessage;
- final String? textAlignment;
-
- const AuxiliaryField({
- required this.key,
- required this.value,
- required this.label,
- required this.changeMessage,
- required this.textAlignment,
- });
-
- factory AuxiliaryField.fromJson(Map json) => AuxiliaryField(
- key: json['key'] as String?,
- value: json['value'] as String?,
- label: json['label'] as String?,
- changeMessage: json['changeMessage'] as String?,
- textAlignment: json['textAlignment'] as String?,
- );
-
- Map toJson() => {
- 'key': key,
- 'value': value,
- 'label': label,
- 'changeMessage': changeMessage,
- 'textAlignment': textAlignment,
- };
-}
-
-class Barcode {
- final String? format;
- final String? message;
- final String? messageEncoding;
- final String? altText;
-
- const Barcode({
- required this.format,
- required this.message,
- required this.messageEncoding,
- required this.altText,
- });
-
- factory Barcode.fromJson(Map json) => Barcode(
- format: json['format'] as String?,
- message: json['message'] as String?,
- messageEncoding: json['messageEncoding'] as String?,
- altText: json['altText'] as String?,
- );
-
- Map toJson() => {
- 'format': format,
- 'message': message,
- 'messageEncoding': messageEncoding,
- 'altText': altText,
- };
-}
-
-class Location {
- final double? latitude;
- final double? longitude;
- final double? altitude;
- final double? distance;
- final String? relevantText;
-
- const Location({
- required this.latitude,
- required this.longitude,
- required this.altitude,
- required this.distance,
- required this.relevantText,
- });
-
- factory Location.fromJson(Map json) => Location(
- latitude: json['latitude'] as double?,
- longitude: json['longitude'] as double?,
- altitude: json['altitude'] as double?,
- distance: json['distance'] as double?,
- relevantText: json['relevantText'] as String?,
- );
-
- Map toJson() => {
- 'latitude': latitude,
- 'longitude': longitude,
- 'altitude': altitude,
- 'distance': distance,
- 'relevantText': relevantText,
- };
}
diff --git a/lib/src/models/pass_structure_dictionary.dart b/lib/src/models/pass_structure_dictionary.dart
new file mode 100644
index 0000000..fc9e8fd
--- /dev/null
+++ b/lib/src/models/pass_structure_dictionary.dart
@@ -0,0 +1,171 @@
+import 'package:pkpass/src/utils/mabe_decode.dart';
+
+/// Keys that define the structure of the pass.
+///
+/// These keys are used for all pass styles and partition the fields into the various parts of the pass.
+class PassStructureDictionary {
+ /// Fields to be displayed in the header on the front of the pass.
+ ///
+ /// Use header fields sparingly; unlike all other fields, they remain visible when a stack of passes are displayed.
+ final List headerFields;
+
+ /// Fields to be displayed prominently on the front of the pass.
+ final List primaryFields;
+
+ /// Fields to be displayed on the front of the pass.
+ final List secondaryFields;
+
+ /// Fields to be on the back of the pass.
+ final List backFields;
+
+ /// Additional fields to be displayed on the front of the pass.
+ final List auxiliaryFields;
+
+ /// Required for boarding passes; otherwise not allowed. Type of transit.
+ final TransitType? transitType;
+
+ const PassStructureDictionary({
+ this.headerFields = const [],
+ this.primaryFields = const [],
+ this.secondaryFields = const [],
+ this.backFields = const [],
+ this.auxiliaryFields = const [],
+ this.transitType,
+ });
+
+ factory PassStructureDictionary.fromJson(Map json) =>
+ PassStructureDictionary(
+ headerFields: (json['headerFields'] as List?)
+ ?.map((i) => DictionaryField.fromJson(i))
+ .toList() ??
+ [],
+ primaryFields: (json['primaryFields'] as List?)
+ ?.map((i) => DictionaryField.fromJson(i))
+ .toList() ??
+ [],
+ secondaryFields: (json['secondaryFields'] as List?)
+ ?.map((i) => DictionaryField.fromJson(i))
+ .toList() ??
+ [],
+ backFields: (json['backFields'] as List?)
+ ?.map((i) => DictionaryField.fromJson(i))
+ .toList() ??
+ [],
+ auxiliaryFields: (json['auxiliaryFields'] as List?)
+ ?.map((i) => DictionaryField.fromJson(i))
+ .toList() ??
+ [],
+ transitType: _TarnsitType.parse(json['transitType'] as String?),
+ );
+}
+
+/// Information about a field.
+class DictionaryField {
+ /// The key must be unique within the scope of the entire pass. For example, “departure-gate.”
+ final String key;
+
+ /// Value of the field, for example, 42.
+ final DictionaryValue value;
+
+ /// Label text for the field.
+ final String? label;
+
+ /// Format string for the alert text that is displayed when the pass is updated. The format string must contain the escape %@, which is replaced with the field’s new value. For example, “Gate changed to %@.”
+ ///
+ /// If you don’t specify a change message, the user isn’t notified when the field changes.
+ final String? changeMessage;
+
+ /// Alignment for the field’s contents.
+ final PassTextAlign? textAlignment;
+
+ /// Attributed value of the field.
+ ///
+ /// The value may contain HTML markup for links. Only the tag and its href attribute are supported. For example, the following is key-value pair specifies a link with the text “Edit my profile”:
+ ///
+ /// "attributedValue": "Edit my profile"
+ ///
+ /// This key’s value overrides the text specified by the value key.
+ final DictionaryValue? attributedValue;
+
+ const DictionaryField({
+ required this.key,
+ required this.value,
+ this.label,
+ this.changeMessage,
+ this.textAlignment,
+ this.attributedValue,
+ });
+
+ factory DictionaryField.fromJson(Map json) =>
+ DictionaryField(
+ key: json['key'] as String,
+ value: DictionaryValue.parse(json['value'] as String),
+ label: json['label'] as String?,
+ changeMessage: json['changeMessage'] as String?,
+ textAlignment:
+ MaybeDecode.maybeTextAlign(json['textAlignment'] as String?),
+ attributedValue: json['attributedValue'] == null
+ ? null
+ : DictionaryValue.parse(json['attributedValue'] as String),
+ );
+}
+
+abstract class DictionaryValue {
+ const DictionaryValue();
+
+ factory DictionaryValue.parse(String value) {
+ final number = int.tryParse(value);
+ if (number != null) return NumberDictionaryValue(number);
+
+ final dateTime = DateTime.tryParse(value);
+ if (dateTime != null) return DateTimeDictionaryValue(dateTime);
+
+ return StringDictionaryValue(value);
+ }
+}
+
+class StringDictionaryValue extends DictionaryValue {
+ final String string;
+
+ const StringDictionaryValue(this.string);
+}
+
+class DateTimeDictionaryValue extends DictionaryValue {
+ final DateTime dateTime;
+
+ const DateTimeDictionaryValue(this.dateTime);
+}
+
+class NumberDictionaryValue extends DictionaryValue {
+ final int number;
+
+ const NumberDictionaryValue(this.number);
+}
+
+enum TransitType {
+ air,
+ boat,
+ bus,
+ generic,
+ train,
+}
+
+enum PassTextAlign { left, center, right, natural }
+
+extension _TarnsitType on TransitType {
+ static TransitType? parse(String? type) {
+ if (type == null) return null;
+ switch (type) {
+ case 'PKTransitTypeAir':
+ return TransitType.air;
+ case 'PKTransitTypeBoat':
+ return TransitType.boat;
+ case 'PKTransitTypeBus':
+ return TransitType.bus;
+ case 'PKTransitTypeTrain':
+ return TransitType.train;
+ default:
+ return TransitType.generic;
+ }
+ }
+}
diff --git a/lib/src/models/pass_web_service.dart b/lib/src/models/pass_web_service.dart
new file mode 100644
index 0000000..41d4140
--- /dev/null
+++ b/lib/src/models/pass_web_service.dart
@@ -0,0 +1,31 @@
+/// TODO: implement PassKit Web Service Reference
+///
+/// Metadata required for Pass Web Service
+///
+/// https://developer.apple.com/library/archive/documentation/PassKit/Reference/PassKit_WebService/WebService.html#//apple_ref/doc/uid/TP40011988
+class PassWebService {
+ /// The authentication token to use with the web service.
+ /// The token must be 16 characters or longer.
+ final String authenticationToken;
+
+ /// The URL of a web service that conforms to the API described in PassKit Web Service Reference.
+ final Uri webServiceURL;
+
+ const PassWebService({
+ required this.authenticationToken,
+ required this.webServiceURL,
+ });
+
+ static PassWebService? maybe({
+ String? authenticationToken,
+ String? webServiceURL,
+ }) {
+ if (authenticationToken == null || webServiceURL == null) return null;
+ final uri = Uri.tryParse(webServiceURL);
+ if (uri == null || uri.scheme != 'https') return null;
+ return PassWebService(
+ authenticationToken: authenticationToken,
+ webServiceURL: uri,
+ );
+ }
+}
diff --git a/lib/src/utils/color_helper.dart b/lib/src/utils/color_helper.dart
new file mode 100644
index 0000000..eefa3af
--- /dev/null
+++ b/lib/src/utils/color_helper.dart
@@ -0,0 +1,71 @@
+/// Creates in [int] with a parsed [Color] value from RGB(A) color value.
+///
+/// Credits : https://pub.dev/packages/from_css_color - ported since dependency
+/// on `dart:ui`, here reimplemented without but with raw int.
+int parseRgbToInt(String color) {
+ List channels = _parseChannels(color)!;
+ int result = 0xFF000000;
+ int shift = 16;
+
+ if (channels.length == 4) {
+ result = (_opacityChannelToHex(channels.removeLast()) << 24) & result;
+ } else if (channels.length != 3) {
+ throw FormatException(
+ 'Incorrect number of values in RGB color string, there must be 3 or 4 of them.',
+ color,
+ );
+ }
+
+ if (_isPercentFormat(channels)) {
+ for (var ch in channels) {
+ result = (_rgbChannelPercentToHex(ch) << shift) | result;
+ shift -= 8;
+ }
+ } else {
+ for (var ch in channels) {
+ result = (_rgbChannelNumToHex(ch) << shift) | result;
+ shift -= 8;
+ }
+ }
+
+ return result;
+}
+
+/// Parses channels from RGBA/HSLA string representation.
+List? _parseChannels(color) {
+ return color.substring(color.indexOf('(') + 1, color.length - 1).split(',');
+}
+
+/// Converts RGB channel numeric [value] to hexadecimal integer form.
+int _rgbChannelNumToHex(String value) {
+ return double.parse(value).clamp(0, 255).floor();
+}
+
+/// Returns `true` if all [rgb] channels are in percent format, `false` in non of them, throws otherwise.
+bool _isPercentFormat(List rgb) {
+ if (rgb.every((ch) => ch.endsWith('%'))) {
+ return true;
+ } else if (rgb.every((ch) => !ch.endsWith('%'))) {
+ return false;
+ } else {
+ throw FormatException(
+ 'Mixing integer and percentage values in the same RGB color definition is forbidden.',
+ rgb.toString(),
+ );
+ }
+}
+
+/// Converts RGBA/HSLA opacity channel [value] to hexadecimal integer form.
+int _opacityChannelToHex(String value) {
+ return (double.parse(value).clamp(0, 1) * 255).floor();
+}
+
+/// Converts RGB channel percentage [value] to hexadecimal integer form.
+int _rgbChannelPercentToHex(String value) {
+ return (_parsePercent(value) * 255 / 100).floor();
+}
+
+/// Parses numeric value from string [percent] representation.
+num _parsePercent(String percent) {
+ return (double.parse(percent.substring(0, percent.length - 1)).clamp(0, 100));
+}
diff --git a/lib/src/utils/mabe_decode.dart b/lib/src/utils/mabe_decode.dart
new file mode 100644
index 0000000..99f0146
--- /dev/null
+++ b/lib/src/utils/mabe_decode.dart
@@ -0,0 +1,29 @@
+import '../models/pass_structure_dictionary.dart';
+import 'color_helper.dart';
+
+abstract class MaybeDecode {
+ const MaybeDecode._();
+
+ static int? maybeColor(String? colorCode) {
+ if (colorCode == null) return null;
+ return parseRgbToInt(colorCode);
+ }
+
+ static DateTime? maybeDateTime(String? timeStamp) {
+ if (timeStamp == null) return null;
+ return DateTime.tryParse(timeStamp);
+ }
+
+ static PassTextAlign? maybeTextAlign(String? align) {
+ switch (align) {
+ case 'PKTextAlignmentLeft':
+ return PassTextAlign.left;
+ case 'PKTextAlignmentCenter':
+ return PassTextAlign.center;
+ case 'PKTextAlignmentRight':
+ return PassTextAlign.right;
+ default:
+ return PassTextAlign.natural;
+ }
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 337d65d..cb3bfd8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -10,6 +10,7 @@ environment:
dependencies:
archive: ^3.3.7
+ barcode: ^2.2.4
crypto: ^3.0.3
intl: ^0.18.1
diff --git a/test/pkpass_test.dart b/test/pkpass_test.dart
index b22f0d4..0394b90 100644
--- a/test/pkpass_test.dart
+++ b/test/pkpass_test.dart
@@ -23,17 +23,12 @@ void main() {
// ignore: unused_local_variable
final file = PassFile(
PassMetadata(
- description: null,
- formatVersion: 0,
- organizationName: null,
- passTypeIdentifier: null,
- serialNumber: null,
- teamIdentifier: null,
- suppressStripShine: null,
- eventTicket: null,
- barcode: null,
- foregroundColor: null,
- locations: [],
+ description: '',
+ formatVersion: 1,
+ organizationName: '',
+ passTypeIdentifier: '',
+ serialNumber: '',
+ teamIdentifier: '',
),
archive,
);