mirror of
https://gitlab.com/TheOneWithTheBraid/dart_pkpass.git
synced 2025-07-05 12:58:47 +00:00
chore: add high level classes
Signed-off-by: The one with the braid <the-one@with-the-braid.cf>
This commit is contained in:
parent
165226bee1
commit
78f88305ec
13 changed files with 678 additions and 316 deletions
20
.idea/libraries/Dart_Packages.xml
generated
20
.idea/libraries/Dart_Packages.xml
generated
|
@ -37,6 +37,13 @@
|
||||||
</list>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry key="barcode">
|
||||||
|
<value>
|
||||||
|
<list>
|
||||||
|
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/barcode-2.2.4/lib" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
<entry key="boolean_selector">
|
<entry key="boolean_selector">
|
||||||
<value>
|
<value>
|
||||||
<list>
|
<list>
|
||||||
|
@ -54,7 +61,7 @@
|
||||||
<entry key="collection">
|
<entry key="collection">
|
||||||
<value>
|
<value>
|
||||||
<list>
|
<list>
|
||||||
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/collection-1.18.0/lib" />
|
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/collection-1.17.2/lib" />
|
||||||
</list>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</entry>
|
||||||
|
@ -219,6 +226,13 @@
|
||||||
</list>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry key="qr">
|
||||||
|
<value>
|
||||||
|
<list>
|
||||||
|
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/qr-3.0.1/lib" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
<entry key="shelf">
|
<entry key="shelf">
|
||||||
<value>
|
<value>
|
||||||
<list>
|
<list>
|
||||||
|
@ -374,9 +388,10 @@
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/archive-3.3.7/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/archive-3.3.7/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/args-2.4.2/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/args-2.4.2/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/async-2.11.0/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/async-2.11.0/lib" />
|
||||||
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/barcode-2.2.4/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/clock-1.1.1/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/clock-1.1.1/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/collection-1.18.0/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/collection-1.17.2/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/convert-3.1.1/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/convert-3.1.1/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/coverage-1.6.3/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/coverage-1.6.3/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/crypto-3.0.3/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/crypto-3.0.3/lib" />
|
||||||
|
@ -400,6 +415,7 @@
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/pointycastle-3.7.3/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/pointycastle-3.7.3/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/pool-1.5.1/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/pool-1.5.1/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/pub_semver-2.1.4/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/pub_semver-2.1.4/lib" />
|
||||||
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/qr-3.0.1/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf-1.4.1/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf-1.4.1/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.2/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.2/lib" />
|
||||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_static-1.1.2/lib" />
|
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_static-1.1.2/lib" />
|
||||||
|
|
27
README.md
27
README.md
|
@ -6,8 +6,35 @@ A Dart native pkpass parsing library
|
||||||
|
|
||||||
- no platform specific dependencies - pure Dart
|
- no platform specific dependencies - pure Dart
|
||||||
- parse any .pkpass file as blob
|
- parse any .pkpass file as blob
|
||||||
|
- checksum verification
|
||||||
- extract metadata
|
- extract metadata
|
||||||
- high level lookup for assets by locale and scale, with proper fallbacks
|
- 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
|
## Getting started
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,9 @@ Future<int> main(List<String> args) async {
|
||||||
);
|
);
|
||||||
|
|
||||||
print('Logo image blob length: ${logo?.length}');
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
89
lib/src/models/barcode.dart
Normal file
89
lib/src/models/barcode.dart
Normal file
|
@ -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<String, Encoding> 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<String, Object?> 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));
|
||||||
|
}
|
30
lib/src/models/beacon.dart
Normal file
30
lib/src/models/beacon.dart
Normal file
|
@ -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<String, Object?> json) => Beacon(
|
||||||
|
proximityUUID: json['proximityUUID'] as String,
|
||||||
|
major: json['major'] as double?,
|
||||||
|
minor: json['minor'] as double?,
|
||||||
|
relevantText: json['relevantText'] as String?,
|
||||||
|
);
|
||||||
|
}
|
30
lib/src/models/location.dart
Normal file
30
lib/src/models/location.dart
Normal file
|
@ -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<String, Object?> json) => Location(
|
||||||
|
latitude: json['latitude'] as double,
|
||||||
|
longitude: json['longitude'] as double,
|
||||||
|
altitude: json['altitude'] as double?,
|
||||||
|
relevantText: json['relevantText'] as String?,
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 {
|
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 int formatVersion;
|
||||||
final String? organizationName;
|
|
||||||
final String? passTypeIdentifier;
|
/// Display name of the organization that originated and signed the pass.
|
||||||
final String? serialNumber;
|
final String organizationName;
|
||||||
final String? teamIdentifier;
|
|
||||||
final bool? suppressStripShine;
|
/// Pass type identifier, as issued by Apple. The value must correspond with
|
||||||
final EventTicket? eventTicket;
|
/// your signing certificate.
|
||||||
final Barcode? barcode;
|
final String passTypeIdentifier;
|
||||||
final String? foregroundColor;
|
|
||||||
final List<Location>? locations;
|
/// 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<Beacon> beacons;
|
||||||
|
|
||||||
|
/// Locations where the pass is relevant. For example, the location of your store.
|
||||||
|
final List<Location> 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<PassBarcode> 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({
|
const PassMetadata({
|
||||||
required this.description,
|
required this.description,
|
||||||
|
@ -18,308 +109,86 @@ class PassMetadata {
|
||||||
required this.passTypeIdentifier,
|
required this.passTypeIdentifier,
|
||||||
required this.serialNumber,
|
required this.serialNumber,
|
||||||
required this.teamIdentifier,
|
required this.teamIdentifier,
|
||||||
required this.suppressStripShine,
|
this.appLaunchURL,
|
||||||
required this.eventTicket,
|
this.expirationDate,
|
||||||
required this.barcode,
|
this.voided = false,
|
||||||
required this.foregroundColor,
|
this.beacons = const [],
|
||||||
required this.locations,
|
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<String, Object?> json) => PassMetadata(
|
factory PassMetadata.fromJson(Map<String, Object?> json) => PassMetadata(
|
||||||
description: json['description'] as String?,
|
description: json['description'] as String,
|
||||||
formatVersion: json['formatVersion'] as int,
|
formatVersion: json['formatVersion'] as int,
|
||||||
organizationName: json['organizationName'] as String?,
|
organizationName: json['organizationName'] as String,
|
||||||
passTypeIdentifier: json['passTypeIdentifier'] as String?,
|
passTypeIdentifier: json['passTypeIdentifier'] as String,
|
||||||
serialNumber: json['serialNumber'] as String?,
|
serialNumber: json['serialNumber'] as String,
|
||||||
teamIdentifier: json['teamIdentifier'] as String?,
|
teamIdentifier: json['teamIdentifier'] as String,
|
||||||
suppressStripShine: json['suppressStripShine'] as bool?,
|
boardingPass: json['boardingPass'] == null
|
||||||
eventTicket: json['ventTicket'] == null
|
|
||||||
? null
|
? null
|
||||||
: EventTicket.fromJson(
|
: PassStructureDictionary.fromJson(
|
||||||
json['eventTicket'] as Map<String, Object?>? ?? {},
|
(json['boardingPass'] as Map).cast<String, Object?>(),
|
||||||
),
|
),
|
||||||
barcode: json['barcode'] == null
|
coupon: json['coupon'] == null
|
||||||
? null
|
? null
|
||||||
: Barcode.fromJson(json['barcode'] as Map<String, Object?>? ?? {}),
|
: PassStructureDictionary.fromJson(
|
||||||
foregroundColor: json['foregroundColor'] as String?,
|
(json['coupon'] as Map).cast<String, Object?>(),
|
||||||
locations: (json['locations'] as List)
|
),
|
||||||
|
eventTicket: json['eventTicket'] == null
|
||||||
|
? null
|
||||||
|
: PassStructureDictionary.fromJson(
|
||||||
|
(json['coupon'] as Map).cast<String, Object?>(),
|
||||||
|
),
|
||||||
|
generic: json['generic'] == null
|
||||||
|
? null
|
||||||
|
: PassStructureDictionary.fromJson(
|
||||||
|
(json['generic'] as Map).cast<String, Object?>(),
|
||||||
|
),
|
||||||
|
storeCard: json['storeCard'] == null
|
||||||
|
? null
|
||||||
|
: PassStructureDictionary.fromJson(
|
||||||
|
(json['storeCard'] as Map).cast<String, Object?>(),
|
||||||
|
),
|
||||||
|
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))
|
.map((i) => Location.fromJson(i))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
appLaunchURL: json['appLaunchURL'] as String?,
|
||||||
|
expirationDate:
|
||||||
Map<String, Object?> toJson() => {
|
MaybeDecode.maybeDateTime(json['expirationDate'] as String?),
|
||||||
'description': description,
|
voided: json['voided'] as bool? ?? false,
|
||||||
'formatVersion': formatVersion,
|
beacons: (json['beacons'] as List? ?? [])
|
||||||
'organizationName': organizationName,
|
.map((i) => Beacon.fromJson(i))
|
||||||
'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<HeaderField> headerFields;
|
|
||||||
final List<PrimaryField> primaryFields;
|
|
||||||
final List<SecondaryField> secondaryFields;
|
|
||||||
final List<BackField> backFields;
|
|
||||||
final List<AuxiliaryField> auxiliaryFields;
|
|
||||||
|
|
||||||
const EventTicket({
|
|
||||||
required this.headerFields,
|
|
||||||
required this.primaryFields,
|
|
||||||
required this.secondaryFields,
|
|
||||||
required this.backFields,
|
|
||||||
required this.auxiliaryFields,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory EventTicket.fromJson(Map<String, Object?> 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))
|
|
||||||
.toList(),
|
.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<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> json) => Barcode(
|
|
||||||
format: json['format'] as String?,
|
|
||||||
message: json['message'] as String?,
|
|
||||||
messageEncoding: json['messageEncoding'] as String?,
|
|
||||||
altText: json['altText'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, Object?> 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<String, Object?> 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<String, Object?> toJson() => {
|
|
||||||
'latitude': latitude,
|
|
||||||
'longitude': longitude,
|
|
||||||
'altitude': altitude,
|
|
||||||
'distance': distance,
|
|
||||||
'relevantText': relevantText,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
171
lib/src/models/pass_structure_dictionary.dart
Normal file
171
lib/src/models/pass_structure_dictionary.dart
Normal file
|
@ -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<DictionaryField> headerFields;
|
||||||
|
|
||||||
|
/// Fields to be displayed prominently on the front of the pass.
|
||||||
|
final List<DictionaryField> primaryFields;
|
||||||
|
|
||||||
|
/// Fields to be displayed on the front of the pass.
|
||||||
|
final List<DictionaryField> secondaryFields;
|
||||||
|
|
||||||
|
/// Fields to be on the back of the pass.
|
||||||
|
final List<DictionaryField> backFields;
|
||||||
|
|
||||||
|
/// Additional fields to be displayed on the front of the pass.
|
||||||
|
final List<DictionaryField> 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<String, Object?> 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 <a> 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": "<a href='http://example.com/customers/123'>Edit my profile</a>"
|
||||||
|
///
|
||||||
|
/// 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<String, Object?> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
lib/src/models/pass_web_service.dart
Normal file
31
lib/src/models/pass_web_service.dart
Normal file
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
71
lib/src/utils/color_helper.dart
Normal file
71
lib/src/utils/color_helper.dart
Normal file
|
@ -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<String> 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<String>? _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<String> 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));
|
||||||
|
}
|
29
lib/src/utils/mabe_decode.dart
Normal file
29
lib/src/utils/mabe_decode.dart
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ environment:
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
archive: ^3.3.7
|
archive: ^3.3.7
|
||||||
|
barcode: ^2.2.4
|
||||||
crypto: ^3.0.3
|
crypto: ^3.0.3
|
||||||
intl: ^0.18.1
|
intl: ^0.18.1
|
||||||
|
|
||||||
|
|
|
@ -23,17 +23,12 @@ void main() {
|
||||||
// ignore: unused_local_variable
|
// ignore: unused_local_variable
|
||||||
final file = PassFile(
|
final file = PassFile(
|
||||||
PassMetadata(
|
PassMetadata(
|
||||||
description: null,
|
description: '',
|
||||||
formatVersion: 0,
|
formatVersion: 1,
|
||||||
organizationName: null,
|
organizationName: '',
|
||||||
passTypeIdentifier: null,
|
passTypeIdentifier: '',
|
||||||
serialNumber: null,
|
serialNumber: '',
|
||||||
teamIdentifier: null,
|
teamIdentifier: '',
|
||||||
suppressStripShine: null,
|
|
||||||
eventTicket: null,
|
|
||||||
barcode: null,
|
|
||||||
foregroundColor: null,
|
|
||||||
locations: [],
|
|
||||||
),
|
),
|
||||||
archive,
|
archive,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue