From cd0f9295c16e3aebbf2a0ceb9f626f4394a239bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 7 Jul 2024 13:25:44 +0200 Subject: [PATCH 01/19] wip --- passkit/lib/passkit.dart | 17 +++++++++++++++++ passkit_ui/lib/src/order/order_widget.dart | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 passkit_ui/lib/src/order/order_widget.dart diff --git a/passkit/lib/passkit.dart b/passkit/lib/passkit.dart index 0c0988f..e3cfca4 100644 --- a/passkit/lib/passkit.dart +++ b/passkit/lib/passkit.dart @@ -1,5 +1,22 @@ library; +// Order +export 'src/order/order_address.dart'; +export 'src/order/order_application.dart'; +export 'src/order/order_barcode.dart'; +export 'src/order/order_customer.dart'; +export 'src/order/order_data.dart'; +export 'src/order/order_line_item.dart'; +export 'src/order/order_location.dart'; +export 'src/order/order_merchant.dart'; +export 'src/order/order_payment.dart'; +export 'src/order/order_pickup_fulfillment.dart'; +export 'src/order/order_provider.dart'; +export 'src/order/order_return.dart'; +export 'src/order/order_return_info.dart'; +export 'src/order/order_shipping_fulfillment.dart'; +export 'src/order/pk_order.dart'; +// PkPass export 'src/pkpass/barcode.dart'; export 'src/pkpass/beacon.dart'; export 'src/pkpass/field_dict.dart'; diff --git a/passkit_ui/lib/src/order/order_widget.dart b/passkit_ui/lib/src/order/order_widget.dart new file mode 100644 index 0000000..0f4f07a --- /dev/null +++ b/passkit_ui/lib/src/order/order_widget.dart @@ -0,0 +1,21 @@ +import 'package:flutter/widgets.dart'; +import 'package:passkit/passkit.dart'; + +class OrderWidget extends StatelessWidget { + const OrderWidget({super.key, required this.order}); + + final PkOrder order; + + // Build the UI according to https://docs-assets.developer.apple.com/published/e5ec23af37b6a9d9cbc90e5d5f47bf8a/wallet-ot-status-on-the-way-fields@2x.png + // https://developer.apple.com/design/human-interface-guidelines/wallet#Order-tracking + @override + Widget build(BuildContext context) { + return Column( + children: [ + // TODO(any): Add merchant logo here + Text(order.order.merchant.displayName), + // TODO(any): Add date of order here + ], + ); + } +} From 5be0302bc498e2eb192647878d3f73067bc052ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 14 Jul 2024 08:20:54 +0200 Subject: [PATCH 02/19] add translations --- passkit_ui/lib/src/order/l10n.dart | 158 +++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 passkit_ui/lib/src/order/l10n.dart diff --git a/passkit_ui/lib/src/order/l10n.dart b/passkit_ui/lib/src/order/l10n.dart new file mode 100644 index 0000000..5bb015c --- /dev/null +++ b/passkit_ui/lib/src/order/l10n.dart @@ -0,0 +1,158 @@ +import 'package:flutter/widgets.dart'; + +abstract class OrderLocalizations { + /// Creates a [OrderLocalizations]. + const OrderLocalizations(); + + String orderedAt(DateTime date); + String get courier; + String get trackingId; + String get trackShipment; + String from(String merchant); + String get deliveredStatus; + String get outForDeliveryStatus; + String get orderPlacedStatus; + String get details; + String get orderId; + String get orderTotal; + String get manageOrder; + String get visitMerchantWebsite; + String get subtotal; + String get coupon; + String get tax; + String get total; + String get transactions; + String status(String status); + String get pendingStatus; + String get amount; + String get shareOrder; + String get markAsComplete; + String get deleteOrder; + String get readyForPickup; + String get barcode; + String pickupTime(DateTime from, DateTime to); + String get pickup; + String get pickupInstructions; + String get pickupWindow; + String get cancelledStatus; + + /// Merchant is responsible for the order, order details and receipt details. + String get merchantIsResponsibleNote; + + /// This method is used to obtain a localized instance of + /// [OrderLocalizations]. + static OrderLocalizations of(BuildContext context) { + return Localizations.of( + context, + OrderLocalizations, + )!; + } +} + +class EnOrderLocalizations extends OrderLocalizations { + @override + String orderedAt(DateTime date) { + return 'Ordered $date'; + } + + @override + String amount = 'Amount'; + + @override + String coupon = 'Coupon'; + + @override + String courier = 'Courier'; + + @override + String deleteOrder = 'Delete Order'; + + @override + String deliveredStatus = 'Delivered'; + + @override + String details = 'Details'; + + @override + String from(String merchant) { + return 'From $merchant'; + } + + @override + String manageOrder = 'Manage Order'; + + @override + String markAsComplete = 'Mark as Complete'; + + @override + String merchantIsResponsibleNote = + 'Merchant is responsible for the order, order details and receipt details.'; + + @override + String orderId = 'Order ID'; + + @override + String orderPlacedStatus = 'Order placed'; + + @override + String orderTotal = 'Order total'; + + @override + String outForDeliveryStatus = 'Out for delivery'; + + @override + String pendingStatus = 'Pending'; + + @override + String shareOrder = 'Share order'; + + @override + String status(String status) { + return 'Status $status'; + } + + @override + String subtotal = 'Subtotal'; + + @override + String tax = 'Tax'; + + @override + String total = 'Total'; + + @override + String trackShipment = 'Track Shipment'; + + @override + String trackingId = 'Tracking ID'; + + @override + String transactions = 'Transactions'; + + @override + String visitMerchantWebsite = 'Visit merchant website'; + + @override + String barcode = 'Barcode'; + + @override + String pickup = 'Pickup'; + + @override + String pickupInstructions = 'Pickup Instructions'; + + @override + String pickupTime(DateTime from, DateTime to) { + // Date, time from - to + return 'Pickup from $from '; + } + + @override + String readyForPickup = 'Ready for Pickup'; + + @override + String pickupWindow = 'Pickup Window'; + + @override + String cancelledStatus = 'Cancelled'; +} From 7dd75cd93029f434e72dfc73df008f7045bb5f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Tue, 16 Jul 2024 20:35:21 +0200 Subject: [PATCH 03/19] wip --- app/lib/home_page.dart | 23 +- app/lib/import_order/import_order_page.dart | 71 ++++++ app/lib/import_order/pick_order.dart | 37 +++ app/lib/import_order/receive_pass.dart | 51 ++++ app/lib/router.dart | 6 + app/pubspec.lock | 8 + app/pubspec.yaml | 1 + passkit_ui/example/pubspec.lock | 20 +- passkit_ui/lib/passkit_ui.dart | 1 + passkit_ui/lib/src/order/l10n.dart | 11 +- .../src/order/order_details_model_sheet.dart | 117 ++++++++++ passkit_ui/lib/src/order/order_widget.dart | 219 +++++++++++++++++- passkit_ui/pubspec.yaml | 2 + 13 files changed, 554 insertions(+), 13 deletions(-) create mode 100644 app/lib/import_order/import_order_page.dart create mode 100644 app/lib/import_order/pick_order.dart create mode 100644 app/lib/import_order/receive_pass.dart create mode 100644 passkit_ui/lib/src/order/order_details_model_sheet.dart diff --git a/app/lib/home_page.dart b/app/lib/home_page.dart index 1cdf376..0d563df 100644 --- a/app/lib/home_page.dart +++ b/app/lib/home_page.dart @@ -1,4 +1,5 @@ import 'package:app/db/database.dart'; +import 'package:app/import_order/import_order_page.dart'; import 'package:app/import_pass/import_page.dart'; import 'package:app/import_pass/pick_pass.dart'; import 'package:app/pass_backside/pass_backside_page.dart'; @@ -51,12 +52,22 @@ class _HomePageState extends State { } // TODO(ueman): Add more validation - await router.push( - '/import', - extra: PkPassImportSource( - bytes: await detail.files.first.readAsBytes(), - ), - ); + if (firstFile.name.endsWith('pkpass')) { + await router.push( + '/import', + extra: PkPassImportSource( + bytes: await detail.files.first.readAsBytes(), + ), + ); + } + if (firstFile.name.endsWith('order')) { + await router.push( + '/importOrder', + extra: PkOrderImportSource( + bytes: await detail.files.first.readAsBytes(), + ), + ); + } }, child: Scaffold( appBar: AppBar( diff --git a/app/lib/import_order/import_order_page.dart b/app/lib/import_order/import_order_page.dart new file mode 100644 index 0000000..8d22f53 --- /dev/null +++ b/app/lib/import_order/import_order_page.dart @@ -0,0 +1,71 @@ +import 'package:content_resolver/content_resolver.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:go_router/go_router.dart'; +import 'package:passkit/passkit.dart'; +import 'package:passkit_ui/passkit_ui.dart'; + +class PkOrderImportSource { + PkOrderImportSource({this.path, this.bytes}) + : assert(path != null || bytes != null); + + final String? path; + final List? bytes; + + Future getOrder() async { + if (path != null) { + final Content content = await ContentResolver.resolveContent(path!); + return PkOrder.fromBytes(content.data, skipVerification: true); + } else if (bytes != null) { + return PkOrder.fromBytes(bytes!, skipVerification: true); + } + throw Exception('No data'); + } +} + +class ImportOrderPage extends StatefulWidget { + const ImportOrderPage({super.key, required this.source}); + + final PkOrderImportSource source; + + @override + State createState() => _ImportOrderPageState(); +} + +class _ImportOrderPageState extends State { + PkOrder? order; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + final pkOrder = await widget.source.getOrder(); + setState(() { + order = pkOrder; + }); + } + + @override + Widget build(BuildContext context) { + if (order == null) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context).import), + actions: [ + IconButton( + // TODO(ueman): Maybe show confirmation dialog here + onPressed: () => context.pop(), + icon: const Icon(Icons.delete), + ), + ], + ), + body: const Center(child: CircularProgressIndicator()), + ); + } else { + return OrderWidget(order: order!); + } + } +} diff --git a/app/lib/import_order/pick_order.dart b/app/lib/import_order/pick_order.dart new file mode 100644 index 0000000..bce2074 --- /dev/null +++ b/app/lib/import_order/pick_order.dart @@ -0,0 +1,37 @@ +import 'package:app/import_order/import_order_page.dart'; +import 'package:app/router.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:path/path.dart'; + +Future pickOrder(BuildContext context) async { + final localizations = AppLocalizations.of(context); + final result = await FilePicker.platform.pickFiles( + dialogTitle: localizations.pickPasses, + allowMultiple: false, + type: FileType.any, + //allowedExtensions: ['pkpass', 'pass'], // This seems to not work even when combined with "type: FileType.custom" + ); + + if (result == null) { + return; + } + + final firstPath = result.files.firstOrNull?.path; + + if (firstPath == null) { + return; + } + + if ({'.order'}.contains(extension(firstPath))) { + // This is probably not a valid order + // TOOD show a hint to the user, that the user picked an ivalid file + return; + } + + await router.push( + '/importOrder', + extra: PkOrderImportSource(path: firstPath), + ); +} diff --git a/app/lib/import_order/receive_pass.dart b/app/lib/import_order/receive_pass.dart new file mode 100644 index 0000000..c5ad1e1 --- /dev/null +++ b/app/lib/import_order/receive_pass.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:app/import_pass/import_page.dart'; +import 'package:app/router.dart'; +import 'package:flutter/services.dart'; +import 'package:receive_intent/receive_intent.dart'; + +Future initReceiveIntent() async { + try { + final receivedIntent = await ReceiveIntent.getInitialIntent(); + await _onIntent(receivedIntent); + } on PlatformException { + // Handle exception + } +} + +// Does not need to be cancelled, since it's running for the lifetime of the app +// ignore: unused_element +StreamSubscription? _sub; + +Future initReceiveIntentWhileRunning() async { + _sub = ReceiveIntent.receivedIntentStream.listen( + (Intent? intent) { + // Validate receivedIntent and warn the user, if it is not correct, + _onIntent(intent); + }, + onError: (err) { + // TODO(ueman): Handle exception + }, + ); +} + +Future _onIntent(Intent? receivedIntent) async { + if (receivedIntent == null || receivedIntent.isNull) { + // Validate receivedIntent and warn the user, if it is not correct, + // but keep in mind it could be `null` or "empty"(`receivedIntent.isNull`). + // + // TODO(ueman): show error popup? + return; + } + if (receivedIntent.action == 'android.intent.action.MAIN') { + return; + } + final path = receivedIntent.data; + + if (path == null) { + // TODO(ueman): show error popup? + return; + } + unawaited(router.push('/import', extra: PkPassImportSource(path: path))); +} diff --git a/app/lib/router.dart b/app/lib/router.dart index 4f55f25..94733f1 100644 --- a/app/lib/router.dart +++ b/app/lib/router.dart @@ -1,5 +1,6 @@ import 'package:app/example/example_passes.dart'; import 'package:app/home_page.dart'; +import 'package:app/import_order/import_order_page.dart'; import 'package:app/import_pass/import_page.dart'; import 'package:app/pass_backside/pass_backside_page.dart'; import 'package:app/settings/settings_page.dart'; @@ -16,6 +17,11 @@ final router = GoRouter( builder: (context, state) => ImportPassPage(source: state.extra as PkPassImportSource), ), + GoRoute( + path: '/importOrder', + builder: (context, state) => + ImportOrderPage(source: state.extra as PkOrderImportSource), + ), GoRoute( path: '/examples', builder: (context, state) => const ExamplePasses(), diff --git a/app/pubspec.lock b/app/pubspec.lock index 8d1fff6..6ae4a56 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -241,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" dart_style: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 1f327ef..f372702 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: android_intent_plus: ^5.0.2 content_resolver: ^0.3.1 + cupertino_icons: ^1.0.8 desktop_drop: ^0.4.4 drift: ^2.11.1 file_picker: ^8.0.5 diff --git a/passkit_ui/example/pubspec.lock b/passkit_ui/example/pubspec.lock index c1973ca..fcc32eb 100644 --- a/passkit_ui/example/pubspec.lock +++ b/passkit_ui/example/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" fake_async: dependency: transitive description: @@ -123,6 +131,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" json_annotation: dependency: transitive description: @@ -193,14 +209,14 @@ packages: path: "../../passkit" relative: true source: path - version: "0.0.2" + version: "0.0.4" passkit_ui: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.0.1" + version: "0.0.3" path: dependency: transitive description: diff --git a/passkit_ui/lib/passkit_ui.dart b/passkit_ui/lib/passkit_ui.dart index e5a5d8c..1c3b2e2 100644 --- a/passkit_ui/lib/passkit_ui.dart +++ b/passkit_ui/lib/passkit_ui.dart @@ -1,3 +1,4 @@ export 'src/widgets/widgets.dart'; export 'src/extensions/extensions.dart'; export 'src/pk_pass_widget.dart'; +export 'src/order/order_widget.dart'; diff --git a/passkit_ui/lib/src/order/l10n.dart b/passkit_ui/lib/src/order/l10n.dart index 5bb015c..dd89de8 100644 --- a/passkit_ui/lib/src/order/l10n.dart +++ b/passkit_ui/lib/src/order/l10n.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; abstract class OrderLocalizations { /// Creates a [OrderLocalizations]. @@ -35,6 +36,7 @@ abstract class OrderLocalizations { String get pickupInstructions; String get pickupWindow; String get cancelledStatus; + String formatCurrency(double amount, String currency); /// Merchant is responsible for the order, order details and receipt details. String get merchantIsResponsibleNote; @@ -52,7 +54,8 @@ abstract class OrderLocalizations { class EnOrderLocalizations extends OrderLocalizations { @override String orderedAt(DateTime date) { - return 'Ordered $date'; + final dateFormat = DateFormat.yMd('en_EN'); + return 'Ordered ${dateFormat.format(date)}'; } @override @@ -155,4 +158,10 @@ class EnOrderLocalizations extends OrderLocalizations { @override String cancelledStatus = 'Cancelled'; + + @override + String formatCurrency(double amount, String currency) { + final numberFormat = NumberFormat.currency(name: currency, locale: 'en_EN'); + return numberFormat.format(amount); + } } diff --git a/passkit_ui/lib/src/order/order_details_model_sheet.dart b/passkit_ui/lib/src/order/order_details_model_sheet.dart new file mode 100644 index 0000000..e619563 --- /dev/null +++ b/passkit_ui/lib/src/order/order_details_model_sheet.dart @@ -0,0 +1,117 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:passkit/passkit.dart'; +import 'package:passkit_ui/src/order/l10n.dart'; + +Future showOrderDetailSheet(BuildContext context, PkOrder order) { + return showModalBottomSheet( + context: context, + builder: (context) => OrderDetailsModelSheet(order: order), + ); +} + +class OrderDetailsModelSheet extends StatelessWidget { + const OrderDetailsModelSheet({super.key, required this.order}); + + final PkOrder order; + + @override + Widget build(BuildContext context) { + final payment = order.order.payment; + final lineItems = order.order.lineItems; + + final l10n = EnOrderLocalizations(); + + return ListView( + children: [ + if (payment != null) + Text( + l10n.formatCurrency(payment.total.amount, payment.total.currency), + textAlign: TextAlign.center, + style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle, + ), + Text( + order.order.merchant.displayName, + textAlign: TextAlign.center, + style: CupertinoTheme.of(context).textTheme.textStyle, + ), + Text( + l10n.orderedAt(order.order.createdAt), + textAlign: TextAlign.center, + style: CupertinoTheme.of(context).textTheme.textStyle, + ), + if (lineItems != null) + CupertinoListSection.insetGrouped( + children: [ + for (final lineItem in lineItems) + CupertinoListTile.notched( + title: Text(lineItem.title), + subtitle: lineItem.subtitle != null + ? Text(lineItem.subtitle!) + : null, + trailing: const Column( + children: [ + Text(''), + ], + ), + ), + _Summary(summaryItems: payment?.summaryItems), + if (payment?.total != null) + CupertinoListTile.notched( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(l10n.total), + Text( + l10n.formatCurrency( + payment!.total.amount, + payment.total.currency, + ), + ), + ], + ), + ), + // Transaction is not added since the properties are marked as + // deprecated. + ], + ), + ], + ); + } +} + +class _Summary extends StatelessWidget { + const _Summary({this.summaryItems}); + + final List? summaryItems; + + @override + Widget build(BuildContext context) { + if (summaryItems == null) { + return const SizedBox.shrink(); + } + if (summaryItems!.isEmpty) { + return const SizedBox.shrink(); + } + + final l10n = EnOrderLocalizations(); + + return CupertinoListTile.notched( + title: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final item in summaryItems!) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(item.label), + Text( + l10n.formatCurrency(item.value.amount, item.value.currency), + ), + ], + ), + ], + ), + ); + } +} diff --git a/passkit_ui/lib/src/order/order_widget.dart b/passkit_ui/lib/src/order/order_widget.dart index 0f4f07a..a55fcb2 100644 --- a/passkit_ui/lib/src/order/order_widget.dart +++ b/passkit_ui/lib/src/order/order_widget.dart @@ -1,5 +1,7 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/cupertino.dart'; import 'package:passkit/passkit.dart'; +import 'package:passkit_ui/src/order/l10n.dart'; +import 'package:passkit_ui/src/order/order_details_model_sheet.dart'; class OrderWidget extends StatelessWidget { const OrderWidget({super.key, required this.order}); @@ -10,12 +12,221 @@ class OrderWidget extends StatelessWidget { // https://developer.apple.com/design/human-interface-guidelines/wallet#Order-tracking @override Widget build(BuildContext context) { + final l10n = EnOrderLocalizations(); + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(order.order.merchant.displayName), + ), + backgroundColor: + CupertinoColors.systemGroupedBackground.resolveFrom(context), + child: ListView( + children: [ + //Image.memory(order.o) + Text( + order.order.merchant.displayName, + textAlign: TextAlign.center, + style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle, + ), + Text( + l10n.orderedAt(order.order.createdAt), + textAlign: TextAlign.center, + style: CupertinoTheme.of(context).textTheme.textStyle, + ), + if (order.order.orderProvider?.displayName != null) + CupertinoListTile( + title: Text(l10n.courier), + subtitle: Text(order.order.orderProvider!.displayName), + ), + if (order.order.fulfillments != null) + for (final fulfillment in order.order.fulfillments!) + _FulfillmentSection(fulfillment: fulfillment, order: order), + DetailsSection(order: order), + InfoSection( + order: order, + onManageOrderClicked: (_) {}, + onVisitMerchantWebsiteClicked: (_) {}, + ), + ], + ), + ); + } +} + +class DetailsSection extends StatelessWidget { + const DetailsSection({super.key, required this.order}); + + final PkOrder order; + + @override + Widget build(BuildContext context) { + final l10n = EnOrderLocalizations(); + + return CupertinoListSection.insetGrouped( + header: Text(l10n.details), + children: [ + CupertinoListTile.notched( + title: Text(l10n.orderId), + subtitle: Text(order.order.orderIdentifier), + ), + if (order.order.payment != null) + CupertinoListTile.notched( + title: Text(l10n.orderTotal), + subtitle: order.order.payment == null + ? null + : Text( + l10n.formatCurrency( + order.order.payment!.total.amount, + order.order.payment!.total.currency, + ), + ), + trailing: const CupertinoListTileChevron(), + onTap: () => showOrderDetailSheet(context, order), + ), + ], + ); + } +} + +class InfoSection extends StatelessWidget { + const InfoSection({ + super.key, + required this.order, + required this.onManageOrderClicked, + required this.onVisitMerchantWebsiteClicked, + }); + + final PkOrder order; + final ValueChanged onManageOrderClicked; + final ValueChanged onVisitMerchantWebsiteClicked; + + @override + Widget build(BuildContext context) { + final l10n = EnOrderLocalizations(); + + const linkColor = CupertinoColors.link; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CupertinoListSection.insetGrouped( + children: [ + CupertinoListTile.notched( + title: Text( + l10n.manageOrder, + style: const TextStyle(color: linkColor), + ), + trailing: + const Icon(CupertinoIcons.arrow_up_right, color: linkColor), + onTap: () => onManageOrderClicked(order.order.orderManagementURL), + ), + CupertinoListTile.notched( + title: Text( + l10n.visitMerchantWebsite, + style: const TextStyle(color: linkColor), + ), + trailing: const Icon(CupertinoIcons.compass, color: linkColor), + onTap: () => + onVisitMerchantWebsiteClicked(order.order.merchant.url), + ), + ], + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + l10n.merchantIsResponsibleNote, + textAlign: TextAlign.center, + style: CupertinoTheme.of(context).textTheme.textStyle, + ), + ), + ], + ); + } +} + +class _FulfillmentSection extends StatelessWidget { + const _FulfillmentSection({required this.fulfillment, required this.order}); + + final Object fulfillment; + final PkOrder order; + + @override + Widget build(BuildContext context) { + return switch (fulfillment) { + OrderShippingFulfillment shipping => + _OrderShippingFulfillmentWidget(fulfillment: shipping, order: order), + OrderPickupFulfillment orderPickup => + _OrderPickupFulfillmentWidget(fulfillment: orderPickup), + _ => const SizedBox.shrink() + }; + } +} + +class _OrderShippingFulfillmentWidget extends StatelessWidget { + const _OrderShippingFulfillmentWidget({ + required this.fulfillment, + required this.order, + }); + + final OrderShippingFulfillment fulfillment; + final PkOrder order; + + @override + Widget build(BuildContext context) { + final l10n = EnOrderLocalizations(); + + return CupertinoListSection.insetGrouped( children: [ - // TODO(any): Add merchant logo here - Text(order.order.merchant.displayName), - // TODO(any): Add date of order here + CupertinoListTile( + title: Text(fulfillment.status.name), + subtitle: Text(fulfillment.deliveredAt?.toString() ?? ''), + trailing: const Icon( + CupertinoIcons.check_mark_circled_solid, + color: CupertinoColors.systemGreen, + ), + ), + if (fulfillment.notes != null) + CupertinoListTile( + title: Text(l10n.from(order.order.merchant.displayName)), + subtitle: Text(fulfillment.notes!), + ), + if (fulfillment.carrier != null) + CupertinoListTile.notched( + title: Text(l10n.courier), + subtitle: Text(fulfillment.carrier!), + ), + if (fulfillment.trackingNumber != null) + CupertinoListTile.notched( + title: Text(l10n.trackingId), + subtitle: Text(fulfillment.trackingNumber!), + ), + if (fulfillment.trackingURL != null) + CupertinoListTile.notched( + title: Text( + l10n.trackShipment, + style: const TextStyle(color: CupertinoColors.link), + ), + onTap: () {}, + ), + if (fulfillment.lineItems != null) + for (final lineItem in fulfillment.lineItems!) + CupertinoListTile.notched( + title: Text(lineItem.title), + subtitle: + lineItem.subtitle != null ? Text(lineItem.subtitle!) : null, + onTap: () {}, + ), ], ); } } + +class _OrderPickupFulfillmentWidget extends StatelessWidget { + const _OrderPickupFulfillmentWidget({required this.fulfillment}); + + final OrderPickupFulfillment fulfillment; + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} diff --git a/passkit_ui/pubspec.yaml b/passkit_ui/pubspec.yaml index 82698fb..1294fab 100644 --- a/passkit_ui/pubspec.yaml +++ b/passkit_ui/pubspec.yaml @@ -19,8 +19,10 @@ dependencies: barcode_widget: ^2.0.0 collection: ^1.18.0 csslib: ^1.0.0 + cupertino_icons: ^1.0.8 flutter: sdk: flutter + intl: ^0.19.0 passkit: ^0.0.4 dev_dependencies: From 2637a33ad907a4579257b698d5d77a5908e21b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Thu, 18 Jul 2024 06:48:26 +0200 Subject: [PATCH 04/19] wip --- app/lib/import_order/import_order_page.dart | 13 +- passkit/lib/src/order/pk_order.dart | 26 +++- passkit_ui/lib/src/order/l10n.dart | 68 ++++++---- passkit_ui/lib/src/order/order_widget.dart | 139 +++++++++++++++++--- 4 files changed, 200 insertions(+), 46 deletions(-) diff --git a/app/lib/import_order/import_order_page.dart b/app/lib/import_order/import_order_page.dart index 8d22f53..f1b2db1 100644 --- a/app/lib/import_order/import_order_page.dart +++ b/app/lib/import_order/import_order_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:passkit/passkit.dart'; import 'package:passkit_ui/passkit_ui.dart'; +import 'package:url_launcher/url_launcher.dart'; class PkOrderImportSource { PkOrderImportSource({this.path, this.bytes}) @@ -65,7 +66,17 @@ class _ImportOrderPageState extends State { body: const Center(child: CircularProgressIndicator()), ); } else { - return OrderWidget(order: order!); + return OrderWidget( + order: order!, + isOrderImport: true, + onDeleteOrderClicked: (order) {}, + onManageOrderClicked: launchUrl, + onVisitMerchantWebsiteClicked: launchUrl, + onShareClicked: (order) {}, + onMarkOrderCompletedClicked: (order) {}, + onTrackingLinkClicked: launchUrl, + onImportOrderClicked: (order) {}, + ); } } } diff --git a/passkit/lib/src/order/pk_order.dart b/passkit/lib/src/order/pk_order.dart index dc06b27..d0fabd4 100644 --- a/passkit/lib/src/order/pk_order.dart +++ b/passkit/lib/src/order/pk_order.dart @@ -1,6 +1,10 @@ +import 'dart:typed_data'; + import 'package:archive/archive.dart'; +import 'package:archive/archive_io.dart'; import 'package:passkit/src/archive_extensions.dart'; import 'package:passkit/src/pkpass/exceptions.dart'; +import 'package:passkit/src/pkpass/pk_pass_image.dart'; import 'order_data.dart'; @@ -39,7 +43,7 @@ class PkOrder { languageData: archive.getTranslations(), // source sourceData: bytes, - ); + ).._archive = archive; } final OrderData order; @@ -56,6 +60,26 @@ class PkOrder { /// pairs. final Map>? languageData; + late Archive _archive; + + // TODO(any): Do proper image loading for orders + // Do a sanity check for paths that already contains @2x/@3x modifier + PkPassImage loadImage(String path, {String? locale}) { + assert(path.isNotEmpty); + + final fileExtension = path.split('.').last; + final twoXResPath = + path.replaceAll('.$fileExtension', '@2x.$fileExtension'); + final threeXResPath = + path.replaceAll('.$fileExtension', '@3x.$fileExtension'); + + return PkPassImage( + image1: _archive.findFile(path)?.content as Uint8List?, + image2: _archive.findFile(twoXResPath)?.content as Uint8List?, + image3: _archive.findFile(threeXResPath)?.content as Uint8List?, + ); + } + /// The bytes of this PkPass final List sourceData; diff --git a/passkit_ui/lib/src/order/l10n.dart b/passkit_ui/lib/src/order/l10n.dart index dd89de8..49eff87 100644 --- a/passkit_ui/lib/src/order/l10n.dart +++ b/passkit_ui/lib/src/order/l10n.dart @@ -37,6 +37,9 @@ abstract class OrderLocalizations { String get pickupWindow; String get cancelledStatus; String formatCurrency(double amount, String currency); + String get markOrderCompleted; + String get track; + String get cancel; /// Merchant is responsible for the order, order details and receipt details. String get merchantIsResponsibleNote; @@ -59,22 +62,22 @@ class EnOrderLocalizations extends OrderLocalizations { } @override - String amount = 'Amount'; + final String amount = 'Amount'; @override - String coupon = 'Coupon'; + final String coupon = 'Coupon'; @override - String courier = 'Courier'; + final String courier = 'Courier'; @override - String deleteOrder = 'Delete Order'; + final String deleteOrder = 'Delete Order'; @override - String deliveredStatus = 'Delivered'; + final String deliveredStatus = 'Delivered'; @override - String details = 'Details'; + final String details = 'Details'; @override String from(String merchant) { @@ -82,32 +85,32 @@ class EnOrderLocalizations extends OrderLocalizations { } @override - String manageOrder = 'Manage Order'; + final String manageOrder = 'Manage Order'; @override - String markAsComplete = 'Mark as Complete'; + final String markAsComplete = 'Mark as Complete'; @override - String merchantIsResponsibleNote = + final String merchantIsResponsibleNote = 'Merchant is responsible for the order, order details and receipt details.'; @override - String orderId = 'Order ID'; + final String orderId = 'Order ID'; @override - String orderPlacedStatus = 'Order placed'; + final String orderPlacedStatus = 'Order placed'; @override - String orderTotal = 'Order total'; + final String orderTotal = 'Order total'; @override - String outForDeliveryStatus = 'Out for delivery'; + final String outForDeliveryStatus = 'Out for delivery'; @override - String pendingStatus = 'Pending'; + final String pendingStatus = 'Pending'; @override - String shareOrder = 'Share order'; + final String shareOrder = 'Share order'; @override String status(String status) { @@ -115,34 +118,34 @@ class EnOrderLocalizations extends OrderLocalizations { } @override - String subtotal = 'Subtotal'; + final String subtotal = 'Subtotal'; @override - String tax = 'Tax'; + final String tax = 'Tax'; @override - String total = 'Total'; + final String total = 'Total'; @override - String trackShipment = 'Track Shipment'; + final String trackShipment = 'Track Shipment'; @override - String trackingId = 'Tracking ID'; + final String trackingId = 'Tracking ID'; @override - String transactions = 'Transactions'; + final String transactions = 'Transactions'; @override - String visitMerchantWebsite = 'Visit merchant website'; + final String visitMerchantWebsite = 'Visit merchant website'; @override - String barcode = 'Barcode'; + final String barcode = 'Barcode'; @override - String pickup = 'Pickup'; + final String pickup = 'Pickup'; @override - String pickupInstructions = 'Pickup Instructions'; + final String pickupInstructions = 'Pickup Instructions'; @override String pickupTime(DateTime from, DateTime to) { @@ -151,17 +154,26 @@ class EnOrderLocalizations extends OrderLocalizations { } @override - String readyForPickup = 'Ready for Pickup'; + final String readyForPickup = 'Ready for Pickup'; @override - String pickupWindow = 'Pickup Window'; + final String pickupWindow = 'Pickup Window'; @override - String cancelledStatus = 'Cancelled'; + final String cancelledStatus = 'Cancelled'; @override String formatCurrency(double amount, String currency) { final numberFormat = NumberFormat.currency(name: currency, locale: 'en_EN'); return numberFormat.format(amount); } + + @override + final String markOrderCompleted = 'Mark as completed'; + + @override + final String track = 'Track'; + + @override + final String cancel = 'Cancel'; } diff --git a/passkit_ui/lib/src/order/order_widget.dart b/passkit_ui/lib/src/order/order_widget.dart index a55fcb2..3a94f70 100644 --- a/passkit_ui/lib/src/order/order_widget.dart +++ b/passkit_ui/lib/src/order/order_widget.dart @@ -1,27 +1,92 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:passkit/passkit.dart'; +import 'package:passkit_ui/src/extensions/pk_pass_image_extension.dart'; import 'package:passkit_ui/src/order/l10n.dart'; import 'package:passkit_ui/src/order/order_details_model_sheet.dart'; class OrderWidget extends StatelessWidget { - const OrderWidget({super.key, required this.order}); + const OrderWidget({ + super.key, + required this.order, + required this.isOrderImport, + required this.onManageOrderClicked, + required this.onVisitMerchantWebsiteClicked, + required this.onShareClicked, + required this.onDeleteOrderClicked, + required this.onMarkOrderCompletedClicked, + required this.onTrackingLinkClicked, + required this.onImportOrderClicked, + }); final PkOrder order; + /// When this is an import, the navigation bar shows a cancle and track button. + /// When this is not an import, the navigation bar shows, depending on the status, + /// a mark as completed button, a share button, and a delete order button. + final bool isOrderImport; + final ValueChanged onManageOrderClicked; + final ValueChanged onVisitMerchantWebsiteClicked; + final ValueChanged onTrackingLinkClicked; + final ValueChanged onShareClicked; + final ValueChanged onDeleteOrderClicked; + final ValueChanged onMarkOrderCompletedClicked; + final ValueChanged onImportOrderClicked; + // Build the UI according to https://docs-assets.developer.apple.com/published/e5ec23af37b6a9d9cbc90e5d5f47bf8a/wallet-ot-status-on-the-way-fields@2x.png // https://developer.apple.com/design/human-interface-guidelines/wallet#Order-tracking @override Widget build(BuildContext context) { + final devicePixelRatio = MediaQuery.devicePixelRatioOf(context); final l10n = EnOrderLocalizations(); + return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: Text(order.order.merchant.displayName), + leading: isOrderImport + ? CupertinoButton( + onPressed: () => Navigator.maybePop(context), + padding: EdgeInsets.zero, + child: Text(l10n.cancel), + ) + : null, + trailing: isOrderImport + ? CupertinoButton( + padding: EdgeInsets.zero, + onPressed: () => onImportOrderClicked(order), + child: Text(l10n.track), + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => onShareClicked(order), + icon: Icon(Icons.adaptive.share), + ), + IconButton( + onPressed: () => onShareClicked(order), + icon: Icon(Icons.adaptive.more_outlined), + ), + ], + ), ), backgroundColor: CupertinoColors.systemGroupedBackground.resolveFrom(context), child: ListView( children: [ - //Image.memory(order.o) + if (order.order.merchant.logo != null) + Padding( + padding: const EdgeInsets.all(20), + child: Image.memory( + order + .loadImage(order.order.merchant.logo!) + .forCorrectPixelRatio(devicePixelRatio), + fit: BoxFit.contain, + width: 150, + height: 150, + ), + ), Text( order.order.merchant.displayName, textAlign: TextAlign.center, @@ -30,7 +95,10 @@ class OrderWidget extends StatelessWidget { Text( l10n.orderedAt(order.order.createdAt), textAlign: TextAlign.center, - style: CupertinoTheme.of(context).textTheme.textStyle, + style: CupertinoTheme.of(context) + .textTheme + .textStyle + .copyWith(color: CupertinoColors.systemGrey), ), if (order.order.orderProvider?.displayName != null) CupertinoListTile( @@ -39,12 +107,16 @@ class OrderWidget extends StatelessWidget { ), if (order.order.fulfillments != null) for (final fulfillment in order.order.fulfillments!) - _FulfillmentSection(fulfillment: fulfillment, order: order), + _FulfillmentSection( + fulfillment: fulfillment, + order: order, + onTrackingLinkClicked: onTrackingLinkClicked, + ), DetailsSection(order: order), InfoSection( order: order, - onManageOrderClicked: (_) {}, - onVisitMerchantWebsiteClicked: (_) {}, + onManageOrderClicked: onManageOrderClicked, + onVisitMerchantWebsiteClicked: onVisitMerchantWebsiteClicked, ), ], ), @@ -64,9 +136,21 @@ class DetailsSection extends StatelessWidget { return CupertinoListSection.insetGrouped( header: Text(l10n.details), children: [ - CupertinoListTile.notched( - title: Text(l10n.orderId), - subtitle: Text(order.order.orderIdentifier), + CupertinoContextMenu( + actions: [ + CupertinoContextMenuAction( + onPressed: () => Clipboard.setData( + ClipboardData(text: order.order.orderIdentifier), + ), + isDefaultAction: true, + trailingIcon: CupertinoIcons.number, + child: const Text('Copy Order Number'), + ), + ], + child: CupertinoListTile.notched( + title: Text(l10n.orderId), + subtitle: Text(order.order.orderIdentifier), + ), ), if (order.order.payment != null) CupertinoListTile.notched( @@ -131,11 +215,14 @@ class InfoSection extends StatelessWidget { ], ), Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8), child: Text( l10n.merchantIsResponsibleNote, textAlign: TextAlign.center, - style: CupertinoTheme.of(context).textTheme.textStyle, + style: CupertinoTheme.of(context) + .textTheme + .textStyle + .copyWith(color: CupertinoColors.systemGrey), ), ), ], @@ -144,16 +231,24 @@ class InfoSection extends StatelessWidget { } class _FulfillmentSection extends StatelessWidget { - const _FulfillmentSection({required this.fulfillment, required this.order}); + const _FulfillmentSection({ + required this.fulfillment, + required this.order, + required this.onTrackingLinkClicked, + }); final Object fulfillment; final PkOrder order; + final ValueChanged onTrackingLinkClicked; @override Widget build(BuildContext context) { return switch (fulfillment) { - OrderShippingFulfillment shipping => - _OrderShippingFulfillmentWidget(fulfillment: shipping, order: order), + OrderShippingFulfillment shipping => _OrderShippingFulfillmentWidget( + fulfillment: shipping, + order: order, + onTrackingLinkClicked: onTrackingLinkClicked, + ), OrderPickupFulfillment orderPickup => _OrderPickupFulfillmentWidget(fulfillment: orderPickup), _ => const SizedBox.shrink() @@ -165,14 +260,17 @@ class _OrderShippingFulfillmentWidget extends StatelessWidget { const _OrderShippingFulfillmentWidget({ required this.fulfillment, required this.order, + required this.onTrackingLinkClicked, }); final OrderShippingFulfillment fulfillment; final PkOrder order; + final ValueChanged onTrackingLinkClicked; @override Widget build(BuildContext context) { final l10n = EnOrderLocalizations(); + final devicePixelRatio = MediaQuery.devicePixelRatioOf(context); return CupertinoListSection.insetGrouped( children: [ @@ -205,15 +303,24 @@ class _OrderShippingFulfillmentWidget extends StatelessWidget { l10n.trackShipment, style: const TextStyle(color: CupertinoColors.link), ), - onTap: () {}, + onTap: () => onTrackingLinkClicked(fulfillment.trackingURL!), ), if (fulfillment.lineItems != null) for (final lineItem in fulfillment.lineItems!) CupertinoListTile.notched( + leading: lineItem.image != null + ? Image.memory( + order + .loadImage(lineItem.image!) + .forCorrectPixelRatio(devicePixelRatio), + fit: BoxFit.contain, + width: 150, + height: 150, + ) + : null, title: Text(lineItem.title), subtitle: lineItem.subtitle != null ? Text(lineItem.subtitle!) : null, - onTap: () {}, ), ], ); From 6438f716eac7c7de39c4d4e0060093243379920d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 21 Jul 2024 19:39:32 +0200 Subject: [PATCH 05/19] fix merge --- passkit/lib/passkit.dart | 16 ---------------- passkit/lib/src/order/pk_order.dart | 4 ++-- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/passkit/lib/passkit.dart b/passkit/lib/passkit.dart index d2e6703..e3cfca4 100644 --- a/passkit/lib/passkit.dart +++ b/passkit/lib/passkit.dart @@ -35,19 +35,3 @@ export 'src/pkpass_webservice/passkit_web_client.dart'; export 'src/pkpass_webservice/passkit_web_client_exceptions.dart'; export 'src/pkpass_webservice/personalization_dictionary.dart'; export 'src/pkpass_webservice/serial_numbers.dart'; - -export 'src/order/order_address.dart'; -export 'src/order/order_application.dart'; -export 'src/order/order_barcode.dart'; -export 'src/order/order_customer.dart'; -export 'src/order/order_data.dart'; -export 'src/order/order_line_item.dart'; -export 'src/order/order_location.dart'; -export 'src/order/order_merchant.dart'; -export 'src/order/order_payment.dart'; -export 'src/order/order_pickup_fulfillment.dart'; -export 'src/order/order_provider.dart'; -export 'src/order/order_return.dart'; -export 'src/order/order_return_info.dart'; -export 'src/order/order_shipping_fulfillment.dart'; -export 'src/order/pk_order.dart'; diff --git a/passkit/lib/src/order/pk_order.dart b/passkit/lib/src/order/pk_order.dart index d0fabd4..e47d9ff 100644 --- a/passkit/lib/src/order/pk_order.dart +++ b/passkit/lib/src/order/pk_order.dart @@ -64,7 +64,7 @@ class PkOrder { // TODO(any): Do proper image loading for orders // Do a sanity check for paths that already contains @2x/@3x modifier - PkPassImage loadImage(String path, {String? locale}) { + PkImage loadImage(String path, {String? locale}) { assert(path.isNotEmpty); final fileExtension = path.split('.').last; @@ -73,7 +73,7 @@ class PkOrder { final threeXResPath = path.replaceAll('.$fileExtension', '@3x.$fileExtension'); - return PkPassImage( + return PkImage( image1: _archive.findFile(path)?.content as Uint8List?, image2: _archive.findFile(twoXResPath)?.content as Uint8List?, image3: _archive.findFile(threeXResPath)?.content as Uint8List?, From 6daebe09ed5c6b718d04df25cd9bb2aeca88fe0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 21 Jul 2024 19:40:02 +0200 Subject: [PATCH 06/19] fix docs --- passkit/lib/src/order/pk_order.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/passkit/lib/src/order/pk_order.dart b/passkit/lib/src/order/pk_order.dart index e47d9ff..59ae7b5 100644 --- a/passkit/lib/src/order/pk_order.dart +++ b/passkit/lib/src/order/pk_order.dart @@ -5,6 +5,7 @@ import 'package:archive/archive_io.dart'; import 'package:passkit/src/archive_extensions.dart'; import 'package:passkit/src/pkpass/exceptions.dart'; import 'package:passkit/src/pkpass/pk_pass_image.dart'; +import 'package:passkit/src/pkpass/pkpass.dart'; import 'order_data.dart'; From 6b501bd490cfdd11809549f8b742ce2a03600bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 21 Jul 2024 19:40:34 +0200 Subject: [PATCH 07/19] remove archive_io import --- passkit/lib/src/order/pk_order.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/passkit/lib/src/order/pk_order.dart b/passkit/lib/src/order/pk_order.dart index 59ae7b5..44b7b05 100644 --- a/passkit/lib/src/order/pk_order.dart +++ b/passkit/lib/src/order/pk_order.dart @@ -1,7 +1,6 @@ import 'dart:typed_data'; import 'package:archive/archive.dart'; -import 'package:archive/archive_io.dart'; import 'package:passkit/src/archive_extensions.dart'; import 'package:passkit/src/pkpass/exceptions.dart'; import 'package:passkit/src/pkpass/pk_pass_image.dart'; From 379d9c247801019631362ae9723cb1096627355a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Tue, 30 Jul 2024 12:30:01 +0200 Subject: [PATCH 08/19] wip --- passkit_ui/lib/src/order/order_widget.dart | 18 +++++++++++------- passkit_ui/lib/src/widgets/squircle.dart | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 passkit_ui/lib/src/widgets/squircle.dart diff --git a/passkit_ui/lib/src/order/order_widget.dart b/passkit_ui/lib/src/order/order_widget.dart index 3a94f70..16a578d 100644 --- a/passkit_ui/lib/src/order/order_widget.dart +++ b/passkit_ui/lib/src/order/order_widget.dart @@ -5,6 +5,7 @@ import 'package:passkit/passkit.dart'; import 'package:passkit_ui/src/extensions/pk_pass_image_extension.dart'; import 'package:passkit_ui/src/order/l10n.dart'; import 'package:passkit_ui/src/order/order_details_model_sheet.dart'; +import 'package:passkit_ui/src/widgets/squircle.dart'; class OrderWidget extends StatelessWidget { const OrderWidget({ @@ -78,13 +79,16 @@ class OrderWidget extends StatelessWidget { if (order.order.merchant.logo != null) Padding( padding: const EdgeInsets.all(20), - child: Image.memory( - order - .loadImage(order.order.merchant.logo!) - .forCorrectPixelRatio(devicePixelRatio), - fit: BoxFit.contain, - width: 150, - height: 150, + child: Squircle( + radius: 20, + child: Image.memory( + order + .loadImage(order.order.merchant.logo!) + .forCorrectPixelRatio(devicePixelRatio), + fit: BoxFit.contain, + width: 150, + height: 150, + ), ), ), Text( diff --git a/passkit_ui/lib/src/widgets/squircle.dart b/passkit_ui/lib/src/widgets/squircle.dart new file mode 100644 index 0000000..e35df49 --- /dev/null +++ b/passkit_ui/lib/src/widgets/squircle.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class Squircle extends StatelessWidget { + const Squircle({super.key, required this.radius, required this.child}); + + final Widget child; + final double radius; + + @override + Widget build(BuildContext context) { + return ClipPath( + clipper: ShapeBorderClipper( + shape: ContinuousRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(radius)), + ), + ), + child: child, + ); + } +} From 0372d988dffda5e872ec2bfc3022305793b97b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 1 Sep 2024 11:35:15 +0200 Subject: [PATCH 09/19] fix conflicts after merge --- app/lib/import_order/import_order_page.dart | 12 ++++++++++-- app/lib/import_order/receive_pass.dart | 7 ++++++- passkit_ui/pubspec.yaml | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/lib/import_order/import_order_page.dart b/app/lib/import_order/import_order_page.dart index f1b2db1..f1bb654 100644 --- a/app/lib/import_order/import_order_page.dart +++ b/app/lib/import_order/import_order_page.dart @@ -16,9 +16,17 @@ class PkOrderImportSource { Future getOrder() async { if (path != null) { final Content content = await ContentResolver.resolveContent(path!); - return PkOrder.fromBytes(content.data, skipVerification: true); + return PkOrder.fromBytes( + content.data, + skipChecksumVerification: true, + skipSignatureVerification: true, + ); } else if (bytes != null) { - return PkOrder.fromBytes(bytes!, skipVerification: true); + return PkOrder.fromBytes( + bytes!, + skipChecksumVerification: true, + skipSignatureVerification: true, + ); } throw Exception('No data'); } diff --git a/app/lib/import_order/receive_pass.dart b/app/lib/import_order/receive_pass.dart index c5ad1e1..66dd07b 100644 --- a/app/lib/import_order/receive_pass.dart +++ b/app/lib/import_order/receive_pass.dart @@ -47,5 +47,10 @@ Future _onIntent(Intent? receivedIntent) async { // TODO(ueman): show error popup? return; } - unawaited(router.push('/import', extra: PkPassImportSource(path: path))); + unawaited( + router.push( + '/import', + extra: PkPassImportSource(contentResolverPath: path), + ), + ); } diff --git a/passkit_ui/pubspec.yaml b/passkit_ui/pubspec.yaml index 461693d..3038eb7 100644 --- a/passkit_ui/pubspec.yaml +++ b/passkit_ui/pubspec.yaml @@ -22,8 +22,8 @@ dependencies: cupertino_icons: ^1.0.8 flutter: sdk: flutter - meta: any intl: ^0.19.0 + meta: ^1.0.0 passkit: ^0.0.5 dev_dependencies: From 99d1d2f38d9411a87a3b8b36867f4f720e1f05da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Wed, 4 Sep 2024 19:37:38 +0200 Subject: [PATCH 10/19] wip --- app/lib/import_order/import_order_page.dart | 22 ++++-- app/lib/import_order/pick_order.dart | 2 +- app/lib/import_pass/pick_pass.dart | 17 ++++- app/pubspec.lock | 81 +++++++++------------ app/pubspec.yaml | 4 +- apple_passkit/example/pubspec.lock | 28 +++---- passkit_ui/example/pubspec.lock | 28 +++---- 7 files changed, 95 insertions(+), 87 deletions(-) diff --git a/app/lib/import_order/import_order_page.dart b/app/lib/import_order/import_order_page.dart index f1bb654..4dcab97 100644 --- a/app/lib/import_order/import_order_page.dart +++ b/app/lib/import_order/import_order_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:content_resolver/content_resolver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -7,15 +9,19 @@ import 'package:passkit_ui/passkit_ui.dart'; import 'package:url_launcher/url_launcher.dart'; class PkOrderImportSource { - PkOrderImportSource({this.path, this.bytes}) - : assert(path != null || bytes != null); + PkOrderImportSource({this.contentResolverPath, this.bytes, this.filePath}) + : assert( + contentResolverPath != null || bytes != null || filePath != null, + ); - final String? path; + final String? contentResolverPath; final List? bytes; + final String? filePath; Future getOrder() async { - if (path != null) { - final Content content = await ContentResolver.resolveContent(path!); + if (contentResolverPath != null) { + final Content content = + await ContentResolver.resolveContent(contentResolverPath!); return PkOrder.fromBytes( content.data, skipChecksumVerification: true, @@ -27,6 +33,12 @@ class PkOrderImportSource { skipChecksumVerification: true, skipSignatureVerification: true, ); + } else if (filePath != null) { + return PkOrder.fromBytes( + await File(filePath!).readAsBytes(), + skipChecksumVerification: true, + skipSignatureVerification: true, + ); } throw Exception('No data'); } diff --git a/app/lib/import_order/pick_order.dart b/app/lib/import_order/pick_order.dart index bce2074..9076219 100644 --- a/app/lib/import_order/pick_order.dart +++ b/app/lib/import_order/pick_order.dart @@ -32,6 +32,6 @@ Future pickOrder(BuildContext context) async { await router.push( '/importOrder', - extra: PkOrderImportSource(path: firstPath), + extra: PkOrderImportSource(contentResolverPath: firstPath), ); } diff --git a/app/lib/import_pass/pick_pass.dart b/app/lib/import_pass/pick_pass.dart index e685132..2a15611 100644 --- a/app/lib/import_pass/pick_pass.dart +++ b/app/lib/import_pass/pick_pass.dart @@ -1,3 +1,4 @@ +import 'package:app/import_order/import_order_page.dart'; import 'package:app/import_pass/import_page.dart'; import 'package:app/router.dart'; import 'package:file_picker/file_picker.dart'; @@ -24,11 +25,19 @@ Future pickPass(BuildContext context) async { return; } - if (!{'.pkpass', '.pass'}.contains(extension(firstPath))) { - // This is probably not a valid PkPass - // TOOD show a hint to the user, that the user picked an ivalid file + if ({'.pkpass', '.pass'}.contains(extension(firstPath))) { + await router.push( + '/import', + extra: PkPassImportSource(filePath: firstPath), + ); return; } - await router.push('/import', extra: PkPassImportSource(filePath: firstPath)); + if ('.order' == extension(firstPath)) { + await router.push( + '/importOrder', + extra: PkOrderImportSource(filePath: firstPath), + ); + return; + } } diff --git a/app/pubspec.lock b/app/pubspec.lock index 490d3a8..c428797 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -5,23 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "72.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.2" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.4.1" android_intent_plus: dependency: "direct main" description: @@ -114,18 +109,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" url: "https://pub.dev" source: hosted - version: "2.4.12" + version: "2.4.11" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "7.3.1" built_collection: dependency: transitive description: @@ -598,18 +593,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: @@ -650,14 +645,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" - macros: - dependency: transitive - description: - name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" - url: "https://pub.dev" - source: hosted - version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -670,18 +657,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.12.0" mime: dependency: transitive description: @@ -720,14 +707,14 @@ packages: path: "../passkit" relative: true source: path - version: "0.0.5" + version: "0.0.6" passkit_ui: dependency: "direct main" description: path: "../passkit_ui" relative: true source: path - version: "0.0.4" + version: "0.0.5" path: dependency: "direct main" description: @@ -844,10 +831,10 @@ packages: dependency: transitive description: name: process_run - sha256: "112a77da35be50617ed9e2230df68d0817972f225e7f97ce8336f76b4e601606" + sha256: c917dfb5f7afad4c7485bc00a4df038621248fce046105020cea276d1a87c820 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.1.0" pub_semver: dependency: transitive description: @@ -1057,10 +1044,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.5.4+2" + version: "2.5.4" sqflite_common_ffi: dependency: transitive description: @@ -1073,10 +1060,10 @@ packages: dependency: transitive description: name: sqflite_common_ffi_web - sha256: "5aa15408f29eca8cc8dcca653c38d66cf9a5fb5a2c1e9826a75ce4ae4938dec1" + sha256: e9d1cb35a5ff7c43072968ed734e0a1a859564fd2b2c8654e0c6244a57dc82a8 url: "https://pub.dev" source: hosted - version: "0.4.5+2" + version: "0.4.4" sqlite3: dependency: transitive description: @@ -1145,10 +1132,10 @@ packages: dependency: transitive description: name: synchronized - sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -1161,10 +1148,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.0" timezone: dependency: transitive description: @@ -1209,10 +1196,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab + sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 url: "https://pub.dev" source: hosted - version: "6.3.10" + version: "6.3.9" url_launcher_ios: dependency: transitive description: @@ -1281,10 +1268,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.1" wakelock_plus: dependency: "direct main" description: @@ -1366,5 +1353,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.1" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.3" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index dec2450..48b4bec 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: "none" version: 0.1.0 environment: - sdk: ">=3.5.0 <4.0.0" - flutter: 3.24.1 + sdk: ">=3.4.0 <4.0.0" + flutter: 3.22.3 dependencies: android_intent_plus: ^5.0.2 diff --git a/apple_passkit/example/pubspec.lock b/apple_passkit/example/pubspec.lock index 45a6e09..d7f4733 100644 --- a/apple_passkit/example/pubspec.lock +++ b/apple_passkit/example/pubspec.lock @@ -109,18 +109,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: @@ -149,18 +149,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.12.0" path: dependency: transitive description: @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.4" process: dependency: transitive description: @@ -242,10 +242,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.0" vector_math: dependency: transitive description: @@ -258,10 +258,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.1" webdriver: dependency: transitive description: diff --git a/passkit_ui/example/pubspec.lock b/passkit_ui/example/pubspec.lock index 02d37fa..8aaeb77 100644 --- a/passkit_ui/example/pubspec.lock +++ b/passkit_ui/example/pubspec.lock @@ -167,18 +167,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: @@ -207,32 +207,32 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.12.0" passkit: dependency: "direct main" description: path: "../../passkit" relative: true source: path - version: "0.0.5" + version: "0.0.6" passkit_ui: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.0.4" + version: "0.0.5" path: dependency: transitive description: @@ -330,10 +330,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.0" typed_data: dependency: transitive description: @@ -354,10 +354,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.1" web: dependency: transitive description: From 0dc6230c25624a578413d4d9fe793ac343f91cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Fri, 11 Oct 2024 17:00:41 +0200 Subject: [PATCH 11/19] order web service wip --- passkit/lib/src/order/pk_order.dart | 1 + .../order_webservice/order_identifiers.dart | 23 ++ .../order_webservice/order_identifiers.g.dart | 21 ++ .../order_webservice/order_web_client.dart | 246 ++++++++++++++++++ .../order_web_client_exceptions.dart | 17 ++ passkit/lib/src/pkpass/pass_data.g.dart | 2 +- 6 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 passkit/lib/src/order_webservice/order_identifiers.dart create mode 100644 passkit/lib/src/order_webservice/order_identifiers.g.dart create mode 100644 passkit/lib/src/order_webservice/order_web_client.dart create mode 100644 passkit/lib/src/order_webservice/order_web_client_exceptions.dart diff --git a/passkit/lib/src/order/pk_order.dart b/passkit/lib/src/order/pk_order.dart index 350e0df..e63c8b9 100644 --- a/passkit/lib/src/order/pk_order.dart +++ b/passkit/lib/src/order/pk_order.dart @@ -85,6 +85,7 @@ class PkOrder { // TODO(any): Do proper image loading for orders // Do a sanity check for paths that already contains @2x/@3x modifier + // What about localized images? PkImage loadImage(String path, {String? locale}) { assert(path.isNotEmpty); diff --git a/passkit/lib/src/order_webservice/order_identifiers.dart b/passkit/lib/src/order_webservice/order_identifiers.dart new file mode 100644 index 0000000..46cb5ce --- /dev/null +++ b/passkit/lib/src/order_webservice/order_identifiers.dart @@ -0,0 +1,23 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'order_identifiers.g.dart'; + +@JsonSerializable() +class OrderIdentifiers { + OrderIdentifiers({ + required this.orderIdentifiers, + required this.lastUpdated, + }); + + factory OrderIdentifiers.fromJson(Map json) => + _$OrderIdentifiersFromJson(json); + + Map toJson() => _$OrderIdentifiersToJson(this); + + /// An array of order identifer strings. + @JsonKey(name: 'orderIdentifiers') + final List orderIdentifiers; + + /// The date and time of when an order was last changed. + final String lastUpdated; +} diff --git a/passkit/lib/src/order_webservice/order_identifiers.g.dart b/passkit/lib/src/order_webservice/order_identifiers.g.dart new file mode 100644 index 0000000..48efea3 --- /dev/null +++ b/passkit/lib/src/order_webservice/order_identifiers.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'order_identifiers.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OrderIdentifiers _$OrderIdentifiersFromJson(Map json) => + OrderIdentifiers( + orderIdentifiers: (json['orderIdentifiers'] as List) + .map((e) => e as String) + .toList(), + lastUpdated: json['lastUpdated'] as String, + ); + +Map _$OrderIdentifiersToJson(OrderIdentifiers instance) => + { + 'orderIdentifiers': instance.orderIdentifiers, + 'lastUpdated': instance.lastUpdated, + }; diff --git a/passkit/lib/src/order_webservice/order_web_client.dart b/passkit/lib/src/order_webservice/order_web_client.dart new file mode 100644 index 0000000..b014720 --- /dev/null +++ b/passkit/lib/src/order_webservice/order_web_client.dart @@ -0,0 +1,246 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:passkit/src/order/pk_order.dart'; +import 'package:passkit/src/order_webservice/order_identifiers.dart'; +import 'package:passkit/src/order_webservice/order_web_client_exceptions.dart'; +import 'package:passkit/src/utils.dart'; + +/// This class allows you to update a [Order] the latest version, if the pass +/// allows it. +/// +/// Docs: +/// [https://developer.apple.com/documentation/walletpasses/send_an_updated_pass] +class OrderWebClient { + /// If a [client] is passed to the constructor it will be used, otherwise a + /// default instance will be created. This is useful for testing, or for using + /// other implementations like [https://pub.dev/packages/cupertino_http] or + /// [https://pub.dev/packages/cronet_http]. + OrderWebClient({Client? client}) : _client = client ?? Client(); + + final Client _client; + + /// Loads the latest version for the given pass. Throws, if the pass doesn't + /// support being updated. To check whether a pass supports being updated, + /// check whether [PkOrder.isWebServiceAvailable] returns true. + /// + /// Returns null if no new order is available. + /// + /// If [modifiedSince] is present, only updates after [modifiedSince] will be + /// considered. + Future getLatestVersion( + PkOrder order, { + DateTime? modifiedSince, + }) async { + if (!order.isWebServiceAvailable) { + throw OrderWebServiceUnsupported(); + } + + final webServiceUrl = order.order.webServiceURL!; + final authenticationToken = order.order.authenticationToken!; + + final identifier = order.order.orderTypeIdentifier; + final serial = order.order.orderIdentifier; + final endpoint = + webServiceUrl.appendPathSegments(['v1', 'orders', identifier, serial]); + + final response = await _client.get( + endpoint, + headers: { + if (modifiedSince != null) + 'If-Modified-Since': formatHttpDate(modifiedSince), + 'Authorization': 'ApplePass $authenticationToken', + }, + ); + + final bytes = switch (response.statusCode) { + 200 => response.bodyBytes, + 304 => null, + 401 => throw OrderWebServiceAuthenticationError(), + _ => throw OrderWebServiceUnrecognizedStatusCode(response.statusCode), + }; + + if (bytes != null) { + return PkOrder.fromBytes(bytes); + } + return null; + } + + /// Record a message on the server. + /// + /// Docs: + /// [https://developer.apple.com/documentation/walletpasses/log_a_message] + Future logMessages(PkOrder order, List messages) async { + final webServiceUrl = order.order.webServiceURL; + if (webServiceUrl == null) { + throw OrderWebServiceUnsupported(); + } + + final endpoint = webServiceUrl.appendPathSegments(['v1', 'log']); + + await _client.post(endpoint, body: jsonEncode(messages)); + } + + /// Set up change notifications for a pass on a device. + /// + /// [deviceLibraryIdentifier] : A unique identifier you use to identify and + /// authenticate the device. + /// + /// [pushToken] : A push token the server uses to send update notifications + /// for a registered pass to a device. + /// + /// Docs: + /// https://developer.apple.com/documentation/walletpasses/register_a_pass_for_update_notifications + Future setupNotifications( + PkOrder order, { + required String deviceLibraryIdentifier, + required String pushToken, + }) async { + // https://yourpasshost.example.com/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber} + + final webServiceUrl = order.order.webServiceURL; + if (webServiceUrl == null) { + throw OrderWebServiceUnsupported(); + } + final authenticationToken = order.order.authenticationToken!; + + final endpoint = webServiceUrl.appendPathSegments( + [ + 'v1', + 'devices', + deviceLibraryIdentifier, + 'registrations', + order.order.orderTypeIdentifier, + order.order.orderIdentifier, + ], + ); + + final response = await _client.post( + endpoint, + headers: { + 'Authorization': 'ApplePass $authenticationToken', + }, + body: jsonEncode({ + 'pushToken': pushToken, + }), + ); + + switch (response.statusCode) { + case 200: + // The serial number is already registered for the device. + // We consider this success too, so no throwing + null; + case 201: + // Success. The registration is successful. + null; + case 401: + throw OrderWebServiceAuthenticationError(); + default: + throw OrderWebServiceUnrecognizedStatusCode(response.statusCode); + } + } + + /// Unregister a Pass for Update Notifications + /// Stop sending update notifications for a pass on a device. + /// + /// [deviceLibraryIdentifier] : A unique identifier you use to identify and + /// authenticate the device. + /// + /// Docs: + /// https://developer.apple.com/documentation/walletpasses/unregister_a_pass_for_update_notifications + Future stopNotifications( + PkOrder order, { + required String deviceLibraryIdentifier, + }) async { + // https://yourpasshost.example.com/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber} + + final webServiceUrl = order.order.webServiceURL; + if (webServiceUrl == null) { + throw OrderWebServiceUnsupported(); + } + final authenticationToken = order.order.authenticationToken!; + + final endpoint = webServiceUrl.appendPathSegments( + [ + 'v1', + 'devices', + deviceLibraryIdentifier, + 'registrations', + order.order.orderTypeIdentifier, + order.order.orderIdentifier, + ], + ); + + final response = await _client.delete( + endpoint, + headers: { + 'Authorization': 'ApplePass $authenticationToken', + }, + ); + + switch (response.statusCode) { + case 200: + // The serial number is already registered for the device. + // We consider this success too, so no throwing + null; + case 401: + throw OrderWebServiceAuthenticationError(); + default: + throw OrderWebServiceUnrecognizedStatusCode(response.statusCode); + } + } + + /// Send the serial numbers for updated passes to a device. + /// + /// [deviceLibraryIdentifier] : A unique identifier you use to identify and + /// authenticate the device. + /// + /// [previousLastUpdated] : The value of the lastUpdated key from the + /// SerialNumbers object returned in a previous request. This value limits the + /// results of the current request to the passes updated since that previous + /// request. + /// + /// Docs: https://developer.apple.com/documentation/walletpasses/get_the_list_of_updatable_passes + Future getListOfUpdatablePasses( + PkOrder order, { + required String deviceLibraryIdentifier, + required String? previousLastUpdated, + }) async { + // https://yourpasshost.example.com/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}?passesUpdatedSince={previousLastUpdated} + final webServiceUrl = order.order.webServiceURL; + if (webServiceUrl == null) { + throw OrderWebServiceUnsupported(); + } + + final endpoint = webServiceUrl.appendPathSegments([ + 'v1', + 'devices', + deviceLibraryIdentifier, + 'registrations', + order.order.orderTypeIdentifier, + ]).replace( + queryParameters: { + if (previousLastUpdated != null) + 'passesUpdatedSince': previousLastUpdated, + }, + ); + + final response = await _client.get(endpoint); + + return switch (response.statusCode) { + 200 => () { + final responseJson = utf8JsonDecode(response.bodyBytes)!; + return OrderIdentifiers.fromJson(responseJson); + }(), + 204 => null, + 401 => throw OrderWebServiceAuthenticationError(), + _ => throw OrderWebServiceUnrecognizedStatusCode(response.statusCode), + }; + } +} + +extension on Uri { + Uri appendPathSegments(List additionalSegments) => + replace(pathSegments: [...pathSegments, ...additionalSegments]); +} diff --git a/passkit/lib/src/order_webservice/order_web_client_exceptions.dart b/passkit/lib/src/order_webservice/order_web_client_exceptions.dart new file mode 100644 index 0000000..1410c9f --- /dev/null +++ b/passkit/lib/src/order_webservice/order_web_client_exceptions.dart @@ -0,0 +1,17 @@ +class OrderWebServiceUnsupported implements Exception { + @override + String toString() => "The Order file doesn't specify a 'webServiceURL' and " + "thus there's no possibility to make a HTTP request."; +} + +class OrderWebServiceAuthenticationError implements Exception {} + +class OrderWebServiceUnrecognizedStatusCode implements Exception { + OrderWebServiceUnrecognizedStatusCode(this.statusCode); + + final int statusCode; + + @override + String toString() => + 'OrderWebServiceUnrecognizedStatusCode(statusCode: $statusCode)'; +} diff --git a/passkit/lib/src/pkpass/pass_data.g.dart b/passkit/lib/src/pkpass/pass_data.g.dart index 6b7f0b3..fe22fc2 100644 --- a/passkit/lib/src/pkpass/pass_data.g.dart +++ b/passkit/lib/src/pkpass/pass_data.g.dart @@ -8,11 +8,11 @@ part of 'pass_data.dart'; PassData _$PassDataFromJson(Map json) => PassData( description: json['description'] as String, - formatVersion: (json['formatVersion'] as num).toInt(), organizationName: json['organizationName'] as String, passTypeIdentifier: json['passTypeIdentifier'] as String, serialNumber: json['serialNumber'] as String, teamIdentifier: json['teamIdentifier'] as String, + formatVersion: (json['formatVersion'] as num?)?.toInt() ?? 1, appLaunchURL: json['appLaunchURL'] as String?, associatedStoreIdentifiers: (json['associatedStoreIdentifiers'] as List?) From 1d36cec05137a32095acc791f83f38799af4a7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Fri, 11 Oct 2024 20:28:18 +0200 Subject: [PATCH 12/19] progress --- passkit/CHANGELOG.md | 2 + passkit/lib/passkit.dart | 3 + passkit/lib/src/order/pk_order.dart | 5 ++ .../order_webservice/order_web_client.dart | 62 +++++++++---------- 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/passkit/CHANGELOG.md b/passkit/CHANGELOG.md index 41b1329..20211e1 100644 --- a/passkit/CHANGELOG.md +++ b/passkit/CHANGELOG.md @@ -1,6 +1,8 @@ ## Unreleased - No longer mark `PkPass.write()` as experimental +- Add webservice support for orders +- Add image support for orders ## 0.0.10 diff --git a/passkit/lib/passkit.dart b/passkit/lib/passkit.dart index b9bacda..fbf5cd6 100644 --- a/passkit/lib/passkit.dart +++ b/passkit/lib/passkit.dart @@ -16,6 +16,9 @@ export 'src/order/order_return.dart'; export 'src/order/order_return_info.dart'; export 'src/order/order_shipping_fulfillment.dart'; export 'src/order/pk_order.dart'; +export 'src/order_webservice/order_identifiers.dart'; +export 'src/order_webservice/order_web_client.dart'; +export 'src/order_webservice/order_web_client_exceptions.dart'; export 'src/pk_image.dart'; export 'src/pkpass/barcode.dart'; export 'src/pkpass/beacon.dart'; diff --git a/passkit/lib/src/order/pk_order.dart b/passkit/lib/src/order/pk_order.dart index e63c8b9..ddc5949 100644 --- a/passkit/lib/src/order/pk_order.dart +++ b/passkit/lib/src/order/pk_order.dart @@ -86,6 +86,11 @@ class PkOrder { // TODO(any): Do proper image loading for orders // Do a sanity check for paths that already contains @2x/@3x modifier // What about localized images? + // + // The Apple docs state that + // > Adding an image of the same name to the top-level folder of the order + // > file overrides any localized versions. + // So non-localized images are actually the fallback. PkImage loadImage(String path, {String? locale}) { assert(path.isNotEmpty); diff --git a/passkit/lib/src/order_webservice/order_web_client.dart b/passkit/lib/src/order_webservice/order_web_client.dart index b014720..fbcad3c 100644 --- a/passkit/lib/src/order_webservice/order_web_client.dart +++ b/passkit/lib/src/order_webservice/order_web_client.dart @@ -7,11 +7,11 @@ import 'package:passkit/src/order_webservice/order_identifiers.dart'; import 'package:passkit/src/order_webservice/order_web_client_exceptions.dart'; import 'package:passkit/src/utils.dart'; -/// This class allows you to update a [Order] the latest version, if the pass +/// This class allows you to update a [PkOrder] the latest version, if the order /// allows it. /// /// Docs: -/// [https://developer.apple.com/documentation/walletpasses/send_an_updated_pass] +/// [https://developer.apple.com/documentation/walletorders] class OrderWebClient { /// If a [client] is passed to the constructor it will be used, otherwise a /// default instance will be created. This is useful for testing, or for using @@ -21,8 +21,8 @@ class OrderWebClient { final Client _client; - /// Loads the latest version for the given pass. Throws, if the pass doesn't - /// support being updated. To check whether a pass supports being updated, + /// Loads the latest version for the given order. Throws, if the order doesn't + /// support being updated. To check whether an order supports being updated, /// check whether [PkOrder.isWebServiceAvailable] returns true. /// /// Returns null if no new order is available. @@ -50,7 +50,7 @@ class OrderWebClient { headers: { if (modifiedSince != null) 'If-Modified-Since': formatHttpDate(modifiedSince), - 'Authorization': 'ApplePass $authenticationToken', + 'Authorization': 'AppleOrder $authenticationToken', }, ); @@ -70,7 +70,7 @@ class OrderWebClient { /// Record a message on the server. /// /// Docs: - /// [https://developer.apple.com/documentation/walletpasses/log_a_message] + /// [https://developer.apple.com/documentation/walletorders/receive-log-messages] Future logMessages(PkOrder order, List messages) async { final webServiceUrl = order.order.webServiceURL; if (webServiceUrl == null) { @@ -82,22 +82,22 @@ class OrderWebClient { await _client.post(endpoint, body: jsonEncode(messages)); } - /// Set up change notifications for a pass on a device. + /// Set up change notifications for an order on a device. /// - /// [deviceLibraryIdentifier] : A unique identifier you use to identify and + /// [deviceIdentifier] : A unique identifier you use to identify and /// authenticate the device. /// /// [pushToken] : A push token the server uses to send update notifications /// for a registered pass to a device. /// /// Docs: - /// https://developer.apple.com/documentation/walletpasses/register_a_pass_for_update_notifications + /// https://developer.apple.com/documentation/walletorders/register-a-device-for-update-notifications Future setupNotifications( PkOrder order, { - required String deviceLibraryIdentifier, + required String deviceIdentifier, required String pushToken, }) async { - // https://yourpasshost.example.com/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber} + // https://your-web-service.com/v1/devices/{deviceIdentifier}/registrations/{orderTypeIdentifier}/{orderIdentifier} final webServiceUrl = order.order.webServiceURL; if (webServiceUrl == null) { @@ -109,7 +109,7 @@ class OrderWebClient { [ 'v1', 'devices', - deviceLibraryIdentifier, + deviceIdentifier, 'registrations', order.order.orderTypeIdentifier, order.order.orderIdentifier, @@ -119,7 +119,7 @@ class OrderWebClient { final response = await _client.post( endpoint, headers: { - 'Authorization': 'ApplePass $authenticationToken', + 'Authorization': 'AppleOrder $authenticationToken', }, body: jsonEncode({ 'pushToken': pushToken, @@ -141,19 +141,19 @@ class OrderWebClient { } } - /// Unregister a Pass for Update Notifications - /// Stop sending update notifications for a pass on a device. + /// Unregister an order for update notifications + /// Stop sending update notifications for an order on a device. /// - /// [deviceLibraryIdentifier] : A unique identifier you use to identify and + /// [deviceIdentifier] : A unique identifier you use to identify and /// authenticate the device. /// /// Docs: - /// https://developer.apple.com/documentation/walletpasses/unregister_a_pass_for_update_notifications + /// https://developer.apple.com/documentation/walletorders/unregister-a-device-from-update-notifications Future stopNotifications( PkOrder order, { - required String deviceLibraryIdentifier, + required String deviceIdentifier, }) async { - // https://yourpasshost.example.com/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber} + // https://your-web-service.com/v1/devices/{deviceIdentifier}/registrations/{orderTypeIdentifier}/{orderIdentifier} final webServiceUrl = order.order.webServiceURL; if (webServiceUrl == null) { @@ -165,7 +165,7 @@ class OrderWebClient { [ 'v1', 'devices', - deviceLibraryIdentifier, + deviceIdentifier, 'registrations', order.order.orderTypeIdentifier, order.order.orderIdentifier, @@ -175,7 +175,7 @@ class OrderWebClient { final response = await _client.delete( endpoint, headers: { - 'Authorization': 'ApplePass $authenticationToken', + 'Authorization': 'AppleOrder $authenticationToken', }, ); @@ -193,21 +193,22 @@ class OrderWebClient { /// Send the serial numbers for updated passes to a device. /// - /// [deviceLibraryIdentifier] : A unique identifier you use to identify and + /// [deviceIdentifier] : A unique identifier you use to identify and /// authenticate the device. /// - /// [previousLastUpdated] : The value of the lastUpdated key from the + /// [lastModified] : The value of the lastUpdated key from the /// SerialNumbers object returned in a previous request. This value limits the /// results of the current request to the passes updated since that previous /// request. /// - /// Docs: https://developer.apple.com/documentation/walletpasses/get_the_list_of_updatable_passes - Future getListOfUpdatablePasses( + /// Docs: + /// https://developer.apple.com/documentation/walletorders/retrieve-the-registrations-for-a-device + Future getListOfRegisteredOrders( PkOrder order, { - required String deviceLibraryIdentifier, - required String? previousLastUpdated, + required String deviceIdentifier, + required String? lastModified, }) async { - // https://yourpasshost.example.com/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}?passesUpdatedSince={previousLastUpdated} + // https://your-web-service.com/v1/devices/{deviceIdentifier}/registrations/{orderTypeIdentifier}?ordersModifiedSince={lastModified} final webServiceUrl = order.order.webServiceURL; if (webServiceUrl == null) { throw OrderWebServiceUnsupported(); @@ -216,13 +217,12 @@ class OrderWebClient { final endpoint = webServiceUrl.appendPathSegments([ 'v1', 'devices', - deviceLibraryIdentifier, + deviceIdentifier, 'registrations', order.order.orderTypeIdentifier, ]).replace( queryParameters: { - if (previousLastUpdated != null) - 'passesUpdatedSince': previousLastUpdated, + if (lastModified != null) 'ordersModifiedSince': lastModified, }, ); From 248fcb0264177c00636e7d395f8405094d949f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 13 Oct 2024 10:58:20 +0200 Subject: [PATCH 13/19] foo --- app/macos/Podfile.lock | 7 +++++++ app/macos/Runner/AppDelegate.swift | 2 +- app/pubspec.lock | 10 +++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/macos/Podfile.lock b/app/macos/Podfile.lock index 382c2cd..3eb011d 100644 --- a/app/macos/Podfile.lock +++ b/app/macos/Podfile.lock @@ -18,6 +18,9 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS - "sqlite3 (3.46.0+1)": - "sqlite3/common (= 3.46.0+1)" - "sqlite3/common (3.46.0+1)" @@ -51,6 +54,7 @@ DEPENDENCIES: - screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) @@ -78,6 +82,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sqflite: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin sqlite3_flutter_libs: :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos url_launcher_macos: @@ -95,6 +101,7 @@ SPEC CHECKSUMS: screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 diff --git a/app/macos/Runner/AppDelegate.swift b/app/macos/Runner/AppDelegate.swift index d53ef64..8e02df2 100644 --- a/app/macos/Runner/AppDelegate.swift +++ b/app/macos/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/app/pubspec.lock b/app/pubspec.lock index 73a483b..04ae82b 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -554,6 +554,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" + source: hosted + version: "4.2.0" intl: dependency: "direct main" description: @@ -712,7 +720,7 @@ packages: path: "../passkit" relative: true source: path - version: "0.0.7" + version: "0.0.10" passkit_ui: dependency: "direct main" description: From da9009d14314909c7534205d97e939b834ee897d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 13 Oct 2024 11:29:21 +0200 Subject: [PATCH 14/19] remove go_router --- app/lib/example/example_passes.dart | 4 +- app/lib/home/home_page.dart | 22 ++--- app/lib/import_order/import_order_page.dart | 6 +- app/lib/import_order/pick_order.dart | 4 +- app/lib/import_order/receive_pass.dart | 4 +- app/lib/import_pass/import_page.dart | 7 +- app/lib/import_pass/pick_pass.dart | 8 +- app/lib/import_pass/receive_pass.dart | 4 +- app/lib/main_app.dart | 5 +- app/lib/router.dart | 89 ++++++++++++--------- app/pubspec.lock | 18 ++--- app/pubspec.yaml | 1 - passkit_ui/example/pubspec.lock | 42 +++++----- 13 files changed, 115 insertions(+), 99 deletions(-) diff --git a/app/lib/example/example_passes.dart b/app/lib/example/example_passes.dart index 4b01d22..ff69411 100644 --- a/app/lib/example/example_passes.dart +++ b/app/lib/example/example_passes.dart @@ -44,9 +44,9 @@ class _ExamplePassesState extends State { child: InkWell( child: PkPassWidget(pass: pass), onTap: () { - router.push( + navigator.pushNamed( '/backside', - extra: PassBackSidePageArgs(pass, false), + arguments: PassBackSidePageArgs(pass, false), ); }, ), diff --git a/app/lib/home/home_page.dart b/app/lib/home/home_page.dart index cd66a09..3398cd5 100644 --- a/app/lib/home/home_page.dart +++ b/app/lib/home/home_page.dart @@ -40,17 +40,17 @@ class _HomePageState extends State { // TODO(ueman): Add more validation if (firstFile.name.endsWith('pkpass')) { - await router.push( + await navigator.pushNamed( '/import', - extra: PkPassImportSource( + arguments: PkPassImportSource( bytes: await detail.files.first.readAsBytes(), ), ); } if (firstFile.name.endsWith('order')) { - await router.push( + await navigator.pushNamed( '/importOrder', - extra: PkOrderImportSource( + arguments: PkOrderImportSource( bytes: await detail.files.first.readAsBytes(), ), ); @@ -62,7 +62,7 @@ class _HomePageState extends State { centerTitle: true, leading: kDebugMode ? IconButton( - onPressed: () => router.push('/examples'), + onPressed: () => navigator.pushNamed('/examples'), icon: const Icon(Icons.card_giftcard), ) : null, @@ -72,7 +72,7 @@ class _HomePageState extends State { icon: const Icon(Icons.file_open), ), IconButton( - onPressed: () => router.push('/settings'), + onPressed: () => navigator.pushNamed('/settings'), icon: const Icon(Icons.settings), tooltip: AppLocalizations.of(context).settings, ), @@ -107,9 +107,10 @@ class _HomePageState extends State { return PassListTile( pass: pass, onTap: () { - router.push( + navigator.pushNamed( '/backside', - extra: PassBackSidePageArgs(pass, true), + arguments: + PassBackSidePageArgs(pass, true), ); }, ); @@ -119,9 +120,10 @@ class _HomePageState extends State { child: InkWell( child: PkPassWidget(pass: pass), onTap: () { - router.push( + navigator.pushNamed( '/backside', - extra: PassBackSidePageArgs(pass, true), + arguments: + PassBackSidePageArgs(pass, true), ); }, ), diff --git a/app/lib/import_order/import_order_page.dart b/app/lib/import_order/import_order_page.dart index 4dcab97..e35a801 100644 --- a/app/lib/import_order/import_order_page.dart +++ b/app/lib/import_order/import_order_page.dart @@ -1,9 +1,9 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:content_resolver/content_resolver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:go_router/go_router.dart'; import 'package:passkit/passkit.dart'; import 'package:passkit_ui/passkit_ui.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -15,7 +15,7 @@ class PkOrderImportSource { ); final String? contentResolverPath; - final List? bytes; + final Uint8List? bytes; final String? filePath; Future getOrder() async { @@ -78,7 +78,7 @@ class _ImportOrderPageState extends State { actions: [ IconButton( // TODO(ueman): Maybe show confirmation dialog here - onPressed: () => context.pop(), + onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.delete), ), ], diff --git a/app/lib/import_order/pick_order.dart b/app/lib/import_order/pick_order.dart index 9076219..dd34fc8 100644 --- a/app/lib/import_order/pick_order.dart +++ b/app/lib/import_order/pick_order.dart @@ -30,8 +30,8 @@ Future pickOrder(BuildContext context) async { return; } - await router.push( + await navigator.pushNamed( '/importOrder', - extra: PkOrderImportSource(contentResolverPath: firstPath), + arguments: PkOrderImportSource(contentResolverPath: firstPath), ); } diff --git a/app/lib/import_order/receive_pass.dart b/app/lib/import_order/receive_pass.dart index 66dd07b..21e2e95 100644 --- a/app/lib/import_order/receive_pass.dart +++ b/app/lib/import_order/receive_pass.dart @@ -48,9 +48,9 @@ Future _onIntent(Intent? receivedIntent) async { return; } unawaited( - router.push( + navigator.pushNamed( '/import', - extra: PkPassImportSource(contentResolverPath: path), + arguments: PkPassImportSource(contentResolverPath: path), ), ); } diff --git a/app/lib/import_pass/import_page.dart b/app/lib/import_pass/import_page.dart index b996bd8..31c0d5d 100644 --- a/app/lib/import_pass/import_page.dart +++ b/app/lib/import_pass/import_page.dart @@ -10,7 +10,6 @@ import 'package:app/router.dart'; import 'package:content_resolver/content_resolver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:go_router/go_router.dart'; import 'package:passkit/passkit.dart'; import 'package:passkit_ui/passkit_ui.dart'; @@ -88,9 +87,9 @@ class _ImportPassPageState extends State { child: InkWell( child: Center(child: PkPassWidget(pass: pass!)), onTap: () { - router.push( + navigator.pushNamed( '/backside', - extra: PassBackSidePageArgs(pass!, false), + arguments: PassBackSidePageArgs(pass!, false), ); }, ), @@ -128,7 +127,7 @@ class _ImportPassPageState extends State { actions: [ IconButton( // TODO(ueman): Maybe show confirmation dialog here - onPressed: () => context.pop(), + onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.delete), ), ], diff --git a/app/lib/import_pass/pick_pass.dart b/app/lib/import_pass/pick_pass.dart index 2a15611..1d6cd44 100644 --- a/app/lib/import_pass/pick_pass.dart +++ b/app/lib/import_pass/pick_pass.dart @@ -26,17 +26,17 @@ Future pickPass(BuildContext context) async { } if ({'.pkpass', '.pass'}.contains(extension(firstPath))) { - await router.push( + await navigator.pushNamed( '/import', - extra: PkPassImportSource(filePath: firstPath), + arguments: PkPassImportSource(filePath: firstPath), ); return; } if ('.order' == extension(firstPath)) { - await router.push( + await navigator.pushNamed( '/importOrder', - extra: PkOrderImportSource(filePath: firstPath), + arguments: PkOrderImportSource(filePath: firstPath), ); return; } diff --git a/app/lib/import_pass/receive_pass.dart b/app/lib/import_pass/receive_pass.dart index 66dd07b..21e2e95 100644 --- a/app/lib/import_pass/receive_pass.dart +++ b/app/lib/import_pass/receive_pass.dart @@ -48,9 +48,9 @@ Future _onIntent(Intent? receivedIntent) async { return; } unawaited( - router.push( + navigator.pushNamed( '/import', - extra: PkPassImportSource(contentResolverPath: path), + arguments: PkPassImportSource(contentResolverPath: path), ), ); } diff --git a/app/lib/main_app.dart b/app/lib/main_app.dart index c0af653..58c1dd5 100644 --- a/app/lib/main_app.dart +++ b/app/lib/main_app.dart @@ -9,9 +9,9 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp.router( + return MaterialApp( debugShowCheckedModeBanner: false, - routerConfig: router, + navigatorKey: navigatorKey, scaffoldMessengerKey: scaffoldMessenger, theme: _lightTheme(), darkTheme: _darkTheme(), @@ -19,6 +19,7 @@ class MainApp extends StatelessWidget { onGenerateTitle: (context) => AppLocalizations.of(context).appName, supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, + onGenerateRoute: routeGenerator, ); } } diff --git a/app/lib/router.dart b/app/lib/router.dart index b16d7f9..4a459c2 100644 --- a/app/lib/router.dart +++ b/app/lib/router.dart @@ -4,41 +4,56 @@ import 'package:app/import_order/import_order_page.dart'; import 'package:app/import_pass/import_page.dart'; import 'package:app/pass_backside/pass_backside_page.dart'; import 'package:app/settings/settings_page.dart'; -import 'package:go_router/go_router.dart'; +import 'package:flutter/material.dart'; -final router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, state) => const HomePage(), - ), - GoRoute( - path: '/import', - builder: (context, state) => - ImportPassPage(source: state.extra as PkPassImportSource), - ), - GoRoute( - path: '/importOrder', - builder: (context, state) => - ImportOrderPage(source: state.extra as PkOrderImportSource), - ), - GoRoute( - path: '/examples', - builder: (context, state) => const ExamplePasses(), - ), - GoRoute( - path: '/settings', - builder: (context, state) => const SettingsPage(), - ), - GoRoute( - path: '/backside', - builder: (context, state) { - final args = state.extra as PassBackSidePageArgs; - return PassBacksidePage( - pass: args.pass, - showDelete: args.showDelete, - ); - }, - ), - ], -); +final navigatorKey = GlobalKey(); + +NavigatorState get navigator => navigatorKey.currentState!; + +Route? routeGenerator(RouteSettings settings) { + return switch (settings.name) { + '/' => MaterialPageRoute( + builder: (context) { + return const HomePage(); + }, + ), + '/import' => MaterialPageRoute( + builder: (context) { + return ImportPassPage( + source: settings.arguments as PkPassImportSource, + ); + }, + ), + '/importOrder' => MaterialPageRoute( + builder: (context) { + return ImportOrderPage( + source: settings.arguments as PkOrderImportSource, + ); + }, + ), + '/examples' => MaterialPageRoute( + builder: (context) { + return const ExamplePasses(); + }, + ), + '/settings' => MaterialPageRoute( + builder: (context) { + return const SettingsPage(); + }, + ), + '/backside' => MaterialPageRoute( + builder: (context) { + final args = settings.arguments as PassBackSidePageArgs; + return PassBacksidePage( + pass: args.pass, + showDelete: args.showDelete, + ); + }, + ), + _ => MaterialPageRoute( + builder: (context) { + return const HomePage(); + }, + ), + }; +} diff --git a/app/pubspec.lock b/app/pubspec.lock index 04ae82b..992dc4d 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -230,6 +230,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" dart_style: dependency: transitive description: @@ -506,14 +514,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" - go_router: - dependency: "direct main" - description: - name: go_router - sha256: "48d03a1e4887b00fe622695139246e3c778ac814eeb32421467b56d23fa64034" - url: "https://pub.dev" - source: hosted - version: "14.2.6" google_fonts: dependency: "direct main" description: @@ -1367,4 +1367,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.1" + flutter: ">=3.24.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 48b4bec..10ad20c 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -23,7 +23,6 @@ dependencies: sdk: flutter geocoding: ^3.0.0 geolocator: ^13.0.0 - go_router: ^14.2.0 google_fonts: ^6.2.1 http: ^1.2.1 intl: any diff --git a/passkit_ui/example/pubspec.lock b/passkit_ui/example/pubspec.lock index b4ec224..c97951c 100644 --- a/passkit_ui/example/pubspec.lock +++ b/passkit_ui/example/pubspec.lock @@ -9,22 +9,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.6.1" - args: - dependency: transitive - description: - name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" - url: "https://pub.dev" - source: hosted - version: "2.5.0" - asn1lib: - dependency: transitive - description: - name: asn1lib - sha256: "2ca377ad4d677bbadca278e0ba4da4e057b80a10b927bfc8f7d8bda8fe2ceb75" - url: "https://pub.dev" - source: hosted - version: "1.5.4" async: dependency: transitive description: @@ -105,14 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - encrypt: + cupertino_icons: dependency: transitive description: - name: encrypt - sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "5.0.3" + version: "1.0.8" fake_async: dependency: transitive description: @@ -155,6 +139,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" + source: hosted + version: "4.2.0" intl: dependency: transitive description: @@ -241,7 +233,7 @@ packages: path: "../../passkit" relative: true source: path - version: "0.0.7" + version: "0.0.10" passkit_ui: dependency: "direct main" description: @@ -382,6 +374,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" From 454fe11bcbe62e027734fa15919c6e85e0a2b660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 13 Oct 2024 13:51:34 +0200 Subject: [PATCH 15/19] wip --- .../src/order/order_shipping_fulfillment.dart | 20 +++- passkit_ui/lib/src/order/l10n.dart | 38 +++++++ passkit_ui/lib/src/order/order_widget.dart | 103 ++++++++++++++---- passkit_ui/lib/src/widgets/squircle.dart | 6 +- 4 files changed, 139 insertions(+), 28 deletions(-) diff --git a/passkit/lib/src/order/order_shipping_fulfillment.dart b/passkit/lib/src/order/order_shipping_fulfillment.dart index 97140a7..8c804f8 100644 --- a/passkit/lib/src/order/order_shipping_fulfillment.dart +++ b/passkit/lib/src/order/order_shipping_fulfillment.dart @@ -88,7 +88,10 @@ class OrderShippingFulfillment { /// Default: shipping /// /// Possible Values: shipping, delivery - @JsonKey(name: 'shippingType') + @JsonKey( + name: 'shippingType', + defaultValue: OrderShippingFulfillmentType.shipping, + ) final OrderShippingFulfillmentType shippingType; /// A localized message describing the fulfillment status. @@ -135,7 +138,20 @@ enum OrderShippingFulfillmentStatus { issue, @JsonValue('cancelled') - cancelled + cancelled; + + double toProgress() { + return switch (this) { + OrderShippingFulfillmentStatus.open => 0, + OrderShippingFulfillmentStatus.processing => 0.25, + OrderShippingFulfillmentStatus.shipped => 0.375, + OrderShippingFulfillmentStatus.onTheWay => 0.5, + OrderShippingFulfillmentStatus.outForDelivery => 0.75, + OrderShippingFulfillmentStatus.delivered => 1, + OrderShippingFulfillmentStatus.issue => 0, + OrderShippingFulfillmentStatus.cancelled => 0, + }; + } } @JsonSerializable() diff --git a/passkit_ui/lib/src/order/l10n.dart b/passkit_ui/lib/src/order/l10n.dart index 49eff87..ac3d5e5 100644 --- a/passkit_ui/lib/src/order/l10n.dart +++ b/passkit_ui/lib/src/order/l10n.dart @@ -1,11 +1,14 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; +import 'package:passkit/passkit.dart'; abstract class OrderLocalizations { /// Creates a [OrderLocalizations]. const OrderLocalizations(); String orderedAt(DateTime date); + String deliveredAt(DateTime dateTime); + String estimatedDeliveryAt(DateTime dateTime); String get courier; String get trackingId; String get trackShipment; @@ -41,9 +44,15 @@ abstract class OrderLocalizations { String get track; String get cancel; + /// Reads something like "Order {[index]} from {[total]}". + /// [index] is 1-indexed + String shipmentXFromY(int index, int total); + /// Merchant is responsible for the order, order details and receipt details. String get merchantIsResponsibleNote; + String shippingStatus(OrderShippingFulfillmentStatus status); + /// This method is used to obtain a localized instance of /// [OrderLocalizations]. static OrderLocalizations of(BuildContext context) { @@ -176,4 +185,33 @@ class EnOrderLocalizations extends OrderLocalizations { @override final String cancel = 'Cancel'; + + @override + String shipmentXFromY(int index, int total) => 'Shipment $index from $total'; + + @override + String shippingStatus(OrderShippingFulfillmentStatus status) { + return switch (status) { + OrderShippingFulfillmentStatus.open => 'Open', + OrderShippingFulfillmentStatus.processing => 'Processing', + OrderShippingFulfillmentStatus.onTheWay => 'On the way', + OrderShippingFulfillmentStatus.outForDelivery => 'Out for delivery', + OrderShippingFulfillmentStatus.delivered => 'Delivered', + OrderShippingFulfillmentStatus.shipped => 'Shipped', + OrderShippingFulfillmentStatus.issue => 'Issue', + OrderShippingFulfillmentStatus.cancelled => 'Cancelled', + }; + } + + @override + String deliveredAt(DateTime dateTime) { + final dateFormat = DateFormat.yMd('en_EN'); + return 'Delivered at ${dateFormat.format(dateTime)}'; + } + + @override + String estimatedDeliveryAt(DateTime dateTime) { + final dateFormat = DateFormat.yMd('en_EN'); + return 'Estimated: ${dateFormat.format(dateTime)}'; + } } diff --git a/passkit_ui/lib/src/order/order_widget.dart b/passkit_ui/lib/src/order/order_widget.dart index 16a578d..05d9311 100644 --- a/passkit_ui/lib/src/order/order_widget.dart +++ b/passkit_ui/lib/src/order/order_widget.dart @@ -77,17 +77,20 @@ class OrderWidget extends StatelessWidget { child: ListView( children: [ if (order.order.merchant.logo != null) - Padding( - padding: const EdgeInsets.all(20), - child: Squircle( - radius: 20, - child: Image.memory( - order - .loadImage(order.order.merchant.logo!) - .forCorrectPixelRatio(devicePixelRatio), - fit: BoxFit.contain, - width: 150, - height: 150, + SizedBox( + width: 150, + height: 150, + child: Center( + child: Squircle( + radius: 20, + child: Image.memory( + order + .loadImage(order.order.merchant.logo!) + .forCorrectPixelRatio(devicePixelRatio), + fit: BoxFit.cover, + width: 120, + height: 120, + ), ), ), ), @@ -110,11 +113,13 @@ class OrderWidget extends StatelessWidget { subtitle: Text(order.order.orderProvider!.displayName), ), if (order.order.fulfillments != null) - for (final fulfillment in order.order.fulfillments!) + for (final fulfillment in order.order.fulfillments!.indexed) _FulfillmentSection( - fulfillment: fulfillment, + fulfillment: fulfillment.$2, order: order, onTrackingLinkClicked: onTrackingLinkClicked, + index: fulfillment.$1, + totalOrders: order.order.fulfillments!.length, ), DetailsSection(order: order), InfoSection( @@ -239,11 +244,15 @@ class _FulfillmentSection extends StatelessWidget { required this.fulfillment, required this.order, required this.onTrackingLinkClicked, + required this.index, + required this.totalOrders, }); final Object fulfillment; final PkOrder order; final ValueChanged onTrackingLinkClicked; + final int index; + final int totalOrders; @override Widget build(BuildContext context) { @@ -252,9 +261,14 @@ class _FulfillmentSection extends StatelessWidget { fulfillment: shipping, order: order, onTrackingLinkClicked: onTrackingLinkClicked, + index: index, + totalOrders: totalOrders, + ), + OrderPickupFulfillment orderPickup => _OrderPickupFulfillmentWidget( + fulfillment: orderPickup, + index: index, + totalOrders: totalOrders, ), - OrderPickupFulfillment orderPickup => - _OrderPickupFulfillmentWidget(fulfillment: orderPickup), _ => const SizedBox.shrink() }; } @@ -265,11 +279,15 @@ class _OrderShippingFulfillmentWidget extends StatelessWidget { required this.fulfillment, required this.order, required this.onTrackingLinkClicked, + required this.index, + required this.totalOrders, }); final OrderShippingFulfillment fulfillment; final PkOrder order; final ValueChanged onTrackingLinkClicked; + final int index; + final int totalOrders; @override Widget build(BuildContext context) { @@ -278,18 +296,51 @@ class _OrderShippingFulfillmentWidget extends StatelessWidget { return CupertinoListSection.insetGrouped( children: [ + Text( + l10n.shipmentXFromY(index + 1, totalOrders), + textAlign: TextAlign.center, + style: CupertinoTheme.of(context) + .textTheme + .textStyle + .copyWith(color: CupertinoColors.systemGrey), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: LinearProgressIndicator( + valueColor: const AlwaysStoppedAnimation( + CupertinoColors.systemGreen, + ), + minHeight: 20, + borderRadius: BorderRadius.circular(10), + value: fulfillment.status.toProgress(), + ), + ), CupertinoListTile( - title: Text(fulfillment.status.name), - subtitle: Text(fulfillment.deliveredAt?.toString() ?? ''), - trailing: const Icon( - CupertinoIcons.check_mark_circled_solid, - color: CupertinoColors.systemGreen, + title: Text(l10n.shippingStatus(fulfillment.status)), + subtitle: Text( + fulfillment.deliveredAt != null + ? l10n.deliveredAt(fulfillment.deliveredAt!) + : '', + ), + trailing: IconButton( + onPressed: () { + showBottomSheet( + context: context, + builder: (_) { + return Text(fulfillment.notes!); + }, + ); + }, + icon: const Icon( + CupertinoIcons.info_circle, + color: CupertinoColors.systemBlue, + ), ), ), - if (fulfillment.notes != null) + if (fulfillment.statusDescription != null) CupertinoListTile( title: Text(l10n.from(order.order.merchant.displayName)), - subtitle: Text(fulfillment.notes!), + subtitle: Text(fulfillment.statusDescription!), ), if (fulfillment.carrier != null) CupertinoListTile.notched( @@ -332,9 +383,15 @@ class _OrderShippingFulfillmentWidget extends StatelessWidget { } class _OrderPickupFulfillmentWidget extends StatelessWidget { - const _OrderPickupFulfillmentWidget({required this.fulfillment}); + const _OrderPickupFulfillmentWidget({ + required this.fulfillment, + required this.index, + required this.totalOrders, + }); final OrderPickupFulfillment fulfillment; + final int index; + final int totalOrders; @override Widget build(BuildContext context) { diff --git a/passkit_ui/lib/src/widgets/squircle.dart b/passkit_ui/lib/src/widgets/squircle.dart index e35df49..8a737df 100644 --- a/passkit_ui/lib/src/widgets/squircle.dart +++ b/passkit_ui/lib/src/widgets/squircle.dart @@ -8,11 +8,11 @@ class Squircle extends StatelessWidget { @override Widget build(BuildContext context) { + final r = BorderRadius.all(Radius.circular(radius)); + return ClipPath( clipper: ShapeBorderClipper( - shape: ContinuousRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(radius)), - ), + shape: ContinuousRectangleBorder(borderRadius: r), ), child: child, ); From 9b90ebcc1059a94b132d61e0fb4df14bdb1de613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 13 Oct 2024 15:01:24 +0200 Subject: [PATCH 16/19] wip --- passkit_ui/lib/src/order/fulfillment.dart | 41 ++++ passkit_ui/lib/src/order/order_widget.dart | 163 +------------ .../lib/src/order/pickup_fulfillment.dart | 20 ++ .../lib/src/order/shipping_fulfillment.dart | 227 ++++++++++++++++++ 4 files changed, 290 insertions(+), 161 deletions(-) create mode 100644 passkit_ui/lib/src/order/fulfillment.dart create mode 100644 passkit_ui/lib/src/order/pickup_fulfillment.dart create mode 100644 passkit_ui/lib/src/order/shipping_fulfillment.dart diff --git a/passkit_ui/lib/src/order/fulfillment.dart b/passkit_ui/lib/src/order/fulfillment.dart new file mode 100644 index 0000000..eecadf8 --- /dev/null +++ b/passkit_ui/lib/src/order/fulfillment.dart @@ -0,0 +1,41 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:passkit/passkit.dart'; +import 'package:passkit_ui/src/order/pickup_fulfillment.dart'; +import 'package:passkit_ui/src/order/shipping_fulfillment.dart'; + +class FulfillmentSection extends StatelessWidget { + const FulfillmentSection({ + super.key, + required this.fulfillment, + required this.order, + required this.onTrackingLinkClicked, + required this.index, + required this.totalOrders, + }); + + final Object fulfillment; + final PkOrder order; + final ValueChanged onTrackingLinkClicked; + final int index; + final int totalOrders; + + @override + Widget build(BuildContext context) { + return switch (fulfillment) { + OrderShippingFulfillment shipping => OrderShippingFulfillmentWidget( + fulfillment: shipping, + order: order, + onTrackingLinkClicked: onTrackingLinkClicked, + index: index, + totalOrders: totalOrders, + ), + OrderPickupFulfillment orderPickup => OrderPickupFulfillmentWidget( + fulfillment: orderPickup, + index: index, + totalOrders: totalOrders, + ), + _ => const SizedBox.shrink() + }; + } +} diff --git a/passkit_ui/lib/src/order/order_widget.dart b/passkit_ui/lib/src/order/order_widget.dart index 05d9311..bdc98a4 100644 --- a/passkit_ui/lib/src/order/order_widget.dart +++ b/passkit_ui/lib/src/order/order_widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:passkit/passkit.dart'; import 'package:passkit_ui/src/extensions/pk_pass_image_extension.dart'; +import 'package:passkit_ui/src/order/fulfillment.dart'; import 'package:passkit_ui/src/order/l10n.dart'; import 'package:passkit_ui/src/order/order_details_model_sheet.dart'; import 'package:passkit_ui/src/widgets/squircle.dart'; @@ -114,7 +115,7 @@ class OrderWidget extends StatelessWidget { ), if (order.order.fulfillments != null) for (final fulfillment in order.order.fulfillments!.indexed) - _FulfillmentSection( + FulfillmentSection( fulfillment: fulfillment.$2, order: order, onTrackingLinkClicked: onTrackingLinkClicked, @@ -238,163 +239,3 @@ class InfoSection extends StatelessWidget { ); } } - -class _FulfillmentSection extends StatelessWidget { - const _FulfillmentSection({ - required this.fulfillment, - required this.order, - required this.onTrackingLinkClicked, - required this.index, - required this.totalOrders, - }); - - final Object fulfillment; - final PkOrder order; - final ValueChanged onTrackingLinkClicked; - final int index; - final int totalOrders; - - @override - Widget build(BuildContext context) { - return switch (fulfillment) { - OrderShippingFulfillment shipping => _OrderShippingFulfillmentWidget( - fulfillment: shipping, - order: order, - onTrackingLinkClicked: onTrackingLinkClicked, - index: index, - totalOrders: totalOrders, - ), - OrderPickupFulfillment orderPickup => _OrderPickupFulfillmentWidget( - fulfillment: orderPickup, - index: index, - totalOrders: totalOrders, - ), - _ => const SizedBox.shrink() - }; - } -} - -class _OrderShippingFulfillmentWidget extends StatelessWidget { - const _OrderShippingFulfillmentWidget({ - required this.fulfillment, - required this.order, - required this.onTrackingLinkClicked, - required this.index, - required this.totalOrders, - }); - - final OrderShippingFulfillment fulfillment; - final PkOrder order; - final ValueChanged onTrackingLinkClicked; - final int index; - final int totalOrders; - - @override - Widget build(BuildContext context) { - final l10n = EnOrderLocalizations(); - final devicePixelRatio = MediaQuery.devicePixelRatioOf(context); - - return CupertinoListSection.insetGrouped( - children: [ - Text( - l10n.shipmentXFromY(index + 1, totalOrders), - textAlign: TextAlign.center, - style: CupertinoTheme.of(context) - .textTheme - .textStyle - .copyWith(color: CupertinoColors.systemGrey), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: LinearProgressIndicator( - valueColor: const AlwaysStoppedAnimation( - CupertinoColors.systemGreen, - ), - minHeight: 20, - borderRadius: BorderRadius.circular(10), - value: fulfillment.status.toProgress(), - ), - ), - CupertinoListTile( - title: Text(l10n.shippingStatus(fulfillment.status)), - subtitle: Text( - fulfillment.deliveredAt != null - ? l10n.deliveredAt(fulfillment.deliveredAt!) - : '', - ), - trailing: IconButton( - onPressed: () { - showBottomSheet( - context: context, - builder: (_) { - return Text(fulfillment.notes!); - }, - ); - }, - icon: const Icon( - CupertinoIcons.info_circle, - color: CupertinoColors.systemBlue, - ), - ), - ), - if (fulfillment.statusDescription != null) - CupertinoListTile( - title: Text(l10n.from(order.order.merchant.displayName)), - subtitle: Text(fulfillment.statusDescription!), - ), - if (fulfillment.carrier != null) - CupertinoListTile.notched( - title: Text(l10n.courier), - subtitle: Text(fulfillment.carrier!), - ), - if (fulfillment.trackingNumber != null) - CupertinoListTile.notched( - title: Text(l10n.trackingId), - subtitle: Text(fulfillment.trackingNumber!), - ), - if (fulfillment.trackingURL != null) - CupertinoListTile.notched( - title: Text( - l10n.trackShipment, - style: const TextStyle(color: CupertinoColors.link), - ), - onTap: () => onTrackingLinkClicked(fulfillment.trackingURL!), - ), - if (fulfillment.lineItems != null) - for (final lineItem in fulfillment.lineItems!) - CupertinoListTile.notched( - leading: lineItem.image != null - ? Image.memory( - order - .loadImage(lineItem.image!) - .forCorrectPixelRatio(devicePixelRatio), - fit: BoxFit.contain, - width: 150, - height: 150, - ) - : null, - title: Text(lineItem.title), - subtitle: - lineItem.subtitle != null ? Text(lineItem.subtitle!) : null, - ), - ], - ); - } -} - -class _OrderPickupFulfillmentWidget extends StatelessWidget { - const _OrderPickupFulfillmentWidget({ - required this.fulfillment, - required this.index, - required this.totalOrders, - }); - - final OrderPickupFulfillment fulfillment; - final int index; - final int totalOrders; - - @override - Widget build(BuildContext context) { - return const SizedBox.shrink(); - } -} diff --git a/passkit_ui/lib/src/order/pickup_fulfillment.dart b/passkit_ui/lib/src/order/pickup_fulfillment.dart new file mode 100644 index 0000000..8f62798 --- /dev/null +++ b/passkit_ui/lib/src/order/pickup_fulfillment.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:passkit/passkit.dart'; + +class OrderPickupFulfillmentWidget extends StatelessWidget { + const OrderPickupFulfillmentWidget({ + super.key, + required this.fulfillment, + required this.index, + required this.totalOrders, + }); + + final OrderPickupFulfillment fulfillment; + final int index; + final int totalOrders; + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} diff --git a/passkit_ui/lib/src/order/shipping_fulfillment.dart b/passkit_ui/lib/src/order/shipping_fulfillment.dart new file mode 100644 index 0000000..06d08ea --- /dev/null +++ b/passkit_ui/lib/src/order/shipping_fulfillment.dart @@ -0,0 +1,227 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:passkit/passkit.dart'; +import 'package:passkit_ui/src/extensions/pk_pass_image_extension.dart'; +import 'package:passkit_ui/src/order/l10n.dart'; +import 'package:passkit_ui/src/widgets/squircle.dart'; + +class OrderShippingFulfillmentWidget extends StatelessWidget { + const OrderShippingFulfillmentWidget({ + super.key, + required this.fulfillment, + required this.order, + required this.onTrackingLinkClicked, + required this.index, + required this.totalOrders, + }); + + final OrderShippingFulfillment fulfillment; + final PkOrder order; + final ValueChanged onTrackingLinkClicked; + final int index; + final int totalOrders; + + @override + Widget build(BuildContext context) { + final l10n = EnOrderLocalizations(); + final devicePixelRatio = MediaQuery.devicePixelRatioOf(context); + + return CupertinoListSection.insetGrouped( + children: [ + _header(), + if (fulfillment.carrier != null) + CupertinoListTile.notched( + title: Text(l10n.courier), + subtitle: Text(fulfillment.carrier!), + ), + if (fulfillment.trackingNumber != null) + CupertinoListTile.notched( + title: Text(l10n.trackingId), + subtitle: Text(fulfillment.trackingNumber!), + ), + if (fulfillment.trackingURL != null) + CupertinoListTile.notched( + title: Text( + l10n.trackShipment, + style: const TextStyle(color: CupertinoColors.link), + ), + onTap: () => onTrackingLinkClicked(fulfillment.trackingURL!), + ), + if (fulfillment.lineItems != null) + for (final lineItem in fulfillment.lineItems!) + CupertinoListTile.notched( + leading: lineItem.image != null + ? Image.memory( + order + .loadImage(lineItem.image!) + .forCorrectPixelRatio(devicePixelRatio), + fit: BoxFit.contain, + width: 150, + height: 150, + ) + : null, + title: Text(lineItem.title), + subtitle: + lineItem.subtitle != null ? Text(lineItem.subtitle!) : null, + ), + ], + ); + } + + _StatusHeader _header() { + return switch (fulfillment.status) { + OrderShippingFulfillmentStatus.open => _StatusHeader( + fulfillment: fulfillment, + index: index, + totalOrders: totalOrders, + order: order, + ), + OrderShippingFulfillmentStatus.processing => _StatusHeader( + fulfillment: fulfillment, + index: index, + totalOrders: totalOrders, + order: order, + ), + OrderShippingFulfillmentStatus.onTheWay => _StatusHeader( + fulfillment: fulfillment, + index: index, + totalOrders: totalOrders, + order: order, + ), + OrderShippingFulfillmentStatus.outForDelivery => _StatusHeader( + fulfillment: fulfillment, + index: index, + totalOrders: totalOrders, + order: order, + ), + OrderShippingFulfillmentStatus.delivered => _StatusHeader( + fulfillment: fulfillment, + index: index, + totalOrders: totalOrders, + order: order, + ), + OrderShippingFulfillmentStatus.shipped => _StatusHeader( + fulfillment: fulfillment, + index: index, + totalOrders: totalOrders, + order: order, + ), + OrderShippingFulfillmentStatus.issue => _StatusHeader( + fulfillment: fulfillment, + index: index, + totalOrders: totalOrders, + order: order, + ), + OrderShippingFulfillmentStatus.cancelled => _StatusHeader( + fulfillment: fulfillment, + index: index, + totalOrders: totalOrders, + order: order, + ), + }; + } +} + +class _StatusHeader extends StatelessWidget { + const _StatusHeader({ + required this.fulfillment, + required this.order, + required this.index, + required this.totalOrders, + }); + + final OrderShippingFulfillment fulfillment; + final PkOrder order; + + final int index; + final int totalOrders; + + @override + Widget build(BuildContext context) { + final l10n = EnOrderLocalizations(); + return Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.shipmentXFromY(index + 1, totalOrders), + style: CupertinoTheme.of(context) + .textTheme + .textStyle + .copyWith(color: CupertinoColors.systemGrey), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + l10n.shippingStatus(fulfillment.status), + style: CupertinoTheme.of(context) + .textTheme + .navLargeTitleTextStyle + .copyWith(fontSize: 20), + ), + CupertinoButton( + onPressed: () { + showBottomSheet( + context: context, + builder: (_) { + return Text(fulfillment.notes!); + }, + ); + }, + child: const Icon( + CupertinoIcons.info_circle, + color: CupertinoColors.systemBlue, + ), + ), + ], + ), + Text( + fulfillment.deliveredAt != null + ? l10n.deliveredAt(fulfillment.deliveredAt!) + : '', + style: CupertinoTheme.of(context).textTheme.textStyle, + ), + const SizedBox(height: 10), + LinearProgressIndicator( + valueColor: const AlwaysStoppedAnimation( + CupertinoColors.systemGreen, + ), + minHeight: 20, + borderRadius: BorderRadius.circular(10), + value: fulfillment.status.toProgress(), + ), + if (fulfillment.statusDescription != null) ...[ + const SizedBox(height: 10), + Squircle( + radius: 20, + child: ColoredBox( + color: CupertinoColors.extraLightBackgroundGray, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + l10n.from(order.order.merchant.displayName), + style: CupertinoTheme.of(context) + .textTheme + .textStyle + .copyWith(color: CupertinoColors.systemGrey), + ), + Text( + fulfillment.statusDescription!, + style: CupertinoTheme.of(context).textTheme.textStyle, + ), + ], + ), + ), + ), + ), + ], + ], + ), + ); + } +} From 4fd648bba5117acd806586cb8c949aefdcd3bfd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 13 Oct 2024 15:57:31 +0200 Subject: [PATCH 17/19] add ability to create an order file, add copyWith, optimize json writing --- passkit/lib/src/order/order_address.dart | 23 +++- passkit/lib/src/order/order_address.g.dart | 28 +++-- passkit/lib/src/order/order_application.dart | 15 ++- .../lib/src/order/order_application.g.dart | 21 +++- passkit/lib/src/order/order_barcode.dart | 16 ++- passkit/lib/src/order/order_barcode.g.dart | 22 ++-- passkit/lib/src/order/order_customer.dart | 18 ++- passkit/lib/src/order/order_data.dart | 62 +++++++++- passkit/lib/src/order/order_data.g.dart | 7 +- passkit/lib/src/order/order_line_item.dart | 34 +++++- passkit/lib/src/order/order_line_item.g.dart | 28 +++-- passkit/lib/src/order/order_location.dart | 14 ++- passkit/lib/src/order/order_location.g.dart | 20 +++- passkit/lib/src/order/order_merchant.dart | 24 ++++ passkit/lib/src/order/order_payment.dart | 66 +++++++++- passkit/lib/src/order/order_payment.g.dart | 60 ++++++---- .../src/order/order_pickup_fulfillment.dart | 35 +++++- .../src/order/order_pickup_fulfillment.g.dart | 40 ++++--- passkit/lib/src/order/order_provider.dart | 18 ++- passkit/lib/src/order/order_return.dart | 32 ++++- passkit/lib/src/order/order_return.g.dart | 39 +++--- passkit/lib/src/order/order_return_info.dart | 19 ++- .../lib/src/order/order_return_info.g.dart | 25 ++-- .../src/order/order_shipping_fulfillment.dart | 40 ++++++- .../order/order_shipping_fulfillment.g.dart | 50 ++++---- passkit/lib/src/order/pk_order.dart | 113 +++++++++++++++++- passkit/lib/src/pk_image_extension.dart | 36 ++++++ passkit/lib/src/pkpass/pkpass.dart | 38 +----- .../lib/src/order/shipping_fulfillment.dart | 31 ++--- 29 files changed, 779 insertions(+), 195 deletions(-) create mode 100644 passkit/lib/src/pk_image_extension.dart diff --git a/passkit/lib/src/order/order_address.dart b/passkit/lib/src/order/order_address.dart index a6e32ae..8fac2f2 100644 --- a/passkit/lib/src/order/order_address.dart +++ b/passkit/lib/src/order/order_address.dart @@ -3,7 +3,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'order_address.g.dart'; /// The physical address for an order. -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderAddress { OrderAddress({ required this.addressLines, @@ -51,4 +51,25 @@ class OrderAddress { /// Additional information associated with the location, such as a district or neighborhood. @JsonKey(name: 'subLocality') final String? subLocality; + + OrderAddress copyWith({ + List? addressLines, + String? administrativeArea, + String? countryCode, + String? locality, + String? postalCode, + String? subAdministrativeArea, + String? subLocality, + }) { + return OrderAddress( + addressLines: addressLines ?? this.addressLines, + administrativeArea: administrativeArea ?? this.administrativeArea, + countryCode: countryCode ?? this.countryCode, + locality: locality ?? this.locality, + postalCode: postalCode ?? this.postalCode, + subAdministrativeArea: + subAdministrativeArea ?? this.subAdministrativeArea, + subLocality: subLocality ?? this.subLocality, + ); + } } diff --git a/passkit/lib/src/order/order_address.g.dart b/passkit/lib/src/order/order_address.g.dart index dfce0f3..6de111d 100644 --- a/passkit/lib/src/order/order_address.g.dart +++ b/passkit/lib/src/order/order_address.g.dart @@ -18,13 +18,21 @@ OrderAddress _$OrderAddressFromJson(Map json) => OrderAddress( subLocality: json['subLocality'] as String?, ); -Map _$OrderAddressToJson(OrderAddress instance) => - { - 'addressLines': instance.addressLines, - 'administrativeArea': instance.administrativeArea, - 'countryCode': instance.countryCode, - 'locality': instance.locality, - 'postalCode': instance.postalCode, - 'subAdministrativeArea': instance.subAdministrativeArea, - 'subLocality': instance.subLocality, - }; +Map _$OrderAddressToJson(OrderAddress instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('addressLines', instance.addressLines); + writeNotNull('administrativeArea', instance.administrativeArea); + writeNotNull('countryCode', instance.countryCode); + writeNotNull('locality', instance.locality); + writeNotNull('postalCode', instance.postalCode); + writeNotNull('subAdministrativeArea', instance.subAdministrativeArea); + writeNotNull('subLocality', instance.subLocality); + return val; +} diff --git a/passkit/lib/src/order/order_application.dart b/passkit/lib/src/order/order_application.dart index 7f30438..f5901ab 100644 --- a/passkit/lib/src/order/order_application.dart +++ b/passkit/lib/src/order/order_application.dart @@ -3,7 +3,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'order_application.g.dart'; /// The details of an app in the App Store. -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderApplication { OrderApplication({ required this.customProductPageIdentifier, @@ -29,4 +29,17 @@ class OrderApplication { /// (Required) The ADAM ID (store identifier) of the application. @JsonKey(name: 'storeIdentifier') final num storeIdentifier; + + OrderApplication copyWith({ + String? customProductPageIdentifier, + String? launchURL, + num? storeIdentifier, + }) { + return OrderApplication( + customProductPageIdentifier: + customProductPageIdentifier ?? this.customProductPageIdentifier, + launchURL: launchURL ?? this.launchURL, + storeIdentifier: storeIdentifier ?? this.storeIdentifier, + ); + } } diff --git a/passkit/lib/src/order/order_application.g.dart b/passkit/lib/src/order/order_application.g.dart index 0d91476..079ea0a 100644 --- a/passkit/lib/src/order/order_application.g.dart +++ b/passkit/lib/src/order/order_application.g.dart @@ -14,9 +14,18 @@ OrderApplication _$OrderApplicationFromJson(Map json) => storeIdentifier: json['storeIdentifier'] as num, ); -Map _$OrderApplicationToJson(OrderApplication instance) => - { - 'customProductPageIdentifier': instance.customProductPageIdentifier, - 'launchURL': instance.launchURL, - 'storeIdentifier': instance.storeIdentifier, - }; +Map _$OrderApplicationToJson(OrderApplication instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull( + 'customProductPageIdentifier', instance.customProductPageIdentifier); + writeNotNull('launchURL', instance.launchURL); + val['storeIdentifier'] = instance.storeIdentifier; + return val; +} diff --git a/passkit/lib/src/order/order_barcode.dart b/passkit/lib/src/order/order_barcode.dart index 3c619f7..95d8d92 100644 --- a/passkit/lib/src/order/order_barcode.dart +++ b/passkit/lib/src/order/order_barcode.dart @@ -3,7 +3,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'order_barcode.g.dart'; /// Information about a pass’s barcode. -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderBarcode { OrderBarcode({ this.altText, @@ -37,6 +37,20 @@ class OrderBarcode { /// iso-8859-1, but you may specify an alternative encoding if required. @JsonKey(name: 'messageEncoding') final String messageEncoding; + + OrderBarcode copyWith({ + String? altText, + OrderBarcodeType? format, + String? message, + String? messageEncoding, + }) { + return OrderBarcode( + altText: altText ?? this.altText, + format: format ?? this.format, + message: message ?? this.message, + messageEncoding: messageEncoding ?? this.messageEncoding, + ); + } } enum OrderBarcodeType { diff --git a/passkit/lib/src/order/order_barcode.g.dart b/passkit/lib/src/order/order_barcode.g.dart index 9a63d21..714adc2 100644 --- a/passkit/lib/src/order/order_barcode.g.dart +++ b/passkit/lib/src/order/order_barcode.g.dart @@ -13,13 +13,21 @@ OrderBarcode _$OrderBarcodeFromJson(Map json) => OrderBarcode( messageEncoding: json['messageEncoding'] as String, ); -Map _$OrderBarcodeToJson(OrderBarcode instance) => - { - 'altText': instance.altText, - 'format': _$OrderBarcodeTypeEnumMap[instance.format]!, - 'message': instance.message, - 'messageEncoding': instance.messageEncoding, - }; +Map _$OrderBarcodeToJson(OrderBarcode instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('altText', instance.altText); + val['format'] = _$OrderBarcodeTypeEnumMap[instance.format]!; + val['message'] = instance.message; + val['messageEncoding'] = instance.messageEncoding; + return val; +} const _$OrderBarcodeTypeEnumMap = { OrderBarcodeType.qr: 'qr', diff --git a/passkit/lib/src/order/order_customer.dart b/passkit/lib/src/order/order_customer.dart index 107785e..20a93e4 100644 --- a/passkit/lib/src/order/order_customer.dart +++ b/passkit/lib/src/order/order_customer.dart @@ -3,7 +3,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'order_customer.g.dart'; /// The details of the order’s customer. -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderCustomer { OrderCustomer({ required this.emailAddress, @@ -39,4 +39,20 @@ class OrderCustomer { /// The customer’s phone number. @JsonKey(name: 'phoneNumber') final String phoneNumber; + + OrderCustomer copyWith({ + String? emailAddress, + String? familyName, + String? givenName, + String? organizationName, + String? phoneNumber, + }) { + return OrderCustomer( + emailAddress: emailAddress ?? this.emailAddress, + familyName: familyName ?? this.familyName, + givenName: givenName ?? this.givenName, + organizationName: organizationName ?? this.organizationName, + phoneNumber: phoneNumber ?? this.phoneNumber, + ); + } } diff --git a/passkit/lib/src/order/order_data.dart b/passkit/lib/src/order/order_data.dart index 2ccbd7d..707063f 100644 --- a/passkit/lib/src/order/order_data.dart +++ b/passkit/lib/src/order/order_data.dart @@ -23,13 +23,13 @@ class OrderData { required this.orderType, required this.orderTypeIdentifier, required this.status, - required this.schemaVersion, + this.schemaVersion = 1, required this.updatedAt, required this.associatedApplications, required this.associatedApplicationIdentifiers, required this.authenticationToken, required this.barcode, - required this.changeNotifications, + this.changeNotifications = OrderChangeNotification.enabled, required this.customer, required this.fulfillments, required this.lineItems, @@ -109,7 +109,7 @@ class OrderData { /// A property that describes whether the device notifies the user about /// relevant changes to the order. The default is enabled. @JsonKey(name: 'changeNotifications') - final OrderChangeNotification? changeNotifications; + final OrderChangeNotification changeNotifications; /// The customer for this order. @JsonKey(name: 'customer') @@ -154,6 +154,62 @@ class OrderData { /// The URL of your web service. This must begin with `HTTPS://`. @JsonKey(name: 'webServiceURL') final Uri? webServiceURL; + + OrderData copyWith({ + DateTime? createdAt, + OrderMerchant? merchant, + String? orderIdentifier, + Uri? orderManagementURL, + String? orderType, + String? orderTypeIdentifier, + OrderStatus? status, + num? schemaVersion, + DateTime? updatedAt, + List? associatedApplications, + List? associatedApplicationIdentifiers, + String? authenticationToken, + OrderBarcode? barcode, + OrderChangeNotification? changeNotifications, + OrderCustomer? customer, + List? fulfillments, + List? lineItems, + String? orderNumber, + OrderProvider? orderProvider, + OrderPayment? payment, + OrderReturnInfo? returnInfo, + List? returns, + String? statusDescription, + Uri? webServiceURL, + }) { + return OrderData( + createdAt: createdAt ?? this.createdAt, + merchant: merchant ?? this.merchant, + orderIdentifier: orderIdentifier ?? this.orderIdentifier, + orderManagementURL: orderManagementURL ?? this.orderManagementURL, + orderType: orderType ?? this.orderType, + orderTypeIdentifier: orderTypeIdentifier ?? this.orderTypeIdentifier, + status: status ?? this.status, + schemaVersion: schemaVersion ?? this.schemaVersion, + updatedAt: updatedAt ?? this.updatedAt, + associatedApplications: + associatedApplications ?? this.associatedApplications, + associatedApplicationIdentifiers: associatedApplicationIdentifiers ?? + this.associatedApplicationIdentifiers, + authenticationToken: authenticationToken ?? this.authenticationToken, + barcode: barcode ?? this.barcode, + changeNotifications: changeNotifications ?? this.changeNotifications, + customer: customer ?? this.customer, + fulfillments: fulfillments ?? this.fulfillments, + lineItems: lineItems ?? this.lineItems, + orderNumber: orderNumber ?? this.orderNumber, + orderProvider: orderProvider ?? this.orderProvider, + payment: payment ?? this.payment, + returnInfo: returnInfo ?? this.returnInfo, + returns: returns ?? this.returns, + statusDescription: statusDescription ?? this.statusDescription, + webServiceURL: webServiceURL ?? this.webServiceURL, + ); + } } List? _fulfillmentsFromJson(List? fulfillments) { diff --git a/passkit/lib/src/order/order_data.g.dart b/passkit/lib/src/order/order_data.g.dart index e100afb..5a23726 100644 --- a/passkit/lib/src/order/order_data.g.dart +++ b/passkit/lib/src/order/order_data.g.dart @@ -15,7 +15,7 @@ OrderData _$OrderDataFromJson(Map json) => OrderData( orderType: json['orderType'] as String, orderTypeIdentifier: json['orderTypeIdentifier'] as String, status: $enumDecode(_$OrderStatusEnumMap, json['status']), - schemaVersion: json['schemaVersion'] as num, + schemaVersion: json['schemaVersion'] as num? ?? 1, updatedAt: DateTime.parse(json['updatedAt'] as String), associatedApplications: (json['associatedApplications'] as List?) ?.map((e) => OrderApplication.fromJson(e as Map)) @@ -29,7 +29,8 @@ OrderData _$OrderDataFromJson(Map json) => OrderData( ? null : OrderBarcode.fromJson(json['barcode'] as Map), changeNotifications: $enumDecodeNullable( - _$OrderChangeNotificationEnumMap, json['changeNotifications']), + _$OrderChangeNotificationEnumMap, json['changeNotifications']) ?? + OrderChangeNotification.enabled, customer: json['customer'] == null ? null : OrderCustomer.fromJson(json['customer'] as Map), @@ -74,7 +75,7 @@ Map _$OrderDataToJson(OrderData instance) => { 'authenticationToken': instance.authenticationToken, 'barcode': instance.barcode, 'changeNotifications': - _$OrderChangeNotificationEnumMap[instance.changeNotifications], + _$OrderChangeNotificationEnumMap[instance.changeNotifications]!, 'customer': instance.customer, 'fulfillments': instance.fulfillments, 'lineItems': instance.lineItems, diff --git a/passkit/lib/src/order/order_line_item.dart b/passkit/lib/src/order/order_line_item.dart index e9f9cb5..db4d74f 100644 --- a/passkit/lib/src/order/order_line_item.dart +++ b/passkit/lib/src/order/order_line_item.dart @@ -2,7 +2,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'order_line_item.g.dart'; -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderLineItem { OrderLineItem({ required this.image, @@ -49,9 +49,29 @@ class OrderLineItem { /// A merchant-specific unique product identifier. @JsonKey(name: 'sku') final String? sku; + + OrderLineItem copyWith({ + String? image, + OrderCurrencyAmount? price, + num? quantity, + String? subtitle, + String? title, + String? gtin, + String? sku, + }) { + return OrderLineItem( + image: image ?? this.image, + price: price ?? this.price, + quantity: quantity ?? this.quantity, + subtitle: subtitle ?? this.subtitle, + title: title ?? this.title, + gtin: gtin ?? this.gtin, + sku: sku ?? this.sku, + ); + } } -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderCurrencyAmount { OrderCurrencyAmount({required this.amount, required this.currency}); @@ -71,4 +91,14 @@ class OrderCurrencyAmount { /// Maximum Length: 3 @JsonKey(name: 'currency') final String currency; + + OrderCurrencyAmount copyWith({ + double? amount, + String? currency, + }) { + return OrderCurrencyAmount( + amount: amount ?? this.amount, + currency: currency ?? this.currency, + ); + } } diff --git a/passkit/lib/src/order/order_line_item.g.dart b/passkit/lib/src/order/order_line_item.g.dart index 33d4267..c1d4bde 100644 --- a/passkit/lib/src/order/order_line_item.g.dart +++ b/passkit/lib/src/order/order_line_item.g.dart @@ -19,16 +19,24 @@ OrderLineItem _$OrderLineItemFromJson(Map json) => sku: json['sku'] as String?, ); -Map _$OrderLineItemToJson(OrderLineItem instance) => - { - 'image': instance.image, - 'price': instance.price, - 'quantity': instance.quantity, - 'subtitle': instance.subtitle, - 'title': instance.title, - 'gtin': instance.gtin, - 'sku': instance.sku, - }; +Map _$OrderLineItemToJson(OrderLineItem instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('image', instance.image); + writeNotNull('price', instance.price); + val['quantity'] = instance.quantity; + writeNotNull('subtitle', instance.subtitle); + val['title'] = instance.title; + writeNotNull('gtin', instance.gtin); + writeNotNull('sku', instance.sku); + return val; +} OrderCurrencyAmount _$OrderCurrencyAmountFromJson(Map json) => OrderCurrencyAmount( diff --git a/passkit/lib/src/order/order_location.dart b/passkit/lib/src/order/order_location.dart index 0b4d86f..23fdc5e 100644 --- a/passkit/lib/src/order/order_location.dart +++ b/passkit/lib/src/order/order_location.dart @@ -3,7 +3,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'order_location.g.dart'; /// The details of a pickup order. -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderLocation { OrderLocation({ this.altitude, @@ -30,4 +30,16 @@ class OrderLocation { /// Minimum Value: -180 /// Maximum Value: 180 final num longitude; + + OrderLocation copyWith({ + num? altitude, + num? latitude, + num? longitude, + }) { + return OrderLocation( + altitude: altitude ?? this.altitude, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } } diff --git a/passkit/lib/src/order/order_location.g.dart b/passkit/lib/src/order/order_location.g.dart index 63e78eb..8d3eb41 100644 --- a/passkit/lib/src/order/order_location.g.dart +++ b/passkit/lib/src/order/order_location.g.dart @@ -13,9 +13,17 @@ OrderLocation _$OrderLocationFromJson(Map json) => longitude: json['longitude'] as num, ); -Map _$OrderLocationToJson(OrderLocation instance) => - { - 'altitude': instance.altitude, - 'latitude': instance.latitude, - 'longitude': instance.longitude, - }; +Map _$OrderLocationToJson(OrderLocation instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('altitude', instance.altitude); + val['latitude'] = instance.latitude; + val['longitude'] = instance.longitude; + return val; +} diff --git a/passkit/lib/src/order/order_merchant.dart b/passkit/lib/src/order/order_merchant.dart index 1b2176e..237d7c7 100644 --- a/passkit/lib/src/order/order_merchant.dart +++ b/passkit/lib/src/order/order_merchant.dart @@ -62,4 +62,28 @@ class OrderMerchant { /// The URL for the merchant’s website or landing page. @JsonKey(name: 'url') final Uri url; + + OrderMerchant copyWith({ + OrderAddress? address, + Uri? businessChatURL, + Uri? contactURL, + String? displayName, + String? emailAddress, + String? logo, + String? merchantIdentifier, + String? phoneNumber, + Uri? url, + }) { + return OrderMerchant( + address: address ?? this.address, + businessChatURL: businessChatURL ?? this.businessChatURL, + contactURL: contactURL ?? this.contactURL, + displayName: displayName ?? this.displayName, + emailAddress: emailAddress ?? this.emailAddress, + logo: logo ?? this.logo, + merchantIdentifier: merchantIdentifier ?? this.merchantIdentifier, + phoneNumber: phoneNumber ?? this.phoneNumber, + url: url ?? this.url, + ); + } } diff --git a/passkit/lib/src/order/order_payment.dart b/passkit/lib/src/order/order_payment.dart index 361003f..4be3ca3 100644 --- a/passkit/lib/src/order/order_payment.dart +++ b/passkit/lib/src/order/order_payment.dart @@ -3,7 +3,7 @@ import 'package:passkit/src/order/order_line_item.dart'; part 'order_payment.g.dart'; -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderPayment { OrderPayment({ required this.total, @@ -58,10 +58,29 @@ class OrderPayment { /// transaction identifiers here to enable additional linking. @JsonKey(name: 'applePayTransactionIdentifiers') final List? applePayTransactionIdentifiers; + + OrderPayment copyWith({ + OrderCurrencyAmount? total, + List? summaryItems, + List? transactions, + List? paymentMethods, + String? status, + List? applePayTransactionIdentifiers, + }) { + return OrderPayment( + total: total ?? this.total, + summaryItems: summaryItems ?? this.summaryItems, + transactions: transactions ?? this.transactions, + paymentMethods: paymentMethods ?? this.paymentMethods, + status: status ?? this.status, + applePayTransactionIdentifiers: + applePayTransactionIdentifiers ?? this.applePayTransactionIdentifiers, + ); + } } /// The details about a payment transaction. -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class PaymentTransaction { PaymentTransaction({ required this.amount, @@ -111,6 +130,27 @@ class PaymentTransaction { /// The filename of a receipt within the bundle that’s associated with the transaction. @JsonKey(name: 'receipt') final String? receipt; + + PaymentTransaction copyWith({ + OrderCurrencyAmount? amount, + DateTime? createdAt, + OrderPaymentMethod? paymentMethod, + PaymentTransactionStatus? status, + String? applePayTransactionIdentifier, + PaymentTransactionType? transactionType, + String? receipt, + }) { + return PaymentTransaction( + amount: amount ?? this.amount, + createdAt: createdAt ?? this.createdAt, + paymentMethod: paymentMethod ?? this.paymentMethod, + status: status ?? this.status, + applePayTransactionIdentifier: + applePayTransactionIdentifier ?? this.applePayTransactionIdentifier, + transactionType: transactionType ?? this.transactionType, + receipt: receipt ?? this.receipt, + ); + } } enum PaymentTransactionStatus { @@ -138,7 +178,7 @@ enum PaymentTransactionType { refund } -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderPaymentMethod { OrderPaymentMethod({required this.displayName}); @@ -152,10 +192,18 @@ class OrderPaymentMethod { /// (Required) The name of the payment method, such as the name of a specific payment pass or card. @JsonKey(name: 'displayName') final String displayName; + + OrderPaymentMethod copyWith({ + String? displayName, + }) { + return OrderPaymentMethod( + displayName: displayName ?? this.displayName, + ); + } } /// A breakdown of the total payment. -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class PaymentSummaryItems { PaymentSummaryItems({required this.label, required this.value}); @@ -173,4 +221,14 @@ class PaymentSummaryItems { /// (Required) The monetary value. @JsonKey(name: 'value') final OrderCurrencyAmount value; + + PaymentSummaryItems copyWith({ + String? label, + OrderCurrencyAmount? value, + }) { + return PaymentSummaryItems( + label: label ?? this.label, + value: value ?? this.value, + ); + } } diff --git a/passkit/lib/src/order/order_payment.g.dart b/passkit/lib/src/order/order_payment.g.dart index 229e63f..faad82b 100644 --- a/passkit/lib/src/order/order_payment.g.dart +++ b/passkit/lib/src/order/order_payment.g.dart @@ -25,15 +25,25 @@ OrderPayment _$OrderPaymentFromJson(Map json) => OrderPayment( .toList(), ); -Map _$OrderPaymentToJson(OrderPayment instance) => - { - 'total': instance.total, - 'summaryItems': instance.summaryItems, - 'transactions': instance.transactions, - 'paymentMethods': instance.paymentMethods, - 'status': instance.status, - 'applePayTransactionIdentifiers': instance.applePayTransactionIdentifiers, - }; +Map _$OrderPaymentToJson(OrderPayment instance) { + final val = { + 'total': instance.total, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('summaryItems', instance.summaryItems); + writeNotNull('transactions', instance.transactions); + writeNotNull('paymentMethods', instance.paymentMethods); + writeNotNull('status', instance.status); + writeNotNull('applePayTransactionIdentifiers', + instance.applePayTransactionIdentifiers); + return val; +} PaymentTransaction _$PaymentTransactionFromJson(Map json) => PaymentTransaction( @@ -50,17 +60,27 @@ PaymentTransaction _$PaymentTransactionFromJson(Map json) => receipt: json['receipt'] as String?, ); -Map _$PaymentTransactionToJson(PaymentTransaction instance) => - { - 'amount': instance.amount, - 'createdAt': instance.createdAt.toIso8601String(), - 'paymentMethod': instance.paymentMethod, - 'status': _$PaymentTransactionStatusEnumMap[instance.status]!, - 'applePayTransactionIdentifier': instance.applePayTransactionIdentifier, - 'transactionType': - _$PaymentTransactionTypeEnumMap[instance.transactionType]!, - 'receipt': instance.receipt, - }; +Map _$PaymentTransactionToJson(PaymentTransaction instance) { + final val = { + 'amount': instance.amount, + 'createdAt': instance.createdAt.toIso8601String(), + 'paymentMethod': instance.paymentMethod, + 'status': _$PaymentTransactionStatusEnumMap[instance.status]!, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull( + 'applePayTransactionIdentifier', instance.applePayTransactionIdentifier); + val['transactionType'] = + _$PaymentTransactionTypeEnumMap[instance.transactionType]!; + writeNotNull('receipt', instance.receipt); + return val; +} const _$PaymentTransactionStatusEnumMap = { PaymentTransactionStatus.pending: 'pending', diff --git a/passkit/lib/src/order/order_pickup_fulfillment.dart b/passkit/lib/src/order/order_pickup_fulfillment.dart index 6b5ebb2..c1551e0 100644 --- a/passkit/lib/src/order/order_pickup_fulfillment.dart +++ b/passkit/lib/src/order/order_pickup_fulfillment.dart @@ -7,7 +7,7 @@ import 'package:passkit/src/order/order_location.dart'; part 'order_pickup_fulfillment.g.dart'; /// The details of a pickup order. -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderPickupFulfillment { OrderPickupFulfillment({ required this.address, @@ -76,6 +76,39 @@ class OrderPickupFulfillment { /// A localized message describing the fulfillment status. final String? statusDescription; + + OrderPickupFulfillment copyWith({ + OrderAddress? address, + OrderBarcode? barcode, + String? displayName, + String? fulfillmentIdentifier, + String? fulfillmentType, + List? lineItems, + OrderLocation? location, + String? notes, + DateTime? pickedUpAt, + DateTime? pickupAt, + String? pickupWindowDuration, + PickupFulfillmentStatus? status, + String? statusDescription, + }) { + return OrderPickupFulfillment( + address: address ?? this.address, + barcode: barcode ?? this.barcode, + displayName: displayName ?? this.displayName, + fulfillmentIdentifier: + fulfillmentIdentifier ?? this.fulfillmentIdentifier, + fulfillmentType: fulfillmentType ?? this.fulfillmentType, + lineItems: lineItems ?? this.lineItems, + location: location ?? this.location, + notes: notes ?? this.notes, + pickedUpAt: pickedUpAt ?? this.pickedUpAt, + pickupAt: pickupAt ?? this.pickupAt, + pickupWindowDuration: pickupWindowDuration ?? this.pickupWindowDuration, + status: status ?? this.status, + statusDescription: statusDescription ?? this.statusDescription, + ); + } } enum PickupFulfillmentStatus { diff --git a/passkit/lib/src/order/order_pickup_fulfillment.g.dart b/passkit/lib/src/order/order_pickup_fulfillment.g.dart index 2f8c29f..5864502 100644 --- a/passkit/lib/src/order/order_pickup_fulfillment.g.dart +++ b/passkit/lib/src/order/order_pickup_fulfillment.g.dart @@ -37,22 +37,30 @@ OrderPickupFulfillment _$OrderPickupFulfillmentFromJson( ); Map _$OrderPickupFulfillmentToJson( - OrderPickupFulfillment instance) => - { - 'address': instance.address, - 'barcode': instance.barcode, - 'displayName': instance.displayName, - 'fulfillmentIdentifier': instance.fulfillmentIdentifier, - 'fulfillmentType': instance.fulfillmentType, - 'lineItems': instance.lineItems, - 'location': instance.location, - 'notes': instance.notes, - 'pickedUpAt': instance.pickedUpAt?.toIso8601String(), - 'pickupAt': instance.pickupAt?.toIso8601String(), - 'pickupWindowDuration': instance.pickupWindowDuration, - 'status': _$PickupFulfillmentStatusEnumMap[instance.status]!, - 'statusDescription': instance.statusDescription, - }; + OrderPickupFulfillment instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('address', instance.address); + writeNotNull('barcode', instance.barcode); + val['displayName'] = instance.displayName; + val['fulfillmentIdentifier'] = instance.fulfillmentIdentifier; + val['fulfillmentType'] = instance.fulfillmentType; + writeNotNull('lineItems', instance.lineItems); + writeNotNull('location', instance.location); + writeNotNull('notes', instance.notes); + writeNotNull('pickedUpAt', instance.pickedUpAt?.toIso8601String()); + writeNotNull('pickupAt', instance.pickupAt?.toIso8601String()); + writeNotNull('pickupWindowDuration', instance.pickupWindowDuration); + val['status'] = _$PickupFulfillmentStatusEnumMap[instance.status]!; + writeNotNull('statusDescription', instance.statusDescription); + return val; +} const _$PickupFulfillmentStatusEnumMap = { PickupFulfillmentStatus.open: 'open', diff --git a/passkit/lib/src/order/order_provider.dart b/passkit/lib/src/order/order_provider.dart index aec443e..4fc3b18 100644 --- a/passkit/lib/src/order/order_provider.dart +++ b/passkit/lib/src/order/order_provider.dart @@ -2,7 +2,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'order_provider.g.dart'; -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderProvider { OrderProvider({ required this.displayName, @@ -37,4 +37,20 @@ class OrderProvider { /// (Required) The URL of the order provder platform. @JsonKey(name: 'url') final Uri url; + + OrderProvider copyWith({ + String? displayName, + String? trackingLogoNameDarkColorScheme, + String? trackingLogoNameLightColorScheme, + Uri? url, + }) { + return OrderProvider( + displayName: displayName ?? this.displayName, + trackingLogoNameDarkColorScheme: trackingLogoNameDarkColorScheme ?? + this.trackingLogoNameDarkColorScheme, + trackingLogoNameLightColorScheme: trackingLogoNameLightColorScheme ?? + this.trackingLogoNameLightColorScheme, + url: url ?? this.url, + ); + } } diff --git a/passkit/lib/src/order/order_return.dart b/passkit/lib/src/order/order_return.dart index 7d51dab..447ac5d 100644 --- a/passkit/lib/src/order/order_return.dart +++ b/passkit/lib/src/order/order_return.dart @@ -4,7 +4,7 @@ import 'package:passkit/src/order/order_line_item.dart'; part 'order_return.g.dart'; /// The details of a return order. -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderReturn { OrderReturn({ required this.returnIdentifier, @@ -84,4 +84,34 @@ class OrderReturn { /// A localized message describing the return status. @JsonKey(name: 'statusDescription') final String? statusDescription; + + OrderReturn copyWith({ + String? returnIdentifier, + String? status, + String? carrier, + DateTime? dropOffBy, + DateTime? initiatedAt, + List? lineItems, + String? notes, + DateTime? returnedAt, + String? returnLabel, + Uri? returnManagementURL, + String? returnNumber, + String? statusDescription, + }) { + return OrderReturn( + returnIdentifier: returnIdentifier ?? this.returnIdentifier, + status: status ?? this.status, + carrier: carrier ?? this.carrier, + dropOffBy: dropOffBy ?? this.dropOffBy, + initiatedAt: initiatedAt ?? this.initiatedAt, + lineItems: lineItems ?? this.lineItems, + notes: notes ?? this.notes, + returnedAt: returnedAt ?? this.returnedAt, + returnLabel: returnLabel ?? this.returnLabel, + returnManagementURL: returnManagementURL ?? this.returnManagementURL, + returnNumber: returnNumber ?? this.returnNumber, + statusDescription: statusDescription ?? this.statusDescription, + ); + } } diff --git a/passkit/lib/src/order/order_return.g.dart b/passkit/lib/src/order/order_return.g.dart index e9248c5..fa72031 100644 --- a/passkit/lib/src/order/order_return.g.dart +++ b/passkit/lib/src/order/order_return.g.dart @@ -31,18 +31,27 @@ OrderReturn _$OrderReturnFromJson(Map json) => OrderReturn( statusDescription: json['statusDescription'] as String?, ); -Map _$OrderReturnToJson(OrderReturn instance) => - { - 'returnIdentifier': instance.returnIdentifier, - 'status': instance.status, - 'carrier': instance.carrier, - 'dropOffBy': instance.dropOffBy?.toIso8601String(), - 'initiatedAt': instance.initiatedAt?.toIso8601String(), - 'lineItems': instance.lineItems, - 'notes': instance.notes, - 'returnedAt': instance.returnedAt?.toIso8601String(), - 'returnLabel': instance.returnLabel, - 'returnManagementURL': instance.returnManagementURL?.toString(), - 'returnNumber': instance.returnNumber, - 'statusDescription': instance.statusDescription, - }; +Map _$OrderReturnToJson(OrderReturn instance) { + final val = { + 'returnIdentifier': instance.returnIdentifier, + 'status': instance.status, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('carrier', instance.carrier); + writeNotNull('dropOffBy', instance.dropOffBy?.toIso8601String()); + writeNotNull('initiatedAt', instance.initiatedAt?.toIso8601String()); + writeNotNull('lineItems', instance.lineItems); + writeNotNull('notes', instance.notes); + writeNotNull('returnedAt', instance.returnedAt?.toIso8601String()); + writeNotNull('returnLabel', instance.returnLabel); + writeNotNull('returnManagementURL', instance.returnManagementURL?.toString()); + writeNotNull('returnNumber', instance.returnNumber); + writeNotNull('statusDescription', instance.statusDescription); + return val; +} diff --git a/passkit/lib/src/order/order_return_info.dart b/passkit/lib/src/order/order_return_info.dart index f2d9b78..c763cdc 100644 --- a/passkit/lib/src/order/order_return_info.dart +++ b/passkit/lib/src/order/order_return_info.dart @@ -2,7 +2,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'order_return_info.g.dart'; -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderReturnInfo { OrderReturnInfo({ required this.returnPolicyURL, @@ -47,4 +47,21 @@ class OrderReturnInfo { /// common return window duration here. @JsonKey(name: 'returnPolicyDescription') final String? returnPolicyDescription; + + OrderReturnInfo copyWith({ + Uri? returnPolicyURL, + bool? displayCountdown, + DateTime? returnDeadline, + Uri? returnManagementURL, + String? returnPolicyDescription, + }) { + return OrderReturnInfo( + returnPolicyURL: returnPolicyURL ?? this.returnPolicyURL, + displayCountdown: displayCountdown ?? this.displayCountdown, + returnDeadline: returnDeadline ?? this.returnDeadline, + returnManagementURL: returnManagementURL ?? this.returnManagementURL, + returnPolicyDescription: + returnPolicyDescription ?? this.returnPolicyDescription, + ); + } } diff --git a/passkit/lib/src/order/order_return_info.g.dart b/passkit/lib/src/order/order_return_info.g.dart index ae1ed3c..59cc48b 100644 --- a/passkit/lib/src/order/order_return_info.g.dart +++ b/passkit/lib/src/order/order_return_info.g.dart @@ -19,11 +19,20 @@ OrderReturnInfo _$OrderReturnInfoFromJson(Map json) => returnPolicyDescription: json['returnPolicyDescription'] as String?, ); -Map _$OrderReturnInfoToJson(OrderReturnInfo instance) => - { - 'returnPolicyURL': instance.returnPolicyURL.toString(), - 'displayCountdown': instance.displayCountdown, - 'returnDeadline': instance.returnDeadline?.toIso8601String(), - 'returnManagementURL': instance.returnManagementURL?.toString(), - 'returnPolicyDescription': instance.returnPolicyDescription, - }; +Map _$OrderReturnInfoToJson(OrderReturnInfo instance) { + final val = { + 'returnPolicyURL': instance.returnPolicyURL.toString(), + 'displayCountdown': instance.displayCountdown, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('returnDeadline', instance.returnDeadline?.toIso8601String()); + writeNotNull('returnManagementURL', instance.returnManagementURL?.toString()); + writeNotNull('returnPolicyDescription', instance.returnPolicyDescription); + return val; +} diff --git a/passkit/lib/src/order/order_shipping_fulfillment.dart b/passkit/lib/src/order/order_shipping_fulfillment.dart index 8c804f8..93c56b6 100644 --- a/passkit/lib/src/order/order_shipping_fulfillment.dart +++ b/passkit/lib/src/order/order_shipping_fulfillment.dart @@ -5,7 +5,7 @@ import 'package:passkit/src/order/order_line_item.dart'; part 'order_shipping_fulfillment.g.dart'; // The details of a shipped order. -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class OrderShippingFulfillment { OrderShippingFulfillment({ required this.fulfillmentIdentifier, @@ -105,6 +105,44 @@ class OrderShippingFulfillment { /// A URL where the customer can track the shipment. @JsonKey(name: 'trackingURL') final Uri? trackingURL; + + OrderShippingFulfillment copyWith({ + String? fulfillmentIdentifier, + String? fulfillmentType, + OrderShippingFulfillmentStatus? status, + String? carrier, + DateTime? deliveredAt, + DateTime? estimatedDeliveryAt, + Duration? estimatedDeliveryWindowDuration, + List? lineItems, + String? notes, + ShippingFulfillmentRecipient? recipient, + DateTime? shippedAt, + OrderShippingFulfillmentType? shippingType, + String? statusDescription, + String? trackingNumber, + Uri? trackingURL, + }) { + return OrderShippingFulfillment( + fulfillmentIdentifier: + fulfillmentIdentifier ?? this.fulfillmentIdentifier, + fulfillmentType: fulfillmentType ?? this.fulfillmentType, + status: status ?? this.status, + carrier: carrier ?? this.carrier, + deliveredAt: deliveredAt ?? this.deliveredAt, + estimatedDeliveryAt: estimatedDeliveryAt ?? this.estimatedDeliveryAt, + estimatedDeliveryWindowDuration: estimatedDeliveryWindowDuration ?? + this.estimatedDeliveryWindowDuration, + lineItems: lineItems ?? this.lineItems, + notes: notes ?? this.notes, + recipient: recipient ?? this.recipient, + shippedAt: shippedAt ?? this.shippedAt, + shippingType: shippingType ?? this.shippingType, + statusDescription: statusDescription ?? this.statusDescription, + trackingNumber: trackingNumber ?? this.trackingNumber, + trackingURL: trackingURL ?? this.trackingURL, + ); + } } enum OrderShippingFulfillmentType { diff --git a/passkit/lib/src/order/order_shipping_fulfillment.g.dart b/passkit/lib/src/order/order_shipping_fulfillment.g.dart index 3b4608f..4a59abc 100644 --- a/passkit/lib/src/order/order_shipping_fulfillment.g.dart +++ b/passkit/lib/src/order/order_shipping_fulfillment.g.dart @@ -48,26 +48,36 @@ OrderShippingFulfillment _$OrderShippingFulfillmentFromJson( ); Map _$OrderShippingFulfillmentToJson( - OrderShippingFulfillment instance) => - { - 'fulfillmentIdentifier': instance.fulfillmentIdentifier, - 'fulfillmentType': instance.fulfillmentType, - 'status': _$OrderShippingFulfillmentStatusEnumMap[instance.status]!, - 'carrier': instance.carrier, - 'deliveredAt': instance.deliveredAt?.toIso8601String(), - 'estimatedDeliveryAt': instance.estimatedDeliveryAt?.toIso8601String(), - 'estimatedDeliveryWindowDuration': - instance.estimatedDeliveryWindowDuration?.inMicroseconds, - 'lineItems': instance.lineItems, - 'notes': instance.notes, - 'recipient': instance.recipient, - 'shippedAt': instance.shippedAt?.toIso8601String(), - 'shippingType': - _$OrderShippingFulfillmentTypeEnumMap[instance.shippingType]!, - 'statusDescription': instance.statusDescription, - 'trackingNumber': instance.trackingNumber, - 'trackingURL': instance.trackingURL?.toString(), - }; + OrderShippingFulfillment instance) { + final val = { + 'fulfillmentIdentifier': instance.fulfillmentIdentifier, + 'fulfillmentType': instance.fulfillmentType, + 'status': _$OrderShippingFulfillmentStatusEnumMap[instance.status]!, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('carrier', instance.carrier); + writeNotNull('deliveredAt', instance.deliveredAt?.toIso8601String()); + writeNotNull( + 'estimatedDeliveryAt', instance.estimatedDeliveryAt?.toIso8601String()); + writeNotNull('estimatedDeliveryWindowDuration', + instance.estimatedDeliveryWindowDuration?.inMicroseconds); + writeNotNull('lineItems', instance.lineItems); + writeNotNull('notes', instance.notes); + writeNotNull('recipient', instance.recipient); + writeNotNull('shippedAt', instance.shippedAt?.toIso8601String()); + val['shippingType'] = + _$OrderShippingFulfillmentTypeEnumMap[instance.shippingType]!; + writeNotNull('statusDescription', instance.statusDescription); + writeNotNull('trackingNumber', instance.trackingNumber); + writeNotNull('trackingURL', instance.trackingURL?.toString()); + return val; +} const _$OrderShippingFulfillmentStatusEnumMap = { OrderShippingFulfillmentStatus.open: 'open', diff --git a/passkit/lib/src/order/pk_order.dart b/passkit/lib/src/order/pk_order.dart index ddc5949..00b2278 100644 --- a/passkit/lib/src/order/pk_order.dart +++ b/passkit/lib/src/order/pk_order.dart @@ -4,8 +4,12 @@ import 'package:archive/archive.dart'; import 'package:passkit/src/archive_extensions.dart'; 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/pk_image_extension.dart'; import 'package:passkit/src/pkpass/exceptions.dart'; +import 'package:passkit/src/strings/strings_writer.dart'; +import 'package:passkit/src/utils.dart'; import 'package:pkcs7/pkcs7.dart'; import 'order_data.dart'; @@ -13,8 +17,8 @@ import 'order_data.dart'; class PkOrder { PkOrder({ required this.order, - required this.manifest, - required this.sourceData, + this.manifest, + this.sourceData, this.languageData, }); @@ -71,7 +75,7 @@ class PkOrder { /// Mapping of files to their respective checksums. Typically not relevant for /// users of this package. - final Map manifest; + final Map? manifest; /// List of available languages. Each value is a language identifier Iterable get availableLanguages => languageData?.keys ?? []; @@ -79,7 +83,7 @@ class PkOrder { /// Translations for this PkPass. /// Consists of a mapping of language identifier to translation key-value /// pairs. - final Map>? languageData; + final Map>? languageData; late Archive _archive; @@ -108,10 +112,109 @@ class PkOrder { } /// The bytes of this PkPass - final Uint8List sourceData; + /// + /// `null` if this object was created with the default constructor. + final Uint8List? sourceData; /// Indicates whether a webservices is available. bool get isWebServiceAvailable => order.webServiceURL != null; + + /// Creates a PkOrder file. If this instance was created via [PkOrder.fromBytes] + /// it overwrites the signature if possible. + /// + /// When written to disk, the file should have an ending of `.order`. + /// + /// [certificatePem] is the certificate to be used to sign the PkPass file. + /// + /// [privateKeyPem] is the private key PEM file. Right now, + /// it's only supported if it's not password protected. + /// + /// Read more about signing [here](https://github.com/ueman/passkit/blob/master/passkit/SIGNING.md). + /// + /// If either [certificatePem] or [privateKeyPem] is null, the resulting PkPass + /// will not be properly signed, but still generated. + /// + /// Setting [overrideWwdrCert] overrides the Apple WWDR certificate, that's + /// shipped with this library. + /// + /// Apple's documentation [here](https://developer.apple.com/documentation/walletorders) + /// explains which fields to set. + /// + /// Remarks: + /// - Image sizes aren't checked, which means it's possible to create orders + /// that look odd and wrong in the Apple Wallet app or in + /// [passkit_ui](https://pub.dev/packages/passkit_ui) + Uint8List? write({ + required String? certificatePem, + required String? privateKeyPem, + X509? overrideWwdrCert, + required List<(String name, PkImage)> images, + }) { + final archive = Archive(); + + final orderContent = utf8JsonEncode(order.toJson()); + final orderFile = ArchiveFile( + 'order.json', + orderContent.length, + orderContent, + ); + archive.addFile(orderFile); + + // TODO(any): Write validation + for (final image in images) { + image.$2.writeToArchive(archive, image.$1.split('.').first); + } + + final translationEntries = languageData?.entries; + if (translationEntries != null && translationEntries.isNotEmpty) { + // TODO(any): Ensure every translation file has the same amount of key value pairs. + + for (final entry in translationEntries) { + final name = '${entry.key}.lproj/pass.strings'; + final localizationFile = + ArchiveFile.string(name, toStringsFile(entry.value)); + archive.addFile(localizationFile); + } + } + + final manifestFile = archive.createManifest(); + + if (certificatePem != null && privateKeyPem != null) { + final signature = writeSignature( + certificatePem, + privateKeyPem, + manifestFile, + order.orderTypeIdentifier, + order.merchant.merchantIdentifier, + true, + overrideWwdrCert, + ); + + final signatureFile = ArchiveFile( + 'signature', + signature.length, + signature, + ); + archive.addFile(signatureFile); + } + + final pkOrder = ZipEncoder().encode(archive); + return pkOrder == null ? null : Uint8List.fromList(pkOrder); + } + + PkOrder copyWith({ + OrderData? order, + Map? manifest, + Map>? languageData, + Uint8List? sourceData, + }) { + return PkOrder( + order: order ?? this.order, + manifest: manifest ?? this.manifest, + languageData: languageData ?? this.languageData, + sourceData: sourceData ?? this.sourceData, + ); + } } extension on Archive { diff --git a/passkit/lib/src/pk_image_extension.dart b/passkit/lib/src/pk_image_extension.dart new file mode 100644 index 0000000..915ac52 --- /dev/null +++ b/passkit/lib/src/pk_image_extension.dart @@ -0,0 +1,36 @@ +import 'package:archive/archive.dart'; +import 'package:passkit/src/pk_image.dart'; + +extension PkImageX on PkImage { + void writeToArchive(Archive archive, String name) { + if (image1 != null) { + archive.addFile(ArchiveFile('$name.png', image1!.lengthInBytes, image1)); + } + if (image2 != null) { + archive + .addFile(ArchiveFile('$name@2x.png', image2!.lengthInBytes, image2)); + } + if (image3 != null) { + archive + .addFile(ArchiveFile('$name@3x.png', image3!.lengthInBytes, image3)); + } + + if (localizedImages != null) { + for (final entry in localizedImages!.entries) { + final lang = entry.key; + for (final image in entry.value.entries) { + final fileName = switch (image.key) { + 1 => '$lang.lproj/$name.png', + 2 => '$lang.lproj/$name@2x.png', + 3 => '$lang.lproj/$name@3x.png', + _ => throw Exception('This case should never happen'), + }; + + archive.addFile( + ArchiveFile(fileName, image.value.lengthInBytes, image.value), + ); + } + } + } + } +} diff --git a/passkit/lib/src/pkpass/pkpass.dart b/passkit/lib/src/pkpass/pkpass.dart index 090db2b..dfbbcda 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/pk_image_extension.dart'; import 'package:passkit/src/pkpass/exceptions.dart'; import 'package:passkit/src/pkpass/pass_data.dart'; import 'package:passkit/src/pkpass/pass_type.dart'; @@ -259,7 +260,8 @@ class PkPass { /// /// 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) + /// that look odd and wrong in the Apple Wallet app or in + /// [passkit_ui](https://pub.dev/packages/passkit_ui) Uint8List? write({ required String? certificatePem, required String? privateKeyPem, @@ -443,37 +445,3 @@ extension on Archive { return null; } } - -extension on PkImage { - void writeToArchive(Archive archive, String name) { - if (image1 != null) { - archive.addFile(ArchiveFile('$name.png', image1!.lengthInBytes, image1)); - } - if (image2 != null) { - archive - .addFile(ArchiveFile('$name@2x.png', image2!.lengthInBytes, image2)); - } - if (image3 != null) { - archive - .addFile(ArchiveFile('$name@3x.png', image3!.lengthInBytes, image3)); - } - - if (localizedImages != null) { - for (final entry in localizedImages!.entries) { - final lang = entry.key; - for (final image in entry.value.entries) { - final fileName = switch (image.key) { - 1 => '$lang.lproj/$name.png', - 2 => '$lang.lproj/$name@2x.png', - 3 => '$lang.lproj/$name@3x.png', - _ => throw Exception('This case should never happen'), - }; - - archive.addFile( - ArchiveFile(fileName, image.value.lengthInBytes, image.value), - ); - } - } - } - } -} diff --git a/passkit_ui/lib/src/order/shipping_fulfillment.dart b/passkit_ui/lib/src/order/shipping_fulfillment.dart index 06d08ea..5b4ce8d 100644 --- a/passkit_ui/lib/src/order/shipping_fulfillment.dart +++ b/passkit_ui/lib/src/order/shipping_fulfillment.dart @@ -161,25 +161,26 @@ class _StatusHeader extends StatelessWidget { .navLargeTitleTextStyle .copyWith(fontSize: 20), ), - CupertinoButton( - onPressed: () { - showBottomSheet( - context: context, - builder: (_) { - return Text(fulfillment.notes!); - }, - ); - }, - child: const Icon( - CupertinoIcons.info_circle, - color: CupertinoColors.systemBlue, + if (fulfillment.notes != null) + CupertinoButton( + onPressed: () { + showBottomSheet( + context: context, + builder: (_) { + return Text(fulfillment.notes!); + }, + ); + }, + child: const Icon( + CupertinoIcons.info_circle, + color: CupertinoColors.systemBlue, + ), ), - ), ], ), Text( - fulfillment.deliveredAt != null - ? l10n.deliveredAt(fulfillment.deliveredAt!) + fulfillment.estimatedDeliveryAt != null + ? l10n.estimatedDeliveryAt(fulfillment.estimatedDeliveryAt!) : '', style: CupertinoTheme.of(context).textTheme.textStyle, ), From 088e9aec61d629cd0df0826284f5fb53d410da96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 13 Oct 2024 16:11:58 +0200 Subject: [PATCH 18/19] changelog, docs --- passkit/CHANGELOG.md | 3 ++- passkit/lib/src/order/pk_order.dart | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/passkit/CHANGELOG.md b/passkit/CHANGELOG.md index 20211e1..4982da2 100644 --- a/passkit/CHANGELOG.md +++ b/passkit/CHANGELOG.md @@ -2,7 +2,8 @@ - No longer mark `PkPass.write()` as experimental - Add webservice support for orders -- Add image support for orders +- Add support for readong images of orders +- Add support for creating order files ## 0.0.10 diff --git a/passkit/lib/src/order/pk_order.dart b/passkit/lib/src/order/pk_order.dart index 00b2278..2c6b97c 100644 --- a/passkit/lib/src/order/pk_order.dart +++ b/passkit/lib/src/order/pk_order.dart @@ -140,6 +140,14 @@ class PkOrder { /// Apple's documentation [here](https://developer.apple.com/documentation/walletorders) /// explains which fields to set. /// + /// ## Images + /// + /// Images in order files work quite differently from passes, since they have + /// no predefined name. + /// You have to add the name, for example `something.png`, to the various properties, + /// and then pass the the image with the name for the [images] argument here. + /// Make sure the names always match, otherwise the order file might not work! + /// /// Remarks: /// - Image sizes aren't checked, which means it's possible to create orders /// that look odd and wrong in the Apple Wallet app or in @@ -148,7 +156,7 @@ class PkOrder { required String? certificatePem, required String? privateKeyPem, X509? overrideWwdrCert, - required List<(String name, PkImage)> images, + required List<(String name, PkImage image)> images, }) { final archive = Archive(); From c9aab94d53710022c1a0a249e4e8ac2a74c880e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Sun, 13 Oct 2024 17:06:34 +0200 Subject: [PATCH 19/19] don't export order UI from passkit_ui just yet --- app/lib/import_order/import_order_page.dart | 3 ++- passkit_ui/lib/passkit_ui.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/lib/import_order/import_order_page.dart b/app/lib/import_order/import_order_page.dart index e35a801..81b35e0 100644 --- a/app/lib/import_order/import_order_page.dart +++ b/app/lib/import_order/import_order_page.dart @@ -5,7 +5,8 @@ import 'package:content_resolver/content_resolver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:passkit/passkit.dart'; -import 'package:passkit_ui/passkit_ui.dart'; +// ignore: implementation_imports +import 'package:passkit_ui/src/order/order_widget.dart'; import 'package:url_launcher/url_launcher.dart'; class PkOrderImportSource { diff --git a/passkit_ui/lib/passkit_ui.dart b/passkit_ui/lib/passkit_ui.dart index b30a7d3..fc6837b 100644 --- a/passkit_ui/lib/passkit_ui.dart +++ b/passkit_ui/lib/passkit_ui.dart @@ -1,5 +1,5 @@ export 'src/export_pass_image.dart'; export 'src/extensions/extensions.dart'; -export 'src/order/order_widget.dart'; +// export 'src/order/order_widget.dart'; # Not ready yet export 'src/pk_pass_widget.dart'; export 'src/widgets/widgets.dart';