From 72a1eb47177a9fe995d97c8c47fa036ff3e619e2 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sun, 10 Dec 2023 12:37:28 -0500 Subject: [PATCH 01/18] BREAKING: Moved UI related code outside of Upgrader and into UpgradeAlert and UpgradeCard. Also, renamed the private methods to make them public. --- CHANGELOG.md | 5 + example/lib/main-cupertino.dart | 2 +- example/lib/main-driver.dart | 9 +- example/lib/main_subclass.dart | 7 - lib/src/appcast.dart | 2 +- lib/src/upgrade_alert.dart | 310 ++++++++++++- lib/src/upgrade_base.dart | 43 -- lib/src/upgrade_card.dart | 167 +++++-- ...grader_device.dart => upgrade_device.dart} | 0 lib/src/upgrader.dart | 361 ++-------------- lib/upgrader.dart | 6 +- pubspec.yaml | 10 +- test/appcast_test.dart | 1 - test/device_test.dart | 1 - test/upgrader_test.dart | 408 +++++++++--------- 15 files changed, 690 insertions(+), 642 deletions(-) delete mode 100644 lib/src/upgrade_base.dart rename lib/src/{upgrader_device.dart => upgrade_device.dart} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f2f38d..7e9b3981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 9.0.0-alpha.1 + +- BREAKING: Moved UI related code outside of Upgrader and into UpgradeAlert and UpgradeCard. Also, +renamed the private methods to make them public. + ## 8.4.0 - [356] Fixed centering issue with Cupertino style UpgradeAlert dialog. diff --git a/example/lib/main-cupertino.dart b/example/lib/main-cupertino.dart index f905c5a1..0b00b67b 100644 --- a/example/lib/main-cupertino.dart +++ b/example/lib/main-cupertino.dart @@ -35,8 +35,8 @@ class MyApp extends StatelessWidget { upgrader: Upgrader( appcastConfig: cfg, debugLogging: true, - dialogStyle: UpgradeDialogStyle.cupertino, ), + dialogStyle: UpgradeDialogStyle.cupertino, child: Center(child: Text('Checking...')), )), ); diff --git a/example/lib/main-driver.dart b/example/lib/main-driver.dart index 7ad1c0a4..a01bc446 100644 --- a/example/lib/main-driver.dart +++ b/example/lib/main-driver.dart @@ -43,9 +43,7 @@ class _MyAppState extends State { ElevatedButton( onPressed: () async { await Upgrader.clearSavedSettings(); - _upgrader = Upgrader( - dialogStyle: UpgradeDialogStyle.cupertino, - debugLogging: true); + _upgrader = Upgrader(debugLogging: true); setState(() => _testState = 2); }, child: Text('Dialog Alert - Cupertino'), @@ -65,7 +63,10 @@ class _MyAppState extends State { break; case 2: content = UpgradeAlert( - key: Key('ua_2'), upgrader: _upgrader, child: scaffold); + key: Key('ua_2'), + upgrader: _upgrader, + dialogStyle: UpgradeDialogStyle.cupertino, + child: scaffold); break; default: } diff --git a/example/lib/main_subclass.dart b/example/lib/main_subclass.dart index d3688376..e4de502f 100644 --- a/example/lib/main_subclass.dart +++ b/example/lib/main_subclass.dart @@ -39,11 +39,4 @@ class MyApp extends StatelessWidget { /// This class extends / subclasses Upgrader. class MyUpgrader extends Upgrader { MyUpgrader() : super(debugLogging: true); - - /// This method overrides super class method. - @override - void popNavigator(BuildContext context) { - print('this method overrides popNavigator'); - super.popNavigator(context); - } } diff --git a/lib/src/appcast.dart b/lib/src/appcast.dart index 3df79e2b..ed61a9e5 100644 --- a/lib/src/appcast.dart +++ b/lib/src/appcast.dart @@ -11,7 +11,7 @@ import 'package:version/version.dart'; import 'package:xml/xml.dart'; import 'upgrade_os.dart'; -import 'upgrader_device.dart'; +import 'upgrade_device.dart'; /// The [Appcast] class is used to download an Appcast, based on the Sparkle /// framework by Andy Matuschak. diff --git a/lib/src/upgrade_alert.dart b/lib/src/upgrade_alert.dart index 886fe2fd..78ce017b 100644 --- a/lib/src/upgrade_alert.dart +++ b/lib/src/upgrade_alert.dart @@ -2,49 +2,323 @@ * Copyright (c) 2021-2023 Larry Aasen. All rights reserved. */ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:upgrader/upgrader.dart'; -/// A widget to display the upgrade dialog. -class UpgradeAlert extends UpgradeBase { - /// The [child] contained by the widget. - final Widget? child; +import 'upgrade_messages.dart'; +import 'upgrader.dart'; +/// There are two different dialog styles: Cupertino and Material +enum UpgradeDialogStyle { cupertino, material } + +/// A widget to display the upgrade dialog. +class UpgradeAlert extends StatefulWidget { /// Creates a new [UpgradeAlert]. - UpgradeAlert({Key? key, Upgrader? upgrader, this.child, this.navigatorKey}) - : super(upgrader ?? Upgrader.sharedInstance, key: key); + UpgradeAlert({ + super.key, + Upgrader? upgrader, + this.canDismissDialog = false, + this.dialogStyle = UpgradeDialogStyle.material, + this.onIgnore, + this.onLater, + this.onUpdate, + this.shouldPopScope, + this.showIgnore = true, + this.showLater = true, + this.showReleaseNotes = true, + this.cupertinoButtonTextStyle, + this.navigatorKey, + this.child, + }) : upgrader = upgrader ?? Upgrader.sharedInstance; + + /// The upgraders used to configure the upgrade dialog. + final Upgrader upgrader; + + /// Can alert dialog be dismissed on tap outside of the alert dialog. Not used by [UpgradeCard]. (default: false) + final bool canDismissDialog; + + /// The upgrade dialog style. Used only on UpgradeAlert. (default: material) + final UpgradeDialogStyle dialogStyle; + + /// Called when the ignore button is tapped or otherwise activated. + /// Return false when the default behavior should not execute. + final BoolCallback? onIgnore; + + /// Called when the later button is tapped or otherwise activated. + final BoolCallback? onLater; + + /// Called when the update button is tapped or otherwise activated. + /// Return false when the default behavior should not execute. + final BoolCallback? onUpdate; + + /// Called when the user taps outside of the dialog and [canDismissDialog] + /// is false. Also called when the back button is pressed. Return true for + /// the screen to be popped. + final BoolCallback? shouldPopScope; + + /// Hide or show Ignore button on dialog (default: true) + final bool showIgnore; + + /// Hide or show Later button on dialog (default: true) + final bool showLater; + + /// Hide or show release notes (default: true) + final bool showReleaseNotes; + + /// The text style for the cupertino dialog buttons. Used only for + /// [UpgradeDialogStyle.cupertino]. Optional. + final TextStyle? cupertinoButtonTextStyle; /// For use by the Router architecture as part of the RouterDelegate. final GlobalKey? navigatorKey; + /// The [child] contained by the widget. + final Widget? child; + + @override + UpgradeAlertBaseState createState() => UpgradeAlertBaseState(); +} + +class UpgradeAlertBaseState extends State { + bool _displayed = false; + + @override + void initState() { + super.initState(); + widget.upgrader.initialize(); + } + /// Describes the part of the user interface represented by this widget. @override - Widget build(BuildContext context, UpgradeBaseState state) { - if (upgrader.debugLogging) { + Widget build(BuildContext context) { + if (widget.upgrader.debugLogging) { print('upgrader: build UpgradeAlert'); } return StreamBuilder( - initialData: state.widget.upgrader.evaluationReady, - stream: state.widget.upgrader.evaluationStream, + initialData: widget.upgrader.evaluationReady, + stream: widget.upgrader.evaluationStream, builder: (BuildContext context, AsyncSnapshot snapshot) { if ((snapshot.connectionState == ConnectionState.waiting || snapshot.connectionState == ConnectionState.active) && snapshot.data != null && snapshot.data!) { - if (upgrader.debugLogging) { + if (widget.upgrader.debugLogging) { print("upgrader: need to evaluate version"); } - final checkContext = - navigatorKey != null && navigatorKey!.currentContext != null - ? navigatorKey!.currentContext! - : context; - upgrader.checkVersion(context: checkContext); + final checkContext = widget.navigatorKey != null && + widget.navigatorKey!.currentContext != null + ? widget.navigatorKey!.currentContext! + : context; + checkVersion(context: checkContext); } - return child ?? const SizedBox.shrink(); + return widget.child ?? const SizedBox.shrink(); + }, + ); + } + + /// Will show the alert dialog when it should be dispalyed. + /// Only called by [UpgradeAlert] and not used by [UpgradeCard]. + void checkVersion({required BuildContext context}) { + if (!_displayed) { + final shouldDisplay = widget.upgrader.shouldDisplayUpgrade(); + if (widget.upgrader.debugLogging) { + print('upgrader: shouldDisplayReleaseNotes: shouldDisplayReleaseNotes'); + } + if (shouldDisplay) { + _displayed = true; + final appMessages = widget.upgrader.determineMessages(context); + + Future.delayed(const Duration(milliseconds: 0), () { + showTheDialog( + context: context, + title: appMessages.message(UpgraderMessage.title), + message: widget.upgrader.body(appMessages), + releaseNotes: + shouldDisplayReleaseNotes ? widget.upgrader.releaseNotes : null, + canDismissDialog: widget.canDismissDialog, + messages: appMessages, + ); + }); + } + } + } + + void onUserIgnored(BuildContext context, bool shouldPop) { + if (widget.upgrader.debugLogging) { + print('upgrader: button tapped: ignore'); + } + + // If this callback has been provided, call it. + final doProcess = widget.onIgnore?.call() ?? true; + + if (doProcess) { + widget.upgrader.saveIgnored(); + } + + if (shouldPop) { + popNavigator(context); + } + } + + void onUserLater(BuildContext context, bool shouldPop) { + if (widget.upgrader.debugLogging) { + print('upgrader: button tapped: later'); + } + + // If this callback has been provided, call it. + widget.onLater?.call(); + + if (shouldPop) { + popNavigator(context); + } + } + + void onUserUpdated(BuildContext context, bool shouldPop) { + if (widget.upgrader.debugLogging) { + print('upgrader: button tapped: update now'); + } + + // If this callback has been provided, call it. + final doProcess = widget.onUpdate?.call() ?? true; + + if (doProcess) { + widget.upgrader.sendUserToAppStore(); + } + + if (shouldPop) { + popNavigator(context); + } + } + + void popNavigator(BuildContext context) { + Navigator.of(context).pop(); + _displayed = false; + } + + bool get shouldDisplayReleaseNotes => + widget.showReleaseNotes && + (widget.upgrader.releaseNotes?.isNotEmpty ?? false); + + void showTheDialog({ + required BuildContext context, + required String? title, + required String message, + required String? releaseNotes, + required bool canDismissDialog, + required UpgraderMessages messages, + }) { + if (widget.upgrader.debugLogging) { + print('upgrader: showTheDialog title: $title'); + print('upgrader: showTheDialog message: $message'); + print('upgrader: showTheDialog releaseNotes: $releaseNotes'); + } + + // Save the date/time as the last time alerted. + widget.upgrader.saveLastAlerted(); + + showDialog( + barrierDismissible: canDismissDialog, + context: context, + builder: (BuildContext context) { + return WillPopScope( + onWillPop: () async => onWillPop(), + child: alertDialog( + title ?? '', + message, + releaseNotes, + context, + widget.dialogStyle == UpgradeDialogStyle.cupertino, + messages, + )); }, ); } + + /// Called when the user taps outside of the dialog and [canDismissDialog] + /// is false. Also called when the back button is pressed. Return true for + /// the screen to be popped. Defaults to false. + bool onWillPop() { + if (widget.upgrader.debugLogging) { + print('upgrader: onWillPop called'); + } + if (widget.shouldPopScope != null) { + final should = widget.shouldPopScope!(); + if (widget.upgrader.debugLogging) { + print('upgrader: shouldPopScope=$should'); + } + return should; + } + + return false; + } + + Widget alertDialog(String title, String message, String? releaseNotes, + BuildContext context, bool cupertino, UpgraderMessages messages) { + // If installed version is below minimum app version, or is a critical update, + // disable ignore and later buttons. + final isBlocked = widget.upgrader.blocked(); + final showIgnore = isBlocked ? false : widget.showIgnore; + final showLater = isBlocked ? false : widget.showLater; + + Widget? notes; + if (releaseNotes != null) { + notes = Padding( + padding: const EdgeInsets.only(top: 15.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: cupertino + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + Text(messages.message(UpgraderMessage.releaseNotes) ?? '', + style: const TextStyle(fontWeight: FontWeight.bold)), + Text(releaseNotes), + ], + )); + } + final textTitle = Text(title, key: const Key('upgrader.dialog.title')); + final content = Container( + constraints: const BoxConstraints(maxHeight: 400), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: + cupertino ? CrossAxisAlignment.center : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(message), + Padding( + padding: const EdgeInsets.only(top: 15.0), + child: Text(messages.message(UpgraderMessage.prompt) ?? '')), + if (notes != null) notes, + ], + ))); + final actions = [ + if (showIgnore) + button(cupertino, messages.message(UpgraderMessage.buttonTitleIgnore), + context, () => onUserIgnored(context, true)), + if (showLater) + button(cupertino, messages.message(UpgraderMessage.buttonTitleLater), + context, () => onUserLater(context, true)), + button(cupertino, messages.message(UpgraderMessage.buttonTitleUpdate), + context, () => onUserUpdated(context, !widget.upgrader.blocked())), + ]; + + return cupertino + ? CupertinoAlertDialog( + title: textTitle, content: content, actions: actions) + : AlertDialog(title: textTitle, content: content, actions: actions); + } + + Widget button(bool cupertino, String? text, BuildContext context, + VoidCallback? onPressed) { + return cupertino + ? CupertinoDialogAction( + textStyle: widget.cupertinoButtonTextStyle, + onPressed: onPressed, + child: Text(text ?? '')) + : TextButton(onPressed: onPressed, child: Text(text ?? '')); + } } diff --git a/lib/src/upgrade_base.dart b/lib/src/upgrade_base.dart deleted file mode 100644 index ad108f12..00000000 --- a/lib/src/upgrade_base.dart +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2018-2023 Larry Aasen. All rights reserved. - */ - -import 'package:flutter/material.dart'; -import 'package:upgrader/upgrader.dart'; - -class UpgradeBase extends StatefulWidget { - /// The upgraders used to configure the upgrade dialog. - final Upgrader upgrader; - - const UpgradeBase(this.upgrader, {Key? key}) : super(key: key); - - Widget build(BuildContext context, UpgradeBaseState state) { - return Container(); - } - - @override - UpgradeBaseState createState() => UpgradeBaseState(); -} - -class UpgradeBaseState extends State { - @override - void initState() { - super.initState(); - initialize(); - } - - @override - Widget build(BuildContext context) => widget.build(context, this); - - Future initialize() => widget.upgrader.initialize(); - - void forceUpdateState() => setState(() {}); - - @override - void dispose() { - if (widget.upgrader.debugLogging) { - print('upgrader: dispose'); - } - super.dispose(); - } -} diff --git a/lib/src/upgrade_card.dart b/lib/src/upgrade_card.dart index d6f17b46..eb6c36a4 100644 --- a/lib/src/upgrade_card.dart +++ b/lib/src/upgrade_card.dart @@ -3,10 +3,31 @@ */ import 'package:flutter/material.dart'; -import 'package:upgrader/upgrader.dart'; + +import 'alert_style_widget.dart'; +import 'upgrade_messages.dart'; +import 'upgrader.dart'; /// A widget to display the upgrade card. -class UpgradeCard extends UpgradeBase { +class UpgradeCard extends StatefulWidget { + /// Creates a new [UpgradeCard]. + UpgradeCard({ + super.key, + Upgrader? upgrader, + this.margin = const EdgeInsets.all(4.0), + this.maxLines = 15, + this.onIgnore, + this.onLater, + this.onUpdate, + this.overflow = TextOverflow.ellipsis, + this.showIgnore = true, + this.showLater = true, + this.showReleaseNotes = true, + }) : upgrader = upgrader ?? Upgrader.sharedInstance; + + /// The upgraders used to configure the upgrade dialog. + final Upgrader upgrader; + /// The empty space that surrounds the card. /// /// The default margin is 4.0 logical pixels on all sides: @@ -16,38 +37,60 @@ class UpgradeCard extends UpgradeBase { /// An optional maximum number of lines for the text to span, wrapping if necessary. final int? maxLines; + /// Called when the ignore button is tapped or otherwise activated. + /// Return false when the default behavior should not execute. + final BoolCallback? onIgnore; + + /// Called when the later button is tapped or otherwise activated. + final VoidCallback? onLater; + + /// Called when the update button is tapped or otherwise activated. + /// Return false when the default behavior should not execute. + final BoolCallback? onUpdate; + /// How visual overflow should be handled. final TextOverflow? overflow; - /// Creates a new [UpgradeCard]. - UpgradeCard({ - super.key, - Upgrader? upgrader, - this.margin = const EdgeInsets.all(4.0), - this.maxLines = 15, - this.overflow = TextOverflow.ellipsis, - }) : super(upgrader ?? Upgrader.sharedInstance); + /// Hide or show Ignore button on dialog (default: true) + final bool showIgnore; + + /// Hide or show Later button on dialog (default: true) + final bool showLater; + + /// Hide or show release notes (default: true) + final bool showReleaseNotes; + + @override + UpgradeCardBaseState createState() => UpgradeCardBaseState(); +} + +class UpgradeCardBaseState extends State { + @override + void initState() { + super.initState(); + widget.upgrader.initialize(); + } /// Describes the part of the user interface represented by this widget. @override - Widget build(BuildContext context, UpgradeBaseState state) { - if (upgrader.debugLogging) { + Widget build(BuildContext context) { + if (widget.upgrader.debugLogging) { print('upgrader: build UpgradeCard'); } return StreamBuilder( - initialData: state.widget.upgrader.evaluationReady, - stream: state.widget.upgrader.evaluationStream, + initialData: widget.upgrader.evaluationReady, + stream: widget.upgrader.evaluationStream, builder: (BuildContext context, AsyncSnapshot snapshot) { if ((snapshot.connectionState == ConnectionState.waiting || snapshot.connectionState == ConnectionState.active) && snapshot.data != null && snapshot.data!) { - if (upgrader.shouldDisplayUpgrade()) { - return buildUpgradeCard(context, state); + if (widget.upgrader.shouldDisplayUpgrade()) { + return buildUpgradeCard(context); } else { - if (upgrader.debugLogging) { + if (widget.upgrader.debugLogging) { print('upgrader: UpgradeCard will not display'); } } @@ -57,13 +100,17 @@ class UpgradeCard extends UpgradeBase { } /// Build the UpgradeCard Widget. - Widget buildUpgradeCard(BuildContext context, UpgradeBaseState state) { - final appMessages = upgrader.determineMessages(context); + Widget buildUpgradeCard(BuildContext context) { + final appMessages = widget.upgrader.determineMessages(context); final title = appMessages.message(UpgraderMessage.title); - final message = upgrader.body(appMessages); - final releaseNotes = upgrader.releaseNotes; - final shouldDisplayReleaseNotes = upgrader.shouldDisplayReleaseNotes(); - if (upgrader.debugLogging) { + final message = widget.upgrader.body(appMessages); + final releaseNotes = widget.upgrader.releaseNotes; + + final isBlocked = widget.upgrader.blocked(); + final showIgnore = isBlocked ? false : widget.showIgnore; + final showLater = isBlocked ? false : widget.showLater; + + if (widget.upgrader.debugLogging) { print('upgrader: UpgradeCard: will display'); print('upgrader: UpgradeCard: showDialog title: $title'); print('upgrader: UpgradeCard: showDialog message: $message'); @@ -85,8 +132,8 @@ class UpgradeCard extends UpgradeBase { style: const TextStyle(fontWeight: FontWeight.bold)), Text( releaseNotes, - maxLines: maxLines, - overflow: overflow, + maxLines: widget.maxLines, + overflow: widget.overflow, ), ], )); @@ -94,7 +141,7 @@ class UpgradeCard extends UpgradeBase { return Card( color: Colors.white, - margin: margin, + margin: widget.margin, child: AlertStyleWidget( title: Text(title ?? ''), content: Column( @@ -111,29 +158,29 @@ class UpgradeCard extends UpgradeBase { ], ), actions: [ - if (upgrader.showIgnore) + if (showIgnore) TextButton( child: Text(appMessages .message(UpgraderMessage.buttonTitleIgnore) ?? ''), onPressed: () { // Save the date/time as the last time alerted. - upgrader.saveLastAlerted(); + widget.upgrader.saveLastAlerted(); - upgrader.onUserIgnored(context, false); - state.forceUpdateState(); + onUserIgnored(); + forceUpdateState(); }), - if (upgrader.showLater) + if (showLater) TextButton( child: Text( appMessages.message(UpgraderMessage.buttonTitleLater) ?? ''), onPressed: () { // Save the date/time as the last time alerted. - upgrader.saveLastAlerted(); + widget.upgrader.saveLastAlerted(); - upgrader.onUserLater(context, false); - state.forceUpdateState(); + onUserLater(); + forceUpdateState(); }), TextButton( child: Text( @@ -141,11 +188,57 @@ class UpgradeCard extends UpgradeBase { ''), onPressed: () { // Save the date/time as the last time alerted. - upgrader.saveLastAlerted(); + widget.upgrader.saveLastAlerted(); - upgrader.onUserUpdated(context, false); - state.forceUpdateState(); + onUserUpdated(); }), ])); } + + void forceUpdateState() => setState(() {}); + + bool get shouldDisplayReleaseNotes => + widget.showReleaseNotes && + (widget.upgrader.releaseNotes?.isNotEmpty ?? false); + + void onUserIgnored() { + if (widget.upgrader.debugLogging) { + print('upgrader: button tapped: ignore'); + } + + // If this callback has been provided, call it. + final doProcess = widget.onIgnore?.call() ?? true; + + if (doProcess) { + widget.upgrader.saveIgnored(); + } + + forceUpdateState(); + } + + void onUserLater() { + if (widget.upgrader.debugLogging) { + print('upgrader: button tapped: later'); + } + + // If this callback has been provided, call it. + widget.onLater?.call(); + + forceUpdateState(); + } + + void onUserUpdated() { + if (widget.upgrader.debugLogging) { + print('upgrader: button tapped: update now'); + } + + // If this callback has been provided, call it. + final doProcess = widget.onUpdate?.call() ?? true; + + if (doProcess) { + widget.upgrader.sendUserToAppStore(); + } + + forceUpdateState(); + } } diff --git a/lib/src/upgrader_device.dart b/lib/src/upgrade_device.dart similarity index 100% rename from lib/src/upgrader_device.dart rename to lib/src/upgrade_device.dart diff --git a/lib/src/upgrader.dart b/lib/src/upgrader.dart index 356642e0..6eaf869b 100644 --- a/lib/src/upgrader.dart +++ b/lib/src/upgrader.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:ui'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; @@ -36,9 +35,6 @@ typedef WillDisplayUpgradeCallback = void Function( /// The type of data in the stream. typedef UpgraderEvaluateNeed = bool; -/// There are two different dialog styles: Cupertino and Material -enum UpgradeDialogStyle { cupertino, material } - /// A class to define the configuration for the appcast. The configuration /// contains two parts: a URL to the appcast, and a list of supported OS /// names, such as "android", "fuchsia", "ios", "linux" "macos", "web", "windows". @@ -57,6 +53,25 @@ Upgrader _sharedInstance = Upgrader(); /// A class to configure the upgrade dialog. class Upgrader with WidgetsBindingObserver { + Upgrader({ + this.appcastConfig, + this.appcast, + this.messages, + this.debugDisplayAlways = false, + this.debugDisplayOnce = false, + this.debugLogging = false, + this.durationUntilAlertAgain = const Duration(days: 3), + this.willDisplayUpgrade, + http.Client? client, + this.countryCode, + this.languageCode, + this.minAppVersion, + UpgraderOS? upgraderOS, + }) : client = client ?? http.Client(), + upgraderOS = upgraderOS ?? UpgraderOS() { + if (debugLogging) print("upgrader: instantiated."); + } + /// Provide an Appcast that can be replaced for mock testing. final Appcast? appcast; @@ -64,9 +79,6 @@ class Upgrader with WidgetsBindingObserver { /// When an appcast is configured for iOS, the iTunes lookup is not used. final AppcastConfiguration? appcastConfig; - /// Can alert dialog be dismissed on tap outside of the alert dialog. Not used by [UpgradeCard]. (default: false) - bool canDismissDialog; - /// Provide an HTTP Client that can be replaced for mock testing. final http.Client client; @@ -85,9 +97,6 @@ class Upgrader with WidgetsBindingObserver { /// Enable print statements for debugging. bool debugLogging; - /// The upgrade dialog style. Used only on UpgradeAlert. (default: material) - UpgradeDialogStyle dialogStyle; - /// Duration until alerting user again final Duration durationUntilAlertAgain; @@ -98,36 +107,6 @@ class Upgrader with WidgetsBindingObserver { /// will be forced to update to the current version. Optional. String? minAppVersion; - /// Called when the ignore button is tapped or otherwise activated. - /// Return false when the default behavior should not execute. - BoolCallback? onIgnore; - - /// Called when the later button is tapped or otherwise activated. - /// Return false when the default behavior should not execute. - BoolCallback? onLater; - - /// Called when the update button is tapped or otherwise activated. - /// Return false when the default behavior should not execute. - BoolCallback? onUpdate; - - /// Called when the user taps outside of the dialog and [canDismissDialog] - /// is false. Also called when the back button is pressed. Return true for - /// the screen to be popped. Not used by [UpgradeCard]. - BoolCallback? shouldPopScope; - - /// Hide or show Ignore button on dialog (default: true) - bool showIgnore; - - /// Hide or show Later button on dialog (default: true) - bool showLater; - - /// Hide or show release notes (default: true) - bool showReleaseNotes; - - /// The text style for the cupertino dialog buttons. Used only for - /// [UpgradeDialogStyle.cupertino]. Optional. - TextStyle? cupertinoButtonTextStyle; - /// Called when [Upgrader] determines that an upgrade may or may not be /// displayed. The [value] parameter will be true when it should be displayed, /// and false when it should not be displayed. One good use for this callback @@ -137,7 +116,6 @@ class Upgrader with WidgetsBindingObserver { /// Provides information on which OS this code is running on. final UpgraderOS upgraderOS; - bool _displayed = false; bool _initCalled = false; PackageInfo? _packageInfo; @@ -164,53 +142,28 @@ class Upgrader with WidgetsBindingObserver { bool get evaluationReady => _evaluationReady; bool _evaluationReady = false; - final notInitializedExceptionMessage = - 'initialize() not called. Must be called first.'; - - Upgrader({ - this.appcastConfig, - this.appcast, - this.messages, - this.debugDisplayAlways = false, - this.debugDisplayOnce = false, - this.debugLogging = false, - this.durationUntilAlertAgain = const Duration(days: 3), - this.onIgnore, - this.onLater, - this.onUpdate, - this.shouldPopScope, - this.willDisplayUpgrade, - http.Client? client, - this.showIgnore = true, - this.showLater = true, - this.showReleaseNotes = true, - this.canDismissDialog = false, - this.countryCode, - this.languageCode, - this.minAppVersion, - this.dialogStyle = UpgradeDialogStyle.material, - this.cupertinoButtonTextStyle, - UpgraderOS? upgraderOS, - }) : client = client ?? http.Client(), - upgraderOS = upgraderOS ?? UpgraderOS() { - if (debugLogging) print("upgrader: instantiated."); - } - /// A shared instance of [Upgrader]. static Upgrader get sharedInstance => _sharedInstance; + static const notInitializedExceptionMessage = + 'upgrader: initialize() not called. Must be called first.'; + + String? currentAppStoreListingURL() => _appStoreListingURL; + + String? currentAppStoreVersion() => _appStoreVersion; + + String? currentInstalledVersion() => _installedVersion; + + String? get releaseNotes => _releaseNotes; + void installPackageInfo({PackageInfo? packageInfo}) { _packageInfo = packageInfo; _initCalled = false; } - void installAppStoreVersion(String version) { - _appStoreVersion = version; - } + void installAppStoreVersion(String version) => _appStoreVersion = version; - void installAppStoreListingURL(String url) { - _appStoreListingURL = url; - } + void installAppStoreListingURL(String url) => _appStoreListingURL = url; /// Initialize [Upgrader] by getting saved preferences, getting platform package info, and getting /// released version info. @@ -230,7 +183,7 @@ class Upgrader with WidgetsBindingObserver { } _initCalled = true; - await _getSavedPrefs(); + await getSavedPrefs(); if (debugLogging) { print('upgrader: default operatingSystem: ' @@ -258,7 +211,7 @@ class Upgrader with WidgetsBindingObserver { _installedVersion = _packageInfo!.version; - await _updateVersionInfo(); + await updateVersionInfo(); // Add an observer of application events. WidgetsBinding.instance.addObserver(this); @@ -287,7 +240,7 @@ class Upgrader with WidgetsBindingObserver { // When app has resumed from background. if (state == AppLifecycleState.resumed) { - await _updateVersionInfo(); + await updateVersionInfo(); /// Trigger the stream to indicate another evaluation should be performed. /// The value will always be true. @@ -295,9 +248,9 @@ class Upgrader with WidgetsBindingObserver { } } - Future _updateVersionInfo() async { + Future updateVersionInfo() async { // If there is an appcast for this platform - if (_isAppcastThisPlatform()) { + if (isAppcastThisPlatform()) { if (debugLogging) { print('upgrader: appcast is available for this platform'); } @@ -357,7 +310,7 @@ class Upgrader with WidgetsBindingObserver { // Get Android version from Google Play Store, or // get iOS version from iTunes Store. if (upgraderOS.isAndroid) { - await _getAndroidStoreVersion(country: country, language: language); + await getAndroidStoreVersion(country: country, language: language); } else if (upgraderOS.isIOS) { final iTunes = ITunesSearchAPI(); iTunes.debugLogging = debugLogging; @@ -384,7 +337,7 @@ class Upgrader with WidgetsBindingObserver { } /// Android info is fetched by parsing the html of the app store page. - Future _getAndroidStoreVersion( + Future getAndroidStoreVersion( {String? country, String? language}) async { final id = _packageInfo!.packageName; final playStore = PlayStoreSearchAPI(client: client); @@ -408,7 +361,7 @@ class Upgrader with WidgetsBindingObserver { return true; } - bool _isAppcastThisPlatform() { + bool isAppcastThisPlatform() { if (appcastConfig == null || appcastConfig!.url == null || appcastConfig!.url!.isEmpty) { @@ -426,26 +379,18 @@ class Upgrader with WidgetsBindingObserver { return supported; } - bool _verifyInit() { + bool verifyInit() { if (!_initCalled) { - throw (notInitializedExceptionMessage); + throw ('upgrader: initialize() not called. Must be called first.'); } return true; } String appName() { - _verifyInit(); + verifyInit(); return _packageInfo?.appName ?? ''; } - String? currentAppStoreListingURL() => _appStoreListingURL; - - String? currentAppStoreVersion() => _appStoreVersion; - - String? currentInstalledVersion() => _installedVersion; - - String? get releaseNotes => _releaseNotes; - String body(UpgraderMessages messages) { var msg = messages.message(UpgraderMessage.body)!; msg = msg.replaceAll('{{appName}}', appName()); @@ -456,33 +401,6 @@ class Upgrader with WidgetsBindingObserver { return msg; } - /// Will show the alert dialog when it should be dispalyed. - /// Only called by [UpgradeAlert] and not used by [UpgradeCard]. - void checkVersion({required BuildContext context}) { - if (!_displayed) { - final shouldDisplay = shouldDisplayUpgrade(); - if (debugLogging) { - print( - 'upgrader: shouldDisplayReleaseNotes: ${shouldDisplayReleaseNotes()}'); - } - if (shouldDisplay) { - _displayed = true; - final appMessages = determineMessages(context); - - Future.delayed(const Duration(milliseconds: 0), () { - _showDialog( - context: context, - title: appMessages.message(UpgraderMessage.title), - message: body(appMessages), - releaseNotes: shouldDisplayReleaseNotes() ? _releaseNotes : null, - canDismissDialog: canDismissDialog, - messages: appMessages, - ); - }); - } - } - } - /// Determine which [UpgraderMessages] object to use. It will be either the one passed /// to [Upgrader], or one based on the app locale. UpgraderMessages determineMessages(BuildContext context) { @@ -531,12 +449,6 @@ class Upgrader with WidgetsBindingObserver { print('upgrader: hasAlerted: $_hasAlerted'); } - // If installed version is below minimum app version, or is a critical update, - // disable ignore and later buttons. - if (isBlocked) { - showIgnore = false; - showLater = false; - } bool rv = true; if (debugDisplayAlways || (debugDisplayOnce && !_hasAlerted)) { rv = true; @@ -629,10 +541,6 @@ class Upgrader with WidgetsBindingObserver { return isAvailable; } - bool shouldDisplayReleaseNotes() { - return showReleaseNotes && (_releaseNotes?.isNotEmpty ?? false); - } - /// Determine the current country code, either from the context, or /// from the system-reported default locale of the device. The default /// is `US`. @@ -665,178 +573,6 @@ class Upgrader with WidgetsBindingObserver { return code; } - void _showDialog({ - required BuildContext context, - required String? title, - required String message, - required String? releaseNotes, - required bool canDismissDialog, - required UpgraderMessages messages, - }) { - if (debugLogging) { - print('upgrader: showDialog title: $title'); - print('upgrader: showDialog message: $message'); - print('upgrader: showDialog releaseNotes: $releaseNotes'); - } - - // Save the date/time as the last time alerted. - saveLastAlerted(); - - showDialog( - barrierDismissible: canDismissDialog, - context: context, - builder: (BuildContext context) { - return WillPopScope( - onWillPop: () async => _shouldPopScope(), - child: _alertDialog( - title ?? '', - message, - releaseNotes, - context, - dialogStyle == UpgradeDialogStyle.cupertino, - messages, - )); - }, - ); - } - - /// Called when the user taps outside of the dialog and [canDismissDialog] - /// is false. Also called when the back button is pressed. Return true for - /// the screen to be popped. Defaults to false. - bool _shouldPopScope() { - if (debugLogging) { - print('upgrader: onWillPop called'); - } - if (shouldPopScope != null) { - final should = shouldPopScope!(); - if (debugLogging) { - print('upgrader: shouldPopScope=$should'); - } - return should; - } - - return false; - } - - Widget _alertDialog(String title, String message, String? releaseNotes, - BuildContext context, bool cupertino, UpgraderMessages messages) { - Widget? notes; - if (releaseNotes != null) { - notes = Padding( - padding: const EdgeInsets.only(top: 15.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: cupertino - ? CrossAxisAlignment.center - : CrossAxisAlignment.start, - children: [ - Text(messages.message(UpgraderMessage.releaseNotes) ?? '', - style: const TextStyle(fontWeight: FontWeight.bold)), - Text(releaseNotes), - ], - )); - } - final textTitle = Text(title, key: const Key('upgrader.dialog.title')); - final content = Container( - constraints: const BoxConstraints(maxHeight: 400), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: - cupertino ? CrossAxisAlignment.center : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(message), - Padding( - padding: const EdgeInsets.only(top: 15.0), - child: Text(messages.message(UpgraderMessage.prompt) ?? '')), - if (notes != null) notes, - ], - ))); - final actions = [ - if (showIgnore) - _button(cupertino, messages.message(UpgraderMessage.buttonTitleIgnore), - context, () => onUserIgnored(context, true)), - if (showLater) - _button(cupertino, messages.message(UpgraderMessage.buttonTitleLater), - context, () => onUserLater(context, true)), - _button(cupertino, messages.message(UpgraderMessage.buttonTitleUpdate), - context, () => onUserUpdated(context, !blocked())), - ]; - - return cupertino - ? CupertinoAlertDialog( - title: textTitle, content: content, actions: actions) - : AlertDialog(title: textTitle, content: content, actions: actions); - } - - Widget _button(bool cupertino, String? text, BuildContext context, - VoidCallback? onPressed) { - return cupertino - ? CupertinoDialogAction( - textStyle: cupertinoButtonTextStyle, - onPressed: onPressed, - child: Text(text ?? '')) - : TextButton(onPressed: onPressed, child: Text(text ?? '')); - } - - void onUserIgnored(BuildContext context, bool shouldPop) { - if (debugLogging) { - print('upgrader: button tapped: ignore'); - } - - // If this callback has been provided, call it. - var doProcess = true; - if (onIgnore != null) { - doProcess = onIgnore!(); - } - - if (doProcess) { - _saveIgnored(); - } - - if (shouldPop) { - popNavigator(context); - } - } - - void onUserLater(BuildContext context, bool shouldPop) { - if (debugLogging) { - print('upgrader: button tapped: later'); - } - - // If this callback has been provided, call it. - var doProcess = true; - if (onLater != null) { - doProcess = onLater!(); - } - - if (doProcess) {} - - if (shouldPop) { - popNavigator(context); - } - } - - void onUserUpdated(BuildContext context, bool shouldPop) { - if (debugLogging) { - print('upgrader: button tapped: update now'); - } - - // If this callback has been provided, call it. - var doProcess = true; - if (onUpdate != null) { - doProcess = onUpdate!(); - } - - if (doProcess) { - _sendUserToAppStore(); - } - - if (shouldPop) { - popNavigator(context); - } - } - static Future clearSavedSettings() async { var prefs = await SharedPreferences.getInstance(); await prefs.remove('userIgnoredVersion'); @@ -846,12 +582,7 @@ class Upgrader with WidgetsBindingObserver { return; } - void popNavigator(BuildContext context) { - Navigator.of(context).pop(); - _displayed = false; - } - - Future _saveIgnored() async { + Future saveIgnored() async { var prefs = await SharedPreferences.getInstance(); _userIgnoredVersion = _appStoreVersion; @@ -871,7 +602,7 @@ class Upgrader with WidgetsBindingObserver { return true; } - Future _getSavedPrefs() async { + Future getSavedPrefs() async { var prefs = await SharedPreferences.getInstance(); final lastTimeAlerted = prefs.getString('lastTimeAlerted'); if (lastTimeAlerted != null) { @@ -885,7 +616,7 @@ class Upgrader with WidgetsBindingObserver { return true; } - void _sendUserToAppStore() async { + void sendUserToAppStore() async { if (_appStoreListingURL == null || _appStoreListingURL!.isEmpty) { if (debugLogging) { print('upgrader: empty _appStoreListingURL'); diff --git a/lib/upgrader.dart b/lib/upgrader.dart index 282bd6bb..811cea11 100644 --- a/lib/upgrader.dart +++ b/lib/upgrader.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 Larry Aasen. All rights reserved. + * Copyright (c) 2018-2023 Larry Aasen. All rights reserved. */ library upgrader; @@ -9,8 +9,8 @@ export 'src/appcast.dart'; export 'src/itunes_search_api.dart'; export 'src/play_store_search_api.dart'; export 'src/upgrade_alert.dart'; -export 'src/upgrade_base.dart'; export 'src/upgrade_card.dart'; -export 'src/upgrade_os.dart'; +export 'src/upgrade_device.dart'; export 'src/upgrade_messages.dart'; +export 'src/upgrade_os.dart'; export 'src/upgrader.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index b2c00972..cd2d1fc3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: upgrader description: Flutter package for prompting users to upgrade when there is a newer version of the app in the store. -version: 8.4.0 +version: 9.0.0-alpha.1 homepage: https://github.com/larryaasen/upgrader environment: - sdk: '>=2.18.6 <4.0.0' - flutter: ">=3.10.0" + sdk: '>=3.1.0 <4.0.0' + flutter: ">=3.13.1" dependencies: flutter: @@ -43,7 +43,7 @@ dev_dependencies: sdk: flutter # From Dart Team: Mock library for Dart inspired by Mockito. - mockito: ^5.4.0 - flutter_lints: ^2.0.1 + mockito: ^5.4.3 + flutter_lints: ^2.0.3 flutter: diff --git a/test/appcast_test.dart b/test/appcast_test.dart index aba07156..dd1dfe0f 100644 --- a/test/appcast_test.dart +++ b/test/appcast_test.dart @@ -8,7 +8,6 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; -import 'package:upgrader/src/upgrader_device.dart'; import 'package:upgrader/upgrader.dart'; void main() { diff --git a/test/device_test.dart b/test/device_test.dart index 656ddb8c..a917d059 100644 --- a/test/device_test.dart +++ b/test/device_test.dart @@ -2,7 +2,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:upgrader/src/upgrader_device.dart'; import 'package:upgrader/upgrader.dart'; void main() { diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 73597702..91729549 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -8,7 +8,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/src/client.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:upgrader/src/upgrader_device.dart'; import 'package:upgrader/upgrader.dart'; import 'appcast_test.dart'; @@ -61,7 +60,7 @@ void main() { try { expect(upgrader.appName(), 'Upgrader'); } catch (e) { - expect(e, upgrader.notInitializedExceptionMessage); + expect(e, Upgrader.notInitializedExceptionMessage); } upgrader.installPackageInfo(packageInfo: packageInfo); @@ -94,7 +93,7 @@ void main() { try { expect(upgrader.appName(), 'Upgrader'); } catch (e) { - expect(e, upgrader.notInitializedExceptionMessage); + expect(e, Upgrader.notInitializedExceptionMessage); } upgrader.installPackageInfo( @@ -164,7 +163,7 @@ void main() { 'https://itunes.apple.com/us/app/google-maps-transit-food/id585027354?mt=8&uo=4'); }, skip: false); - testWidgets('test UpgradeWidget', (WidgetTester tester) async { + testWidgets('test UpgradeAlert', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader( upgraderOS: MockUpgraderOS(ios: true), @@ -180,21 +179,6 @@ void main() { upgrader.initialize().then((value) {}); await tester.pumpAndSettle(); - var called = false; - var notCalled = true; - upgrader.onUpdate = () { - called = true; - return true; - }; - upgrader.onIgnore = () { - notCalled = false; - return true; - }; - upgrader.onLater = () { - notCalled = false; - return true; - }; - expect(upgrader.isUpdateAvailable(), true); expect(upgrader.isTooSoon(), false); @@ -214,21 +198,34 @@ void main() { expect(upgrader.messages!.buttonTitleUpdate, 'ccc'); expect(upgrader.messages!.releaseNotes, 'ddd'); - // await tester.runAsync(() async { - final GlobalKey globalKey = GlobalKey(); - final myWidget = _MyWidget(key: globalKey, upgrader: upgrader); - await tester.pumpWidget(myWidget); + var called = false; + var notCalled = true; + + final upgradeAlert = wrapper( + UpgradeAlert( + upgrader: upgrader, + onUpdate: () { + called = true; + return true; + }, + onIgnore: () { + notCalled = false; + return true; + }, + onLater: () { + notCalled = false; + return true; + }, + child: const Center(child: Text('Upgrading')), + ), + ); + await tester.pumpWidget(upgradeAlert); expect(find.text('Upgrader test'), findsOneWidget); expect(find.text('Upgrading'), findsOneWidget); // Pump the UI so the upgrader can display its dialog await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); expect(upgrader.isTooSoon(), true); @@ -254,7 +251,7 @@ void main() { // }); }, skip: false); - testWidgets('test UpgradeWidget Cupertino', (WidgetTester tester) async { + testWidgets('test UpgradeAlert Cupertino', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); const cupertinoButtonTextStyle = TextStyle( @@ -265,7 +262,6 @@ void main() { upgraderOS: MockUpgraderOS(ios: true), client: client, debugLogging: true, - cupertinoButtonTextStyle: cupertinoButtonTextStyle, ); upgrader.installPackageInfo( @@ -277,21 +273,6 @@ void main() { upgrader.initialize().then((value) {}); await tester.pumpAndSettle(); - var called = false; - var notCalled = true; - upgrader.onUpdate = () { - called = true; - return true; - }; - upgrader.onIgnore = () { - notCalled = false; - return true; - }; - upgrader.onLater = () { - notCalled = false; - return true; - }; - expect(upgrader.isUpdateAvailable(), true); expect(upgrader.isTooSoon(), false); @@ -308,16 +289,37 @@ void main() { expect(upgrader.messages!.buttonTitleIgnore, 'aaa'); expect(upgrader.messages!.buttonTitleLater, 'bbb'); expect(upgrader.messages!.buttonTitleUpdate, 'ccc'); - upgrader.dialogStyle = UpgradeDialogStyle.cupertino; - await tester.pumpWidget(_MyWidget(upgrader: upgrader)); + var called = false; + var notCalled = true; + + final upgradeAlert = wrapper( + UpgradeAlert( + upgrader: upgrader, + cupertinoButtonTextStyle: cupertinoButtonTextStyle, + dialogStyle: UpgradeDialogStyle.cupertino, + onUpdate: () { + called = true; + return true; + }, + onIgnore: () { + notCalled = false; + return true; + }, + onLater: () { + notCalled = false; + return true; + }, + child: const Center(child: Text('Upgrading')), + ), + ); + await tester.pumpWidget(upgradeAlert); expect(find.text('Upgrader test'), findsOneWidget); expect(find.text('Upgrading'), findsOneWidget); // Pump the UI so the upgrader can display its dialog await tester.pumpAndSettle(); - await tester.pumpAndSettle(); expect(upgrader.isTooSoon(), true); @@ -346,7 +348,7 @@ void main() { expect(notCalled, true); }, skip: false); - testWidgets('test UpgradeWidget ignore', (WidgetTester tester) async { + testWidgets('test UpgradeAlert ignore', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader(upgraderOS: MockUpgraderOS(ios: true), client: client); @@ -360,32 +362,36 @@ void main() { upgrader.initialize().then((value) {}); await tester.pumpAndSettle(); - var called = false; - var notCalled = true; - upgrader.onIgnore = () { - called = true; - return true; - }; - upgrader.onUpdate = () { - notCalled = false; - return true; - }; - upgrader.onLater = () { - notCalled = false; - return true; - }; - expect(upgrader.isTooSoon(), false); expect(upgrader.messages, isNull); upgrader.messages = UpgraderMessages(); expect(upgrader.messages, isNotNull); - await tester.pumpWidget(_MyWidget(upgrader: upgrader)); + var called = false; + var notCalled = true; + final upgradeAlert = wrapper( + UpgradeAlert( + upgrader: upgrader, + onUpdate: () { + notCalled = false; + return true; + }, + onIgnore: () { + called = true; + return true; + }, + onLater: () { + notCalled = false; + return true; + }, + child: const Center(child: Text('Upgrading')), + ), + ); + await tester.pumpWidget(upgradeAlert); // Pump the UI so the upgrader can display its dialog await tester.pumpAndSettle(); - await tester.pumpAndSettle(); await tester.tap(find.text(upgrader.messages!.buttonTitleIgnore)); await tester.pumpAndSettle(); @@ -394,7 +400,7 @@ void main() { expect(notCalled, true); }, skip: false); - testWidgets('test UpgradeWidget later', (WidgetTester tester) async { + testWidgets('test UpgradeAlert later', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader(upgraderOS: MockUpgraderOS(ios: true), client: client); @@ -408,32 +414,36 @@ void main() { upgrader.initialize().then((value) {}); await tester.pumpAndSettle(); - var called = false; - var notCalled = true; - upgrader.onLater = () { - called = true; - return true; - }; - upgrader.onIgnore = () { - notCalled = false; - return true; - }; - upgrader.onUpdate = () { - notCalled = false; - return true; - }; - expect(upgrader.isTooSoon(), false); expect(upgrader.messages, isNull); upgrader.messages = UpgraderMessages(); expect(upgrader.messages, isNotNull); - await tester.pumpWidget(_MyWidget(upgrader: upgrader)); + var called = false; + var notCalled = true; + final upgradeAlert = wrapper( + UpgradeAlert( + upgrader: upgrader, + onUpdate: () { + notCalled = false; + return true; + }, + onIgnore: () { + notCalled = false; + return true; + }, + onLater: () { + called = true; + return true; + }, + child: const Center(child: Text('Upgrading')), + ), + ); + await tester.pumpWidget(upgradeAlert); // Pump the UI so the upgrader can display its dialog await tester.pumpAndSettle(); - await tester.pumpAndSettle(); await tester.tap(find.text(upgrader.messages!.buttonTitleLater)); await tester.pumpAndSettle(); @@ -442,7 +452,7 @@ void main() { expect(notCalled, true); }, skip: false); - testWidgets('test UpgradeWidget pop scope', (WidgetTester tester) async { + testWidgets('test UpgradeAlert pop scope', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader(upgraderOS: MockUpgraderOS(ios: true), client: client); @@ -456,32 +466,36 @@ void main() { upgrader.initialize().then((value) {}); await tester.pumpAndSettle(); - var called = false; - upgrader.shouldPopScope = () { - called = true; - return true; - }; - expect(upgrader.isTooSoon(), false); expect(upgrader.messages, isNull); upgrader.messages = UpgraderMessages(); expect(upgrader.messages, isNotNull); - await tester.pumpWidget(_MyWidget(upgrader: upgrader)); + var called = false; + final upgradeAlert = wrapper( + UpgradeAlert( + upgrader: upgrader, + shouldPopScope: () { + called = true; + return true; + }, + child: const Center(child: Text('Upgrading')), + ), + ); + await tester.pumpWidget(upgradeAlert); // Pump the UI so the upgrader can display its dialog await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - // TODO: this test does not pop scope because there is no way to do that. + // Note: his test does not pop scope because there is no way to do that. // await tester.pageBack(); // await tester.pumpAndSettle(); // expect(find.text(upgrader.messages.buttonTitleLater), findsNothing); expect(called, false); }, skip: false); - testWidgets('test UpgradeWidget Card upgrade', (WidgetTester tester) async { + testWidgets('test UpgradeCard upgrade', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader( upgraderOS: MockUpgraderOS(ios: true), @@ -497,24 +511,27 @@ void main() { upgrader.initialize().then((value) {}); await tester.pumpAndSettle(); - var called = false; - var notCalled = true; - upgrader.onUpdate = () { - called = true; - return true; - }; - upgrader.onLater = () { - notCalled = false; - return true; - }; - upgrader.onIgnore = () { - notCalled = false; - return true; - }; - expect(upgrader.isTooSoon(), false); - await tester.pumpWidget(_MyWidgetCard(upgrader: upgrader)); + var called = false; + var notCalled = true; + final upgradeCard = wrapper( + UpgradeCard( + upgrader: upgrader, + onUpdate: () { + called = true; + return true; + }, + onIgnore: () { + notCalled = false; + return true; + }, + onLater: () { + notCalled = false; + }, + ), + ); + await tester.pumpWidget(upgradeCard); // Pump the UI so the upgrade card is displayed await tester.pumpAndSettle(); @@ -533,7 +550,7 @@ void main() { expect(find.text(upgrader.messages!.buttonTitleUpdate), findsNothing); }, skip: false); - testWidgets('test UpgradeWidget Card ignore', (WidgetTester tester) async { + testWidgets('test UpgradeCard ignore', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader( upgraderOS: MockUpgraderOS(ios: true), @@ -549,24 +566,27 @@ void main() { upgrader.initialize().then((value) {}); await tester.pumpAndSettle(); - var called = false; - var notCalled = true; - upgrader.onIgnore = () { - called = true; - return true; - }; - upgrader.onLater = () { - notCalled = false; - return true; - }; - upgrader.onUpdate = () { - notCalled = false; - return true; - }; - expect(upgrader.isTooSoon(), false); - await tester.pumpWidget(_MyWidgetCard(upgrader: upgrader)); + var called = false; + var notCalled = true; + final upgradeCard = wrapper( + UpgradeCard( + upgrader: upgrader, + onUpdate: () { + notCalled = false; + return true; + }, + onIgnore: () { + called = true; + return true; + }, + onLater: () { + notCalled = false; + }, + ), + ); + await tester.pumpWidget(upgradeCard); // Pump the UI so the upgrade card is displayed await tester.pumpAndSettle(); @@ -583,7 +603,7 @@ void main() { expect(find.text(upgrader.messages!.buttonTitleIgnore), findsNothing); }, skip: false); - testWidgets('test UpgradeWidget Card later', (WidgetTester tester) async { + testWidgets('test UpgradeCard later', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader( upgraderOS: MockUpgraderOS(ios: true), @@ -599,24 +619,27 @@ void main() { upgrader.initialize().then((value) {}); await tester.pumpAndSettle(); - var called = false; - var notCalled = true; - upgrader.onLater = () { - called = true; - return true; - }; - upgrader.onIgnore = () { - notCalled = false; - return true; - }; - upgrader.onUpdate = () { - notCalled = false; - return true; - }; - expect(upgrader.isTooSoon(), false); - await tester.pumpWidget(_MyWidgetCard(upgrader: upgrader)); + var called = false; + var notCalled = true; + final upgradeCard = wrapper( + UpgradeCard( + upgrader: upgrader, + onUpdate: () { + notCalled = false; + return true; + }, + onIgnore: () { + notCalled = false; + return true; + }, + onLater: () { + called = true; + }, + ), + ); + await tester.pumpWidget(upgradeCard); // Pump the UI so the upgrade card is displayed await tester.pumpAndSettle(const Duration(milliseconds: 5000)); @@ -668,7 +691,12 @@ void main() { upgrader.minAppVersion = '1.0.0'; - await tester.pumpWidget(_MyWidgetCard(upgrader: upgrader)); + final upgradeCard = wrapper( + UpgradeCard( + upgrader: upgrader, + ), + ); + await tester.pumpWidget(upgradeCard); // Pump the UI so the upgrade card is displayed await tester.pumpAndSettle(const Duration(milliseconds: 5000)); @@ -744,24 +772,27 @@ void main() { upgrader.initialize().then((value) {}); await tester.pumpAndSettle(); - var called = false; - var notCalled = true; - upgrader.onLater = () { - called = true; - return true; - }; - upgrader.onIgnore = () { - notCalled = false; - return true; - }; - upgrader.onUpdate = () { - notCalled = false; - return true; - }; - expect(upgrader.isTooSoon(), false); - await tester.pumpWidget(_MyWidgetCard(upgrader: upgrader)); + var called = false; + var notCalled = true; + final upgradeCard = wrapper( + UpgradeCard( + upgrader: upgrader, + onUpdate: () { + notCalled = false; + return true; + }, + onIgnore: () { + notCalled = false; + return true; + }, + onLater: () { + called = true; + }, + ), + ); + await tester.pumpWidget(upgradeCard); // Pump the UI so the upgrade card is displayed await tester.pumpAndSettle(); @@ -1110,50 +1141,6 @@ void verifyMessages(UpgraderMessages messages, String code) { expect(messages.message(UpgraderMessage.title), isNotEmpty); } -class _MyWidget extends StatelessWidget { - final Upgrader upgrader; - - const _MyWidget({Key? key, required this.upgrader}) : super(key: key); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Upgrader test', - home: Scaffold( - appBar: AppBar( - title: const Text('Upgrader test'), - ), - body: UpgradeAlert( - upgrader: upgrader, - child: const Column( - children: [Text('Upgrading')], - )), - ), - ); - } -} - -class _MyWidgetCard extends StatelessWidget { - final Upgrader upgrader; - - const _MyWidgetCard({Key? key, required this.upgrader}) : super(key: key); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Upgrader test', - home: Scaffold( - appBar: AppBar( - title: const Text('Upgrader test'), - ), - body: Column( - children: [UpgradeCard(upgrader: upgrader)], - ), - ), - ); - } -} - class MyUpgraderMessages extends UpgraderMessages { @override String get buttonTitleIgnore => 'aaa'; @@ -1167,3 +1154,12 @@ class MyUpgraderMessages extends UpgraderMessages { @override String get releaseNotes => 'ddd'; } + +Widget wrapper(Widget child) { + return MaterialApp( + home: Scaffold( + body: child, + appBar: AppBar(title: const Text('Upgrader test')), + ), + ); +} From 93a111d4ed76ab1fe0c1d01fd0dc0377528724dd Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sun, 10 Dec 2023 12:53:24 -0500 Subject: [PATCH 02/18] Updated workflow to use Flutter 3.13.1 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d27f71cf..a05dd070 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: runs-on: macos-latest strategy: matrix: - flutter-version: ['3.10.0'] + flutter-version: ['3.13.1'] steps: - uses: actions/checkout@v3 From cffaaf56a9669d2fb4326f6d439a6bc168b1e144 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sun, 10 Dec 2023 14:45:06 -0500 Subject: [PATCH 03/18] Added two more unit tests. --- test/upgrader_test.dart | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 91729549..3d72efce 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -495,6 +495,36 @@ void main() { expect(called, false); }, skip: false); + testWidgets('test UpgradeAlert no update', (WidgetTester tester) async { + expect(Upgrader.sharedInstance.isTooSoon(), false); + + final upgradeAlert = wrapper(UpgradeAlert()); + await tester.pumpWidget(upgradeAlert); + + // Pump the UI + await tester.pumpAndSettle(); + + expect(find.text('IGNORE'), findsNothing); + expect(find.text('LATER'), findsNothing); + expect(find.text('UPDATE'), findsNothing); + expect(find.text('Release Notes'), findsNothing); + }); + + testWidgets('test UpgradeCard no update', (WidgetTester tester) async { + expect(Upgrader.sharedInstance.isTooSoon(), false); + + final upgradeCard = wrapper(UpgradeCard()); + await tester.pumpWidget(upgradeCard); + + // Pump the UI + await tester.pumpAndSettle(); + + expect(find.text('IGNORE'), findsNothing); + expect(find.text('LATER'), findsNothing); + expect(find.text('UPDATE'), findsNothing); + expect(find.text('Release Notes'), findsNothing); + }); + testWidgets('test UpgradeCard upgrade', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader( From e829617720377f20464d9557ef4986e542e81783 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Wed, 13 Dec 2023 08:05:33 -0500 Subject: [PATCH 04/18] Added unit tests. --- example/lib/main-gorouter.dart | 2 +- pubspec.yaml | 1 + test/device_test.dart | 93 +++++++++++++++++++++++++++++++++- test/upgrader_test.dart | 76 +++++++++++++++++++++++++-- 4 files changed, 166 insertions(+), 6 deletions(-) diff --git a/example/lib/main-gorouter.dart b/example/lib/main-gorouter.dart index eeb3790c..c451ee09 100644 --- a/example/lib/main-gorouter.dart +++ b/example/lib/main-gorouter.dart @@ -13,7 +13,7 @@ void main() async { runApp(MyApp()); } -GoRouter routerConfig = GoRouter( +final routerConfig = GoRouter( initialLocation: '/page2', routes: [ GoRoute( diff --git a/pubspec.yaml b/pubspec.yaml index cd2d1fc3..ebd55c7c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,5 +45,6 @@ dev_dependencies: # From Dart Team: Mock library for Dart inspired by Mockito. mockito: ^5.4.3 flutter_lints: ^2.0.3 + go_router: ^7.1.1 flutter: diff --git a/test/device_test.dart b/test/device_test.dart index a917d059..e6b881a3 100644 --- a/test/device_test.dart +++ b/test/device_test.dart @@ -32,6 +32,28 @@ void main() { await device.getOsVersionString(MockUpgraderOS(android: true)), isNull); }); + test('testing UpgraderDevice iOS', () async { + deviceInfo = _iosInfo(baseOS: '1.2.3'); + final device = UpgraderDevice(); + expect(await device.getOsVersionString(MockUpgraderOS(ios: true)), '1.2.3'); + + // Verify invalid OS version + deviceInfo = _iosInfo(baseOS: '.'); + expect(await device.getOsVersionString(MockUpgraderOS(ios: true)), isNull); + }); + + // Note: this test causes exception in DeviceInfoPlugin + // test('testing UpgraderDevice Linux', () async { + // deviceInfo = _linuxInfo(baseOS: '1.2.3'); + // final device = UpgraderDevice(); + // expect( + // await device.getOsVersionString(MockUpgraderOS(linux: true)), '1.2.3'); + // // Verify invalid OS version + // deviceInfo = _linuxInfo(baseOS: '.'); + // expect( + // await device.getOsVersionString(MockUpgraderOS(linux: true)), isNull); + // }); + test('testing UpgraderDevice macOS', () async { deviceInfo = _macOSInfo(baseOS: '1.2.3'); final device = UpgraderDevice(); @@ -43,6 +65,23 @@ void main() { expect( await device.getOsVersionString(MockUpgraderOS(macos: true)), isNull); }); + + test('testing UpgraderDevice Web', () async { + final device = UpgraderDevice(); + expect(await device.getOsVersionString(MockUpgraderOS(web: true)), '0.0.0'); + }); + + // Note: this test causes exception in DeviceInfoPlugin + // test('testing UpgraderDevice Windows', () async { + // deviceInfo = _windowsInfo(baseOS: '1.2.3'); + // final device = UpgraderDevice(); + // expect(await device.getOsVersionString(MockUpgraderOS(windows: true)), + // '1.2.3'); + // // Verify invalid OS version + // deviceInfo = _windowsInfo(baseOS: '.'); + // expect( + // await device.getOsVersionString(MockUpgraderOS(windows: true)), isNull); + // }); } Map _androidInfo({required String baseOS}) { @@ -61,7 +100,6 @@ Map _androidInfo({required String baseOS}) { 'sdkInt': 1, 'securityPatch': 'a', }; - final build = { 'board': 'a', 'bootloader': 'a', @@ -89,6 +127,29 @@ Map _androidInfo({required String baseOS}) { return build; } +Map _iosInfo({required String baseOS}) { + final info = { + 'systemVersion': baseOS, + }; + return info; +} + +Map _linuxInfo({required String baseOS}) { + return { + 'name': 'a', + 'version': baseOS, + 'id': 'a', + 'idLike': ['a'], + 'versionCodename': 'a', + 'versionId': 'a', + 'prettyName': 'a', + 'buildId': 'a', + 'variant': 'a', + 'variantId': 'a', + 'machineId': 'a', + }; +} + Map _macOSInfo({required String baseOS}) { final info = { 'computerName': 'a', @@ -106,6 +167,36 @@ Map _macOSInfo({required String baseOS}) { 'cpuFrequency': 0, 'systemGUID': 'a', }; + return info; +} +Map _windowsInfo({required String baseOS}) { + final info = { + 'computerName': 'a', + 'numberOfCores': 'a', + 'systemMemoryInMegabytes': 'a', + 'userName': 'a', + 'majorVersion': 'a', + 'minorVersion': 'a', + 'buildNumber': 'a', + 'platformId': 'a', + 'csdVersion': 'a', + 'servicePackMajor': 'a', + 'servicePackMinor': 'a', + 'suitMask': 'a', + 'productType': 'a', + 'reserved': 'a', + 'buildLab': 'a', + 'buildLabEx': 'a', + 'digitalProductId': 'a', + 'displayVersion': baseOS, + 'editionId': 'a', + 'installDate': 'a', + 'productId': 'a', + 'productName': 'a', + 'registeredOwner': 'a', + 'releaseId': 'a', + 'deviceId': 'a', + }; return info; } diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 3d72efce..323de212 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; import 'package:http/src/client.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -350,8 +351,11 @@ void main() { testWidgets('test UpgradeAlert ignore', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); - final upgrader = - Upgrader(upgraderOS: MockUpgraderOS(ios: true), client: client); + final upgrader = Upgrader( + upgraderOS: MockUpgraderOS(ios: true), + client: client, + debugLogging: true, + ); upgrader.installPackageInfo( packageInfo: PackageInfo( @@ -402,8 +406,11 @@ void main() { testWidgets('test UpgradeAlert later', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); - final upgrader = - Upgrader(upgraderOS: MockUpgraderOS(ios: true), client: client); + final upgrader = Upgrader( + upgraderOS: MockUpgraderOS(ios: true), + client: client, + debugLogging: true, + ); upgrader.installPackageInfo( packageInfo: PackageInfo( @@ -1120,6 +1127,67 @@ void main() { expect(upgrader.appName(), isEmpty); expect(upgrader.currentInstalledVersion(), isEmpty); }, skip: false); + + testWidgets('test UpgradeAlert with GoRouter', (WidgetTester tester) async { + final client = MockITunesSearchClient.setupMockClient(); + final upgrader = Upgrader( + upgraderOS: MockUpgraderOS(ios: true), + client: client, + debugLogging: true, + ); + + upgrader.installPackageInfo( + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.larryaasen.upgrader', + version: '0.9.9', + buildNumber: '400')); + + GoRouter routerConfig = GoRouter( + initialLocation: '/page2', + routes: [ + GoRoute( + path: '/page1', + builder: (BuildContext context, GoRouterState state) => Scaffold( + appBar: AppBar(title: const Text('Upgrader GoRouter Example')), + body: const Center(child: Text('Checking... page1'))), + ), + GoRoute( + path: '/page2', + builder: (BuildContext context, GoRouterState state) => Scaffold( + appBar: AppBar(title: const Text('Upgrader GoRouter Example')), + body: const Center(child: Text('Checking... page2')), + ), + ), + ], + ); + + final router = MaterialApp.router( + title: 'Upgrader GoRouter Example', + routerConfig: routerConfig, + builder: (context, child) { + return UpgradeAlert( + upgrader: upgrader, + navigatorKey: routerConfig.routerDelegate.navigatorKey, + child: child ?? const Text('child'), + ); + }, + ); + + await tester.pumpWidget(router); + + // Pump the UI so the upgrade card is displayed + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + + expect(find.text('Upgrader GoRouter Example'), findsOneWidget); + + expect(find.text('Update App?'), findsOneWidget); + expect(find.text('IGNORE'), findsOneWidget); + expect(find.text('LATER'), findsOneWidget); + expect(find.text('UPDATE NOW'), findsOneWidget); + expect(find.text('Release Notes'), findsOneWidget); + }); }); test('test UpgraderMessages', () { From 7b705fa2598245f3c2f7fa5a34b411bf19838524 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Wed, 13 Dec 2023 08:36:49 -0500 Subject: [PATCH 05/18] Removed warnings. --- test/device_test.dart | 90 +++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/test/device_test.dart b/test/device_test.dart index e6b881a3..a4ef2229 100644 --- a/test/device_test.dart +++ b/test/device_test.dart @@ -134,21 +134,21 @@ Map _iosInfo({required String baseOS}) { return info; } -Map _linuxInfo({required String baseOS}) { - return { - 'name': 'a', - 'version': baseOS, - 'id': 'a', - 'idLike': ['a'], - 'versionCodename': 'a', - 'versionId': 'a', - 'prettyName': 'a', - 'buildId': 'a', - 'variant': 'a', - 'variantId': 'a', - 'machineId': 'a', - }; -} +// Map _linuxInfo({required String baseOS}) { +// return { +// 'name': 'a', +// 'version': baseOS, +// 'id': 'a', +// 'idLike': ['a'], +// 'versionCodename': 'a', +// 'versionId': 'a', +// 'prettyName': 'a', +// 'buildId': 'a', +// 'variant': 'a', +// 'variantId': 'a', +// 'machineId': 'a', +// }; +// } Map _macOSInfo({required String baseOS}) { final info = { @@ -170,33 +170,33 @@ Map _macOSInfo({required String baseOS}) { return info; } -Map _windowsInfo({required String baseOS}) { - final info = { - 'computerName': 'a', - 'numberOfCores': 'a', - 'systemMemoryInMegabytes': 'a', - 'userName': 'a', - 'majorVersion': 'a', - 'minorVersion': 'a', - 'buildNumber': 'a', - 'platformId': 'a', - 'csdVersion': 'a', - 'servicePackMajor': 'a', - 'servicePackMinor': 'a', - 'suitMask': 'a', - 'productType': 'a', - 'reserved': 'a', - 'buildLab': 'a', - 'buildLabEx': 'a', - 'digitalProductId': 'a', - 'displayVersion': baseOS, - 'editionId': 'a', - 'installDate': 'a', - 'productId': 'a', - 'productName': 'a', - 'registeredOwner': 'a', - 'releaseId': 'a', - 'deviceId': 'a', - }; - return info; -} +// Map _windowsInfo({required String baseOS}) { +// final info = { +// 'computerName': 'a', +// 'numberOfCores': 'a', +// 'systemMemoryInMegabytes': 'a', +// 'userName': 'a', +// 'majorVersion': 'a', +// 'minorVersion': 'a', +// 'buildNumber': 'a', +// 'platformId': 'a', +// 'csdVersion': 'a', +// 'servicePackMajor': 'a', +// 'servicePackMinor': 'a', +// 'suitMask': 'a', +// 'productType': 'a', +// 'reserved': 'a', +// 'buildLab': 'a', +// 'buildLabEx': 'a', +// 'digitalProductId': 'a', +// 'displayVersion': baseOS, +// 'editionId': 'a', +// 'installDate': 'a', +// 'productId': 'a', +// 'productName': 'a', +// 'registeredOwner': 'a', +// 'releaseId': 'a', +// 'deviceId': 'a', +// }; +// return info; +// } From 23acdaf180d4e6194ede710328ca32575bf781a5 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Thu, 14 Dec 2023 07:53:11 -0500 Subject: [PATCH 06/18] Fixed unit test. --- test/device_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/device_test.dart b/test/device_test.dart index a4ef2229..c46a479f 100644 --- a/test/device_test.dart +++ b/test/device_test.dart @@ -128,8 +128,22 @@ Map _androidInfo({required String baseOS}) { } Map _iosInfo({required String baseOS}) { + const iosUtsnameMap = { + 'release': 'release', + 'version': 'version', + 'machine': 'machine', + 'sysname': 'sysname', + 'nodename': 'nodename', + }; final info = { + 'name': 'name', + 'model': 'model', + 'utsname': iosUtsnameMap, + 'systemName': 'systemName', + 'isPhysicalDevice': 'false', 'systemVersion': baseOS, + 'localizedModel': 'localizedModel', + 'identifierForVendor': 'identifierForVendor', }; return info; } From a978f2d7d4252a1064eeeb883b44d4861ddd5266 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Wed, 27 Dec 2023 20:03:46 -0500 Subject: [PATCH 07/18] BREAKING: Moved UI related code outside of Upgrader and into UpgradeAlert and UpgradeCard. Also, renamed the private methods to make them public. Added and improved example code and README. --- README.md | 80 ++++++----- example/lib/main-alert-theme.dart | 52 +++++++ example/lib/main-appcast.dart | 15 +- example/lib/main-cupertino.dart | 19 +-- example/lib/main-custom-alert.dart | 89 ++++++++++++ example/lib/main-custom-card.dart | 58 ++++++++ example/lib/main-macos.dart | 17 +-- example/lib/main-messages.dart | 19 +-- example/lib/main-min-app-version.dart | 19 +-- example/lib/main-stateful.dart | 1 - example/lib/main_localized_rtl.dart | 4 - example/lib/main_subclass.dart | 6 +- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- lib/src/upgrade_alert.dart | 129 +++++++++--------- lib/src/upgrade_card.dart | 9 +- lib/src/upgrader.dart | 6 +- 17 files changed, 367 insertions(+), 160 deletions(-) create mode 100644 example/lib/main-alert-theme.dart create mode 100644 example/lib/main-custom-alert.dart create mode 100644 example/lib/main-custom-card.dart diff --git a/README.md b/README.md index 55e18ec3..dff8c8e4 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,11 @@ Tapping IGNORE prevents the alert from being displayed again for that version. Tapping the LATER button just closes the alert allowing the alert to be displayed sometime in the future. -Tapping the UPDATE NOW button takes the user to the App Store (iOS) or Google Play Store (Android) where the user is required to initiate the update process. +Tapping the UPDATE NOW button takes the user to the App Store (iOS) or Google Play Store (Android) where the user is expected to initiate the update process. ## Alert Example -Just wrap your body widget in the `UpgradeAlert` widget, and it will handle the rest. +Just wrap your home widget in the `UpgradeAlert` widget, and it will handle the rest. ```dart class MyApp extends StatelessWidget { const MyApp({Key key}) : super(key: key); @@ -71,14 +71,14 @@ class MyApp extends StatelessWidget { ![image](screenshots/example1.png) -## Cupertino Alert Example +## Cupertino alert example You can also display a Cupertino style dialog by using the `dialogStyle` parameter. ```dart - body: UpgradeAlert( - upgrader: Upgrader(dialogStyle: UpgradeDialogStyle.cupertino), - child: Center(child: Text('Checking...')), - ) + body: UpgradeAlert( + dialogStyle: UpgradeDialogStyle.cupertino, + child: Center(child: Text('Checking...')), + ) ``` ## Screenshot of Cupertino alert @@ -111,30 +111,48 @@ For [appcast](#appcast)), the release notes are taken from the description field ## Customization -The Upgrader class can be customized by setting parameters in the constructor. +The alert can be customized by changing the `DialogTheme` on the `MaterialApp`, or by overriding methods in the `UpgradeAlert` class. See these examples for more details: +- [example/lib/main-alert-theme.dart](example/lib/main-alert-theme.dart) +- [example/lib/main-custom-alert.dart](example/lib/main-custom-alert.dart) + +Here are the custom parameters for `UpgradeAlert`: -* appcast: Provide an Appcast that can be replaced for mock testing, defaults to ```null``` -* appcastConfig: the appcast configuration, defaults to ```null``` * canDismissDialog: can alert dialog be dismissed on tap outside of the alert dialog, which defaults to ```false``` (not used by UpgradeCard) -* countryCode: the country code that will override the system locale, which defaults to ```null``` * cupertinoButtonTextStyle: the text style for the cupertino dialog buttons, which defaults to ```null``` -* languageCode: the language code that will override the system locale, which defaults to ```null``` -* client: an HTTP Client that can be replaced for mock testing, defaults to ```null``` -* debugDisplayAlways: always force the upgrade to be available, defaults to ```false``` -* debugDisplayOnce: display the upgrade at least once, defaults to ```false``` -* debugLogging: display logging statements, which defaults to ```false``` * dialogStyle: the upgrade dialog style, either ```material``` or ```cupertino```, defaults to ```material```, used only by UpgradeAlert, works on Android and iOS. -* durationUntilAlertAgain: duration until alerting user again, which defaults to ```3 days``` -* messages: optional localized messages used for display in `upgrader` -* minAppVersion: the minimum app version supported by this app. Earlier versions of this app will be forced to update to the current version. It should be a valid version string like this: ```2.0.13```. Defaults to ```null```. * onIgnore: called when the ignore button is tapped, defaults to ```null``` * onLater: called when the later button is tapped, defaults to ```null``` * onUpdate: called when the update button is tapped, defaults to ```null``` -* platform: The [TargetPlatform] that identifies the platform on which the package is currently executing. Defaults to [defaultTargetPlatform]. Note that [TargetPlatform] does not include web, but includes mobile and desktop. This parameter is normally used to change the target platform during testing. * shouldPopScope: called when the back button is tapped, defaults to ```null``` * showIgnore: hide or show Ignore button, which defaults to ```true``` * showLater: hide or show Later button, which defaults to ```true``` * showReleaseNotes: hide or show release notes, which defaults to ```true``` + +Here are the custom parameters for `UpgradeCard`: + +* margin: The empty space that surrounds the card, defaults to ```null``` +* maxLines: An optional maximum number of lines for the text to span, wrapping if necessary, defaults to ```null``` +* onIgnore: called when the ignore button is tapped, defaults to ```null``` +* onLater: called when the later button is tapped, defaults to ```null``` +* onUpdate: called when the update button is tapped, defaults to ```null``` +* overflow: How visual overflow should be handled, defaults to ```null``` +* showIgnore: hide or show Ignore button, which defaults to ```true``` +* showLater: hide or show Later button, which defaults to ```true``` +* showReleaseNotes: hide or show release notes, which defaults to ```true``` + +The `Upgrader` class can be customized by setting parameters in the constructor, and passing it + +* appcast: Provide an Appcast that can be replaced for mock testing, defaults to ```null``` +* appcastConfig: the appcast configuration, defaults to ```null``` +* client: an HTTP Client that can be replaced for mock testing, defaults to ```null``` +* countryCode: the country code that will override the system locale, which defaults to ```null``` +* languageCode: the language code that will override the system locale, which defaults to ```null``` +* debugDisplayAlways: always force the upgrade to be available, defaults to ```false``` +* debugDisplayOnce: display the upgrade at least once, defaults to ```false``` +* debugLogging: display logging statements, which defaults to ```false``` +* durationUntilAlertAgain: duration until alerting user again, which defaults to ```3 days``` +* messages: optional localized messages used for display in `upgrader` +* minAppVersion: the minimum app version supported by this app. Earlier versions of this app will be forced to update to the current version. It should be a valid version string like this: ```2.0.13```. Defaults to ```null```. * upgraderOS: Provides information on which OS this code is running on, defaults to ```null``` * willDisplayUpgrade: called when ```upgrader``` determines that an upgrade may or may not be displayed, defaults to ```null``` @@ -204,7 +222,7 @@ When using the ```UpgradeAlert``` widget, the Android back button will not dismiss the alert dialog by default. To allow the back button to dismiss the dialog, use ```shouldPopScope``` and return true like this: ``` -UpgradeAlert(Upgrader(shouldPopScope: () => true)); +UpgradeAlert(shouldPopScope: () => true); ``` ## Country Code @@ -218,7 +236,7 @@ On Android, the `upgrader` package uses the system locale to determine the count ## Android Language Code -Android description and release notes language, defaults to `en`. +Android description and release notes language default to `en`. ## Limitations These widgets work on both Android and iOS. When running on Android the Google @@ -258,22 +276,20 @@ The Appcast class can be used stand alone or as part of `upgrader`. ### Appcast Example This is an Appcast example for Android. ```dart +static const appcastURL = + 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; +final upgrader = Upgrader( + appcastConfig: + AppcastConfiguration(url: appcastURL, supportedOS: ['android'])); + @override Widget build(BuildContext context) { - // On Android, setup the Appcast. - // On iOS, the default behavior will be to use the App Store version. - final appcastURL = - 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; - final cfg = AppcastConfiguration(url: appcastURL, supportedOS: ['android']); - return MaterialApp( title: 'Upgrader Example', home: Scaffold( - appBar: AppBar( - title: Text('Upgrader Example'), - ), + appBar: AppBar(title: Text('Upgrader Appcast Example')), body: UpgradeAlert( - Upgrader(appcastConfig: cfg), + upgrader: upgrader, child: Center(child: Text('Checking...')), )), ); diff --git a/example/lib/main-alert-theme.dart b/example/lib/main-alert-theme.dart new file mode 100644 index 00000000..8326b0ac --- /dev/null +++ b/example/lib/main-alert-theme.dart @@ -0,0 +1,52 @@ +// Copyright (c) 2023 Larry Aasen. All rights reserved. + +import 'package:flutter/material.dart'; +import 'package:upgrader/upgrader.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Only call clearSavedSettings() during testing to reset internal values. + await Upgrader.clearSavedSettings(); // REMOVE this for release builds + + // On Android, the default behavior will be to use the Google Play Store + // version of the app. + // On iOS, the default behavior will be to use the App Store version of + // the app, so update the Bundle Identifier in example/ios/Runner with a + // valid identifier already in the App Store. + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + MyApp({super.key}); + + final dark = ThemeData.dark(useMaterial3: true); + + final light = ThemeData( + dialogTheme: DialogTheme( + titleTextStyle: TextStyle(color: Colors.red, fontSize: 48), + contentTextStyle: TextStyle(color: Colors.green, fontSize: 18), + ), + // Change the text buttons. + textButtonTheme: const TextButtonThemeData( + style: ButtonStyle( + // Change the color of the text buttons. + foregroundColor: MaterialStatePropertyAll(Colors.orange), + ), + ), + ); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Upgrader Example', + home: UpgradeAlert( + child: Scaffold( + appBar: AppBar(title: Text('Upgrader Example')), + body: Center(child: Text('Checking...')), + )), + theme: light, + darkTheme: dark, + ); + } +} diff --git a/example/lib/main-appcast.dart b/example/lib/main-appcast.dart index 34059b15..236ec972 100644 --- a/example/lib/main-appcast.dart +++ b/example/lib/main-appcast.dart @@ -21,21 +21,20 @@ void main() async { class MyApp extends StatelessWidget { MyApp({Key? key}) : super(key: key); + static const appcastURL = + 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; + final upgrader = Upgrader( + appcastConfig: + AppcastConfiguration(url: appcastURL, supportedOS: ['android'])); + @override Widget build(BuildContext context) { - final appcastURL = - 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; - final cfg = AppcastConfiguration(url: appcastURL, supportedOS: ['android']); - return MaterialApp( title: 'Upgrader Example', home: Scaffold( appBar: AppBar(title: Text('Upgrader Appcast Example')), body: UpgradeAlert( - upgrader: Upgrader( - appcastConfig: cfg, - debugLogging: true, - ), + upgrader: upgrader, child: Center(child: Text('Checking...')), )), ); diff --git a/example/lib/main-cupertino.dart b/example/lib/main-cupertino.dart index 0b00b67b..5546804b 100644 --- a/example/lib/main-cupertino.dart +++ b/example/lib/main-cupertino.dart @@ -23,22 +23,15 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - final appcastURL = - 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; - final cfg = AppcastConfiguration(url: appcastURL, supportedOS: ['android']); - return MaterialApp( title: 'Upgrader Example', home: Scaffold( - appBar: AppBar(title: Text('Upgrader Cupertino Example')), - body: UpgradeAlert( - upgrader: Upgrader( - appcastConfig: cfg, - debugLogging: true, - ), - dialogStyle: UpgradeDialogStyle.cupertino, - child: Center(child: Text('Checking...')), - )), + appBar: AppBar(title: Text('Upgrader Cupertino Example')), + body: UpgradeAlert( + dialogStyle: UpgradeDialogStyle.cupertino, + child: Center(child: Text('Checking...')), + ), + ), ); } } diff --git a/example/lib/main-custom-alert.dart b/example/lib/main-custom-alert.dart new file mode 100644 index 00000000..ca8110af --- /dev/null +++ b/example/lib/main-custom-alert.dart @@ -0,0 +1,89 @@ +// Copyright (c) 2023 Larry Aasen. All rights reserved. + +import 'package:flutter/material.dart'; +import 'package:upgrader/upgrader.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Only call clearSavedSettings() during testing to reset internal values. + await Upgrader.clearSavedSettings(); // REMOVE this for release builds + + // On Android, the default behavior will be to use the Google Play Store + // version of the app. + // On iOS, the default behavior will be to use the App Store version of + // the app, so update the Bundle Identifier in example/ios/Runner with a + // valid identifier already in the App Store. + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + MyApp({super.key}); + + final upgrader = MyUpgrader(debugLogging: true); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Upgrader Example', + home: MyUpgradeAlert( + upgrader: upgrader, + child: Scaffold( + appBar: AppBar(title: Text('Upgrader Example')), + body: Center(child: Text('Checking...')), + )), + ); + } +} + +class MyUpgrader extends Upgrader { + MyUpgrader({super.debugLogging}); + + @override + bool isTooSoon() { + return super.isTooSoon(); + } +} + +class MyUpgradeAlert extends UpgradeAlert { + MyUpgradeAlert({super.upgrader, super.child}); + + @override + void showTheDialog({ + required BuildContext context, + required String? title, + required String message, + required String? releaseNotes, + required bool canDismissDialog, + required UpgraderMessages messages, + }) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Update?'), + content: const SingleChildScrollView( + child: ListBody( + children: [ + Text('Would you like to update?'), + ], + ), + ), + actions: [ + TextButton( + child: const Text('No'), + onPressed: () { + onUserIgnored(context, true); + }, + ), + TextButton( + child: const Text('Yes'), + onPressed: () { + onUserUpdated(context, !upgrader.blocked()); + }, + ), + ], + ); + }); + } +} diff --git a/example/lib/main-custom-card.dart b/example/lib/main-custom-card.dart new file mode 100644 index 00000000..9407bb83 --- /dev/null +++ b/example/lib/main-custom-card.dart @@ -0,0 +1,58 @@ +// Copyright (c) 2023 Larry Aasen. All rights reserved. + +import 'package:flutter/material.dart'; +import 'package:upgrader/upgrader.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Only call clearSavedSettings() during testing to reset internal values. + await Upgrader.clearSavedSettings(); // REMOVE this for release builds + + // On Android, the default behavior will be to use the Google Play Store + // version of the app. + // On iOS, the default behavior will be to use the App Store version of + // the app, so update the Bundle Identifier in example/ios/Runner with a + // valid identifier already in the App Store. + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Upgrader Card Example', + home: Scaffold( + appBar: AppBar(title: Text('Upgrader Card Example')), + body: Container( + margin: EdgeInsets.only(left: 12.0, right: 12.0), + child: SingleChildScrollView( + child: Column( + children: [ + _simpleCard, + _simpleCard, + MyUpgradeCard(), + _simpleCard, + _simpleCard, + ], + ), + ), + ), + ), + ); + } + + Widget get _simpleCard => Card( + child: SizedBox( + width: 200, + height: 50, + child: Center(child: Text('Card')), + ), + ); +} + +class MyUpgradeCard extends UpgradeCard { + MyUpgradeCard({super.upgrader}); +} diff --git a/example/lib/main-macos.dart b/example/lib/main-macos.dart index 42039639..d6271b46 100644 --- a/example/lib/main-macos.dart +++ b/example/lib/main-macos.dart @@ -15,19 +15,20 @@ void main() async { class MyApp extends StatelessWidget { MyApp({Key? key}) : super(key: key); + static const appcastURL = + 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast_macos.xml'; + final upgrader = Upgrader( + appcastConfig: + AppcastConfiguration(url: appcastURL, supportedOS: ['macos']), + debugLogging: true, + ); + @override Widget build(BuildContext context) { - final appcastURL = - 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast_macos.xml'; - final cfg = AppcastConfiguration(url: appcastURL, supportedOS: ['macos']); - return MaterialApp( title: 'Upgrader Example', home: UpgradeAlert( - upgrader: Upgrader( - appcastConfig: cfg, - debugLogging: true, - ), + upgrader: upgrader, child: Scaffold( appBar: AppBar(title: Text('Upgrader Example')), body: Center(child: Text('Checking...')), diff --git a/example/lib/main-messages.dart b/example/lib/main-messages.dart index 7c78c2e1..44567a86 100644 --- a/example/lib/main-messages.dart +++ b/example/lib/main-messages.dart @@ -76,20 +76,21 @@ class MyApp extends StatelessWidget { } class DemoApp extends StatelessWidget { + static const appcastURL = + 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; + final upgrader = Upgrader( + appcastConfig: + AppcastConfiguration(url: appcastURL, supportedOS: ['android']), + debugLogging: true, + messages: MyUpgraderMessages(code: 'es'), + ); + @override Widget build(BuildContext context) { - final appcastURL = - 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; - final cfg = AppcastConfiguration(url: appcastURL, supportedOS: ['android']); - return Scaffold( appBar: AppBar(title: Text(DemoLocalizations.of(context).title)), body: UpgradeAlert( - upgrader: Upgrader( - appcastConfig: cfg, - debugLogging: true, - messages: MyUpgraderMessages(code: 'es'), - ), + upgrader: upgrader, child: Center(child: Text(DemoLocalizations.of(context).checking)), )); } diff --git a/example/lib/main-min-app-version.dart b/example/lib/main-min-app-version.dart index 7cabd1da..7f47fb94 100644 --- a/example/lib/main-min-app-version.dart +++ b/example/lib/main-min-app-version.dart @@ -21,22 +21,23 @@ void main() async { class MyApp extends StatelessWidget { MyApp({Key? key}) : super(key: key); + static const appcastURL = + 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; + final upgrader = Upgrader( + appcastConfig: + AppcastConfiguration(url: appcastURL, supportedOS: ['android']), + debugLogging: true, + minAppVersion: '1.1.0', + ); + @override Widget build(BuildContext context) { - final appcastURL = - 'https://raw.githubusercontent.com/larryaasen/upgrader/master/test/testappcast.xml'; - final cfg = AppcastConfiguration(url: appcastURL, supportedOS: ['android']); - return MaterialApp( title: 'Upgrader Example', home: Scaffold( appBar: AppBar(title: Text('Upgrader Example')), body: UpgradeAlert( - upgrader: Upgrader( - appcastConfig: cfg, - debugLogging: true, - minAppVersion: '1.1.0', - ), + upgrader: upgrader, child: Center(child: Text('Checking...')), )), ); diff --git a/example/lib/main-stateful.dart b/example/lib/main-stateful.dart index 7fad27d0..20ece52c 100644 --- a/example/lib/main-stateful.dart +++ b/example/lib/main-stateful.dart @@ -37,7 +37,6 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - print('_MyAppState.build called'); return MaterialApp( title: 'Upgrader StatefulWidget Example', home: UpgradeAlert( diff --git a/example/lib/main_localized_rtl.dart b/example/lib/main_localized_rtl.dart index d3c4c20f..c963951c 100644 --- a/example/lib/main_localized_rtl.dart +++ b/example/lib/main_localized_rtl.dart @@ -10,10 +10,6 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, setup the Appcast. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. runApp(MyApp()); } diff --git a/example/lib/main_subclass.dart b/example/lib/main_subclass.dart index e4de502f..f0f9f56c 100644 --- a/example/lib/main_subclass.dart +++ b/example/lib/main_subclass.dart @@ -20,7 +20,9 @@ void main() async { } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + MyApp({Key? key}) : super(key: key); + + final upgrader = MyUpgrader(); @override Widget build(BuildContext context) { @@ -29,7 +31,7 @@ class MyApp extends StatelessWidget { home: Scaffold( appBar: AppBar(title: Text('Upgrader Subclass Example')), body: UpgradeAlert( - upgrader: MyUpgrader(), + upgrader: upgrader, child: Center(child: Text('Checking...')), )), ); diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index dcce4e95..cc5da2e1 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -203,7 +203,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index fb7259e1..83d88728 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UpgradeAlertBaseState(); -} - -class UpgradeAlertBaseState extends State { - bool _displayed = false; + static bool _displayed = false; @override - void initState() { - super.initState(); - widget.upgrader.initialize(); - } + UpgradeAlertBaseState createState() => UpgradeAlertBaseState(); - /// Describes the part of the user interface represented by this widget. - @override Widget build(BuildContext context) { - if (widget.upgrader.debugLogging) { + if (upgrader.debugLogging) { print('upgrader: build UpgradeAlert'); } return StreamBuilder( - initialData: widget.upgrader.evaluationReady, - stream: widget.upgrader.evaluationStream, + initialData: upgrader.evaluationReady, + stream: upgrader.evaluationStream, builder: (BuildContext context, AsyncSnapshot snapshot) { if ((snapshot.connectionState == ConnectionState.waiting || snapshot.connectionState == ConnectionState.active) && snapshot.data != null && snapshot.data!) { - if (widget.upgrader.debugLogging) { + if (upgrader.debugLogging) { print("upgrader: need to evaluate version"); } - final checkContext = widget.navigatorKey != null && - widget.navigatorKey!.currentContext != null - ? widget.navigatorKey!.currentContext! - : context; + final checkContext = + navigatorKey != null && navigatorKey!.currentContext != null + ? navigatorKey!.currentContext! + : context; checkVersion(context: checkContext); } - return widget.child ?? const SizedBox.shrink(); + return child ?? const SizedBox.shrink(); }, ); } @@ -122,40 +112,40 @@ class UpgradeAlertBaseState extends State { /// Will show the alert dialog when it should be dispalyed. /// Only called by [UpgradeAlert] and not used by [UpgradeCard]. void checkVersion({required BuildContext context}) { - if (!_displayed) { - final shouldDisplay = widget.upgrader.shouldDisplayUpgrade(); - if (widget.upgrader.debugLogging) { - print('upgrader: shouldDisplayReleaseNotes: shouldDisplayReleaseNotes'); - } - if (shouldDisplay) { - _displayed = true; - final appMessages = widget.upgrader.determineMessages(context); - - Future.delayed(const Duration(milliseconds: 0), () { - showTheDialog( - context: context, - title: appMessages.message(UpgraderMessage.title), - message: widget.upgrader.body(appMessages), - releaseNotes: - shouldDisplayReleaseNotes ? widget.upgrader.releaseNotes : null, - canDismissDialog: widget.canDismissDialog, - messages: appMessages, - ); - }); - } + if (_displayed) return; + + final shouldDisplay = upgrader.shouldDisplayUpgrade(); + if (upgrader.debugLogging) { + print('upgrader: shouldDisplayReleaseNotes: shouldDisplayReleaseNotes'); + } + if (shouldDisplay) { + _displayed = true; + final appMessages = upgrader.determineMessages(context); + + Future.delayed(const Duration(milliseconds: 0), () { + showTheDialog( + context: context, + title: appMessages.message(UpgraderMessage.title), + message: upgrader.body(appMessages), + releaseNotes: + shouldDisplayReleaseNotes ? upgrader.releaseNotes : null, + canDismissDialog: canDismissDialog, + messages: appMessages, + ); + }); } } void onUserIgnored(BuildContext context, bool shouldPop) { - if (widget.upgrader.debugLogging) { + if (upgrader.debugLogging) { print('upgrader: button tapped: ignore'); } // If this callback has been provided, call it. - final doProcess = widget.onIgnore?.call() ?? true; + final doProcess = onIgnore?.call() ?? true; if (doProcess) { - widget.upgrader.saveIgnored(); + upgrader.saveIgnored(); } if (shouldPop) { @@ -164,12 +154,12 @@ class UpgradeAlertBaseState extends State { } void onUserLater(BuildContext context, bool shouldPop) { - if (widget.upgrader.debugLogging) { + if (upgrader.debugLogging) { print('upgrader: button tapped: later'); } // If this callback has been provided, call it. - widget.onLater?.call(); + onLater?.call(); if (shouldPop) { popNavigator(context); @@ -177,15 +167,15 @@ class UpgradeAlertBaseState extends State { } void onUserUpdated(BuildContext context, bool shouldPop) { - if (widget.upgrader.debugLogging) { + if (upgrader.debugLogging) { print('upgrader: button tapped: update now'); } // If this callback has been provided, call it. - final doProcess = widget.onUpdate?.call() ?? true; + final doProcess = onUpdate?.call() ?? true; if (doProcess) { - widget.upgrader.sendUserToAppStore(); + upgrader.sendUserToAppStore(); } if (shouldPop) { @@ -199,8 +189,7 @@ class UpgradeAlertBaseState extends State { } bool get shouldDisplayReleaseNotes => - widget.showReleaseNotes && - (widget.upgrader.releaseNotes?.isNotEmpty ?? false); + showReleaseNotes && (upgrader.releaseNotes?.isNotEmpty ?? false); void showTheDialog({ required BuildContext context, @@ -210,14 +199,14 @@ class UpgradeAlertBaseState extends State { required bool canDismissDialog, required UpgraderMessages messages, }) { - if (widget.upgrader.debugLogging) { + if (upgrader.debugLogging) { print('upgrader: showTheDialog title: $title'); print('upgrader: showTheDialog message: $message'); print('upgrader: showTheDialog releaseNotes: $releaseNotes'); } // Save the date/time as the last time alerted. - widget.upgrader.saveLastAlerted(); + upgrader.saveLastAlerted(); showDialog( barrierDismissible: canDismissDialog, @@ -230,7 +219,7 @@ class UpgradeAlertBaseState extends State { message, releaseNotes, context, - widget.dialogStyle == UpgradeDialogStyle.cupertino, + dialogStyle == UpgradeDialogStyle.cupertino, messages, )); }, @@ -241,12 +230,12 @@ class UpgradeAlertBaseState extends State { /// is false. Also called when the back button is pressed. Return true for /// the screen to be popped. Defaults to false. bool onWillPop() { - if (widget.upgrader.debugLogging) { + if (upgrader.debugLogging) { print('upgrader: onWillPop called'); } - if (widget.shouldPopScope != null) { - final should = widget.shouldPopScope!(); - if (widget.upgrader.debugLogging) { + if (shouldPopScope != null) { + final should = shouldPopScope!(); + if (upgrader.debugLogging) { print('upgrader: shouldPopScope=$should'); } return should; @@ -259,9 +248,9 @@ class UpgradeAlertBaseState extends State { BuildContext context, bool cupertino, UpgraderMessages messages) { // If installed version is below minimum app version, or is a critical update, // disable ignore and later buttons. - final isBlocked = widget.upgrader.blocked(); - final showIgnore = isBlocked ? false : widget.showIgnore; - final showLater = isBlocked ? false : widget.showLater; + final isBlocked = upgrader.blocked(); + final showIgnore = isBlocked ? false : this.showIgnore; + final showLater = isBlocked ? false : this.showLater; Widget? notes; if (releaseNotes != null) { @@ -303,7 +292,7 @@ class UpgradeAlertBaseState extends State { button(cupertino, messages.message(UpgraderMessage.buttonTitleLater), context, () => onUserLater(context, true)), button(cupertino, messages.message(UpgraderMessage.buttonTitleUpdate), - context, () => onUserUpdated(context, !widget.upgrader.blocked())), + context, () => onUserUpdated(context, !upgrader.blocked())), ]; return cupertino @@ -316,9 +305,21 @@ class UpgradeAlertBaseState extends State { VoidCallback? onPressed) { return cupertino ? CupertinoDialogAction( - textStyle: widget.cupertinoButtonTextStyle, + textStyle: cupertinoButtonTextStyle, onPressed: onPressed, child: Text(text ?? '')) : TextButton(onPressed: onPressed, child: Text(text ?? '')); } } + +class UpgradeAlertBaseState extends State { + @override + void initState() { + super.initState(); + widget.upgrader.initialize(); + } + + /// Describes the part of the user interface represented by this widget. + @override + Widget build(BuildContext context) => widget.build(context); +} diff --git a/lib/src/upgrade_card.dart b/lib/src/upgrade_card.dart index eb6c36a4..b813b877 100644 --- a/lib/src/upgrade_card.dart +++ b/lib/src/upgrade_card.dart @@ -14,7 +14,7 @@ class UpgradeCard extends StatefulWidget { UpgradeCard({ super.key, Upgrader? upgrader, - this.margin = const EdgeInsets.all(4.0), + this.margin, this.maxLines = 15, this.onIgnore, this.onLater, @@ -30,9 +30,8 @@ class UpgradeCard extends StatefulWidget { /// The empty space that surrounds the card. /// - /// The default margin is 4.0 logical pixels on all sides: - /// `EdgeInsets.all(4.0)`. - final EdgeInsetsGeometry margin; + /// The default margin is [Card.margin]. + final EdgeInsetsGeometry? margin; /// An optional maximum number of lines for the text to span, wrapping if necessary. final int? maxLines; @@ -140,7 +139,7 @@ class UpgradeCardBaseState extends State { } return Card( - color: Colors.white, + // color: Colors.white, margin: widget.margin, child: AlertStyleWidget( title: Text(title ?? ''), diff --git a/lib/src/upgrader.dart b/lib/src/upgrader.dart index 6eaf869b..6cb5ca31 100644 --- a/lib/src/upgrader.dart +++ b/lib/src/upgrader.dart @@ -107,15 +107,15 @@ class Upgrader with WidgetsBindingObserver { /// will be forced to update to the current version. Optional. String? minAppVersion; + /// Provides information on which OS this code is running on. + final UpgraderOS upgraderOS; + /// Called when [Upgrader] determines that an upgrade may or may not be /// displayed. The [value] parameter will be true when it should be displayed, /// and false when it should not be displayed. One good use for this callback /// is logging metrics for your app. WillDisplayUpgradeCallback? willDisplayUpgrade; - /// Provides information on which OS this code is running on. - final UpgraderOS upgraderOS; - bool _initCalled = false; PackageInfo? _packageInfo; From e654355faaad301bdfb1f561ff68080a006afd19 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Thu, 28 Dec 2023 13:36:05 -0500 Subject: [PATCH 08/18] Moved methods again from the alert class to the state class. --- CHANGELOG.md | 2 +- example/lib/main-custom-alert.dart | 9 ++- lib/src/upgrade_alert.dart | 114 +++++++++++++++-------------- 3 files changed, 68 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9b3981..e7ee2ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## 9.0.0-alpha.1 - BREAKING: Moved UI related code outside of Upgrader and into UpgradeAlert and UpgradeCard. Also, -renamed the private methods to make them public. +renamed the private methods to make them public. Added and improved example code and README. ## 8.4.0 diff --git a/example/lib/main-custom-alert.dart b/example/lib/main-custom-alert.dart index ca8110af..62006c84 100644 --- a/example/lib/main-custom-alert.dart +++ b/example/lib/main-custom-alert.dart @@ -48,6 +48,13 @@ class MyUpgrader extends Upgrader { class MyUpgradeAlert extends UpgradeAlert { MyUpgradeAlert({super.upgrader, super.child}); + /// Override the [createState] method to provide a custom class + /// with overridden methods. + @override + UpgradeAlertState createState() => MyUpgradeAlertState(); +} + +class MyUpgradeAlertState extends UpgradeAlertState { @override void showTheDialog({ required BuildContext context, @@ -79,7 +86,7 @@ class MyUpgradeAlert extends UpgradeAlert { TextButton( child: const Text('Yes'), onPressed: () { - onUserUpdated(context, !upgrader.blocked()); + onUserUpdated(context, !widget.upgrader.blocked()); }, ), ], diff --git a/lib/src/upgrade_alert.dart b/lib/src/upgrade_alert.dart index 4f7a7035..e7af06b3 100644 --- a/lib/src/upgrade_alert.dart +++ b/lib/src/upgrade_alert.dart @@ -12,6 +12,8 @@ import 'upgrader.dart'; enum UpgradeDialogStyle { cupertino, material } /// A widget to display the upgrade dialog. +/// Override the [createState] method to provide a custom class +/// with overridden methods. class UpgradeAlert extends StatefulWidget { /// Creates a new [UpgradeAlert]. UpgradeAlert({ @@ -75,36 +77,50 @@ class UpgradeAlert extends StatefulWidget { /// The [child] contained by the widget. final Widget? child; - static bool _displayed = false; + @override + UpgradeAlertState createState() => UpgradeAlertState(); +} + +/// The [UpgradeAlert] widget state. +class UpgradeAlertState extends State { + /// Is the alert dialog being displayed right now? + bool displayed = false; @override - UpgradeAlertBaseState createState() => UpgradeAlertBaseState(); + void initState() { + super.initState(); + widget.upgrader.initialize(); + } + /// Describes the part of the user interface represented by this widget. + @override Widget build(BuildContext context) { - if (upgrader.debugLogging) { + if (widget.upgrader.debugLogging) { print('upgrader: build UpgradeAlert'); } return StreamBuilder( - initialData: upgrader.evaluationReady, - stream: upgrader.evaluationStream, + initialData: widget.upgrader.evaluationReady, + stream: widget.upgrader.evaluationStream, builder: (BuildContext context, AsyncSnapshot snapshot) { if ((snapshot.connectionState == ConnectionState.waiting || snapshot.connectionState == ConnectionState.active) && snapshot.data != null && snapshot.data!) { - if (upgrader.debugLogging) { + if (widget.upgrader.debugLogging) { print("upgrader: need to evaluate version"); } - final checkContext = - navigatorKey != null && navigatorKey!.currentContext != null - ? navigatorKey!.currentContext! - : context; - checkVersion(context: checkContext); + if (!displayed) { + final checkContext = widget.navigatorKey != null && + widget.navigatorKey!.currentContext != null + ? widget.navigatorKey!.currentContext! + : context; + checkVersion(context: checkContext); + } } - return child ?? const SizedBox.shrink(); + return widget.child ?? const SizedBox.shrink(); }, ); } @@ -112,24 +128,22 @@ class UpgradeAlert extends StatefulWidget { /// Will show the alert dialog when it should be dispalyed. /// Only called by [UpgradeAlert] and not used by [UpgradeCard]. void checkVersion({required BuildContext context}) { - if (_displayed) return; - - final shouldDisplay = upgrader.shouldDisplayUpgrade(); - if (upgrader.debugLogging) { + final shouldDisplay = widget.upgrader.shouldDisplayUpgrade(); + if (widget.upgrader.debugLogging) { print('upgrader: shouldDisplayReleaseNotes: shouldDisplayReleaseNotes'); } if (shouldDisplay) { - _displayed = true; - final appMessages = upgrader.determineMessages(context); + displayed = true; + final appMessages = widget.upgrader.determineMessages(context); Future.delayed(const Duration(milliseconds: 0), () { showTheDialog( context: context, title: appMessages.message(UpgraderMessage.title), - message: upgrader.body(appMessages), + message: widget.upgrader.body(appMessages), releaseNotes: - shouldDisplayReleaseNotes ? upgrader.releaseNotes : null, - canDismissDialog: canDismissDialog, + shouldDisplayReleaseNotes ? widget.upgrader.releaseNotes : null, + canDismissDialog: widget.canDismissDialog, messages: appMessages, ); }); @@ -137,15 +151,15 @@ class UpgradeAlert extends StatefulWidget { } void onUserIgnored(BuildContext context, bool shouldPop) { - if (upgrader.debugLogging) { + if (widget.upgrader.debugLogging) { print('upgrader: button tapped: ignore'); } // If this callback has been provided, call it. - final doProcess = onIgnore?.call() ?? true; + final doProcess = widget.onIgnore?.call() ?? true; if (doProcess) { - upgrader.saveIgnored(); + widget.upgrader.saveIgnored(); } if (shouldPop) { @@ -154,12 +168,12 @@ class UpgradeAlert extends StatefulWidget { } void onUserLater(BuildContext context, bool shouldPop) { - if (upgrader.debugLogging) { + if (widget.upgrader.debugLogging) { print('upgrader: button tapped: later'); } // If this callback has been provided, call it. - onLater?.call(); + widget.onLater?.call(); if (shouldPop) { popNavigator(context); @@ -167,15 +181,15 @@ class UpgradeAlert extends StatefulWidget { } void onUserUpdated(BuildContext context, bool shouldPop) { - if (upgrader.debugLogging) { + if (widget.upgrader.debugLogging) { print('upgrader: button tapped: update now'); } // If this callback has been provided, call it. - final doProcess = onUpdate?.call() ?? true; + final doProcess = widget.onUpdate?.call() ?? true; if (doProcess) { - upgrader.sendUserToAppStore(); + widget.upgrader.sendUserToAppStore(); } if (shouldPop) { @@ -185,12 +199,14 @@ class UpgradeAlert extends StatefulWidget { void popNavigator(BuildContext context) { Navigator.of(context).pop(); - _displayed = false; + displayed = false; } bool get shouldDisplayReleaseNotes => - showReleaseNotes && (upgrader.releaseNotes?.isNotEmpty ?? false); + widget.showReleaseNotes && + (widget.upgrader.releaseNotes?.isNotEmpty ?? false); + /// Show the alert dialog. void showTheDialog({ required BuildContext context, required String? title, @@ -199,14 +215,14 @@ class UpgradeAlert extends StatefulWidget { required bool canDismissDialog, required UpgraderMessages messages, }) { - if (upgrader.debugLogging) { + if (widget.upgrader.debugLogging) { print('upgrader: showTheDialog title: $title'); print('upgrader: showTheDialog message: $message'); print('upgrader: showTheDialog releaseNotes: $releaseNotes'); } // Save the date/time as the last time alerted. - upgrader.saveLastAlerted(); + widget.upgrader.saveLastAlerted(); showDialog( barrierDismissible: canDismissDialog, @@ -219,7 +235,7 @@ class UpgradeAlert extends StatefulWidget { message, releaseNotes, context, - dialogStyle == UpgradeDialogStyle.cupertino, + widget.dialogStyle == UpgradeDialogStyle.cupertino, messages, )); }, @@ -230,12 +246,12 @@ class UpgradeAlert extends StatefulWidget { /// is false. Also called when the back button is pressed. Return true for /// the screen to be popped. Defaults to false. bool onWillPop() { - if (upgrader.debugLogging) { + if (widget.upgrader.debugLogging) { print('upgrader: onWillPop called'); } - if (shouldPopScope != null) { - final should = shouldPopScope!(); - if (upgrader.debugLogging) { + if (widget.shouldPopScope != null) { + final should = widget.shouldPopScope!(); + if (widget.upgrader.debugLogging) { print('upgrader: shouldPopScope=$should'); } return should; @@ -248,9 +264,9 @@ class UpgradeAlert extends StatefulWidget { BuildContext context, bool cupertino, UpgraderMessages messages) { // If installed version is below minimum app version, or is a critical update, // disable ignore and later buttons. - final isBlocked = upgrader.blocked(); - final showIgnore = isBlocked ? false : this.showIgnore; - final showLater = isBlocked ? false : this.showLater; + final isBlocked = widget.upgrader.blocked(); + final showIgnore = isBlocked ? false : widget.showIgnore; + final showLater = isBlocked ? false : widget.showLater; Widget? notes; if (releaseNotes != null) { @@ -292,7 +308,7 @@ class UpgradeAlert extends StatefulWidget { button(cupertino, messages.message(UpgraderMessage.buttonTitleLater), context, () => onUserLater(context, true)), button(cupertino, messages.message(UpgraderMessage.buttonTitleUpdate), - context, () => onUserUpdated(context, !upgrader.blocked())), + context, () => onUserUpdated(context, !widget.upgrader.blocked())), ]; return cupertino @@ -305,21 +321,9 @@ class UpgradeAlert extends StatefulWidget { VoidCallback? onPressed) { return cupertino ? CupertinoDialogAction( - textStyle: cupertinoButtonTextStyle, + textStyle: widget.cupertinoButtonTextStyle, onPressed: onPressed, child: Text(text ?? '')) : TextButton(onPressed: onPressed, child: Text(text ?? '')); } } - -class UpgradeAlertBaseState extends State { - @override - void initState() { - super.initState(); - widget.upgrader.initialize(); - } - - /// Describes the part of the user interface represented by this widget. - @override - Widget build(BuildContext context) => widget.build(context); -} From 0635af78ecbf99c5d2302687b6aafb0695f53103 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Fri, 29 Dec 2023 09:15:13 -0500 Subject: [PATCH 09/18] Updated examples and docs. --- README.md | 4 + example/lib/main-alert-theme.dart | 2 +- example/lib/main-card-theme.dart | 69 +++++++++++++++ example/lib/main-custom-alert.dart | 2 +- example/lib/main-custom-card.dart | 34 +++++++- lib/src/upgrade_card.dart | 133 +++++++++++++++-------------- 6 files changed, 176 insertions(+), 68 deletions(-) create mode 100644 example/lib/main-card-theme.dart diff --git a/README.md b/README.md index dff8c8e4..6c6be291 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,10 @@ The alert can be customized by changing the `DialogTheme` on the `MaterialApp`, - [example/lib/main-alert-theme.dart](example/lib/main-alert-theme.dart) - [example/lib/main-custom-alert.dart](example/lib/main-custom-alert.dart) +The card can be customized by changing the `CardTheme` on the `MaterialApp`, or by overriding methods in the `UpgradeCard` class. See these examples for more details: +- [example/lib/main-card-theme.dart](example/lib/main-card-theme.dart) +- [example/lib/main-custom-card.dart](example/lib/main-custom-card.dart) + Here are the custom parameters for `UpgradeAlert`: * canDismissDialog: can alert dialog be dismissed on tap outside of the alert dialog, which defaults to ```false``` (not used by UpgradeCard) diff --git a/example/lib/main-alert-theme.dart b/example/lib/main-alert-theme.dart index 8326b0ac..54587a3f 100644 --- a/example/lib/main-alert-theme.dart +++ b/example/lib/main-alert-theme.dart @@ -42,7 +42,7 @@ class MyApp extends StatelessWidget { title: 'Upgrader Example', home: UpgradeAlert( child: Scaffold( - appBar: AppBar(title: Text('Upgrader Example')), + appBar: AppBar(title: Text('Upgrader Alert Theme Example')), body: Center(child: Text('Checking...')), )), theme: light, diff --git a/example/lib/main-card-theme.dart b/example/lib/main-card-theme.dart new file mode 100644 index 00000000..b6cf347b --- /dev/null +++ b/example/lib/main-card-theme.dart @@ -0,0 +1,69 @@ +// Copyright (c) 2023 Larry Aasen. All rights reserved. + +import 'package:flutter/material.dart'; +import 'package:upgrader/upgrader.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Only call clearSavedSettings() during testing to reset internal values. + await Upgrader.clearSavedSettings(); // REMOVE this for release builds + + // On Android, the default behavior will be to use the Google Play Store + // version of the app. + // On iOS, the default behavior will be to use the App Store version of + // the app, so update the Bundle Identifier in example/ios/Runner with a + // valid identifier already in the App Store. + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + MyApp({super.key}); + + final dark = ThemeData.dark(useMaterial3: true); + + final light = ThemeData( + cardTheme: CardTheme(color: Colors.greenAccent), + // Change the text buttons. + textButtonTheme: const TextButtonThemeData( + style: ButtonStyle( + // Change the color of the text buttons. + foregroundColor: MaterialStatePropertyAll(Colors.orange), + ), + ), + ); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Upgrader Card Example', + home: Scaffold( + appBar: AppBar(title: Text('Upgrader Card Theme Example')), + body: Container( + margin: EdgeInsets.only(left: 12.0, right: 12.0), + child: SingleChildScrollView( + child: Column( + children: [ + _simpleCard, + _simpleCard, + UpgradeCard(), + _simpleCard, + _simpleCard, + ], + ), + ), + ), + ), + theme: light, + darkTheme: dark, + ); + } + + Widget get _simpleCard => Card( + child: SizedBox( + width: 200, + height: 50, + child: Center(child: Text('Card')), + ), + ); +} diff --git a/example/lib/main-custom-alert.dart b/example/lib/main-custom-alert.dart index 62006c84..71900567 100644 --- a/example/lib/main-custom-alert.dart +++ b/example/lib/main-custom-alert.dart @@ -29,7 +29,7 @@ class MyApp extends StatelessWidget { home: MyUpgradeAlert( upgrader: upgrader, child: Scaffold( - appBar: AppBar(title: Text('Upgrader Example')), + appBar: AppBar(title: Text('Upgrader Custom Alert Example')), body: Center(child: Text('Checking...')), )), ); diff --git a/example/lib/main-custom-card.dart b/example/lib/main-custom-card.dart index 9407bb83..03116bc6 100644 --- a/example/lib/main-custom-card.dart +++ b/example/lib/main-custom-card.dart @@ -23,9 +23,9 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Upgrader Card Example', + title: 'Upgrader Example', home: Scaffold( - appBar: AppBar(title: Text('Upgrader Card Example')), + appBar: AppBar(title: Text('Upgrader Custom Card Example')), body: Container( margin: EdgeInsets.only(left: 12.0, right: 12.0), child: SingleChildScrollView( @@ -55,4 +55,34 @@ class MyApp extends StatelessWidget { class MyUpgradeCard extends UpgradeCard { MyUpgradeCard({super.upgrader}); + + /// Override the [createState] method to provide a custom class + /// with overridden methods. + @override + UpgradeCardState createState() => MyUpgradeCardState(); +} + +class MyUpgradeCardState extends UpgradeCardState { + @override + Widget buildUpgradeCard(BuildContext context) { + final appMessages = widget.upgrader.determineMessages(context); + final title = appMessages.message(UpgraderMessage.title); + return Card( + color: Colors.greenAccent, + child: AlertStyleWidget( + actions: [ + TextButton( + child: Text( + appMessages.message(UpgraderMessage.buttonTitleUpdate) ?? ''), + onPressed: () { + widget.upgrader.saveLastAlerted(); + onUserUpdated(); + }, + ), + ], + content: Text(''), + title: Text(title ?? ''), + ), + ); + } } diff --git a/lib/src/upgrade_card.dart b/lib/src/upgrade_card.dart index b813b877..0ec15e68 100644 --- a/lib/src/upgrade_card.dart +++ b/lib/src/upgrade_card.dart @@ -9,6 +9,10 @@ import 'upgrade_messages.dart'; import 'upgrader.dart'; /// A widget to display the upgrade card. +/// The only reason this is a [StatefulWidget] and not a [StatelessWidget] is that +/// the widget needs to rebulid after one of the buttons have been tapped. +/// Override the [createState] method to provide a custom class +/// with overridden methods. class UpgradeCard extends StatefulWidget { /// Creates a new [UpgradeCard]. UpgradeCard({ @@ -60,10 +64,11 @@ class UpgradeCard extends StatefulWidget { final bool showReleaseNotes; @override - UpgradeCardBaseState createState() => UpgradeCardBaseState(); + UpgradeCardState createState() => UpgradeCardState(); } -class UpgradeCardBaseState extends State { +/// The [UpgradeCard] widget state. +class UpgradeCardState extends State { @override void initState() { super.initState(); @@ -98,17 +103,13 @@ class UpgradeCardBaseState extends State { }); } - /// Build the UpgradeCard Widget. + /// Build the UpgradeCard widget. Widget buildUpgradeCard(BuildContext context) { final appMessages = widget.upgrader.determineMessages(context); final title = appMessages.message(UpgraderMessage.title); final message = widget.upgrader.body(appMessages); final releaseNotes = widget.upgrader.releaseNotes; - final isBlocked = widget.upgrader.blocked(); - final showIgnore = isBlocked ? false : widget.showIgnore; - final showLater = isBlocked ? false : widget.showLater; - if (widget.upgrader.debugLogging) { print('upgrader: UpgradeCard: will display'); print('upgrader: UpgradeCard: showDialog title: $title'); @@ -139,62 +140,66 @@ class UpgradeCardBaseState extends State { } return Card( - // color: Colors.white, - margin: widget.margin, - child: AlertStyleWidget( - title: Text(title ?? ''), - content: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(message), - Padding( - padding: const EdgeInsets.only(top: 15.0), - child: Text( - appMessages.message(UpgraderMessage.prompt) ?? '')), - if (notes != null) notes, - ], - ), - actions: [ - if (showIgnore) - TextButton( - child: Text(appMessages - .message(UpgraderMessage.buttonTitleIgnore) ?? - ''), - onPressed: () { - // Save the date/time as the last time alerted. - widget.upgrader.saveLastAlerted(); - - onUserIgnored(); - forceUpdateState(); - }), - if (showLater) - TextButton( - child: Text( - appMessages.message(UpgraderMessage.buttonTitleLater) ?? - ''), - onPressed: () { - // Save the date/time as the last time alerted. - widget.upgrader.saveLastAlerted(); - - onUserLater(); - forceUpdateState(); - }), - TextButton( - child: Text( - appMessages.message(UpgraderMessage.buttonTitleUpdate) ?? - ''), - onPressed: () { - // Save the date/time as the last time alerted. - widget.upgrader.saveLastAlerted(); - - onUserUpdated(); - }), - ])); + margin: widget.margin, + child: AlertStyleWidget( + title: Text(title ?? ''), + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(message), + Padding( + padding: const EdgeInsets.only(top: 15.0), + child: Text(appMessages.message(UpgraderMessage.prompt) ?? '')), + if (notes != null) notes, + ], + ), + actions: actions(appMessages), + ), + ); } - void forceUpdateState() => setState(() {}); + void forceRebuild() => setState(() {}); + + List actions(UpgraderMessages appMessages) { + final isBlocked = widget.upgrader.blocked(); + final showIgnore = isBlocked ? false : widget.showIgnore; + final showLater = isBlocked ? false : widget.showLater; + return [ + if (showIgnore) + TextButton( + child: Text( + appMessages.message(UpgraderMessage.buttonTitleIgnore) ?? ''), + onPressed: () { + // Save the date/time as the last time alerted. + widget.upgrader.saveLastAlerted(); + + onUserIgnored(); + forceRebuild(); + }), + if (showLater) + TextButton( + child: Text( + appMessages.message(UpgraderMessage.buttonTitleLater) ?? ''), + onPressed: () { + // Save the date/time as the last time alerted. + widget.upgrader.saveLastAlerted(); + + onUserLater(); + forceRebuild(); + }), + TextButton( + child: Text( + appMessages.message(UpgraderMessage.buttonTitleUpdate) ?? ''), + onPressed: () { + // Save the date/time as the last time alerted. + widget.upgrader.saveLastAlerted(); + + onUserUpdated(); + }), + ]; + } bool get shouldDisplayReleaseNotes => widget.showReleaseNotes && @@ -212,7 +217,7 @@ class UpgradeCardBaseState extends State { widget.upgrader.saveIgnored(); } - forceUpdateState(); + forceRebuild(); } void onUserLater() { @@ -223,7 +228,7 @@ class UpgradeCardBaseState extends State { // If this callback has been provided, call it. widget.onLater?.call(); - forceUpdateState(); + forceRebuild(); } void onUserUpdated() { @@ -238,6 +243,6 @@ class UpgradeCardBaseState extends State { widget.upgrader.sendUserToAppStore(); } - forceUpdateState(); + forceRebuild(); } } From e15deb6af07647349106e8c1ae5e507764191341 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Fri, 29 Dec 2023 09:47:06 -0500 Subject: [PATCH 10/18] Improved unit test for shouldPopScope. --- test/upgrader_test.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 323de212..c338a05f 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -495,12 +495,11 @@ void main() { // Pump the UI so the upgrader can display its dialog await tester.pumpAndSettle(); - // Note: his test does not pop scope because there is no way to do that. - // await tester.pageBack(); - // await tester.pumpAndSettle(); - // expect(find.text(upgrader.messages.buttonTitleLater), findsNothing); - expect(called, false); - }, skip: false); + final dynamic widgetsAppState = tester.state(find.byType(WidgetsApp)); + await widgetsAppState.didPopRoute(); + await tester.pump(); + expect(called, true); + }); testWidgets('test UpgradeAlert no update', (WidgetTester tester) async { expect(Upgrader.sharedInstance.isTooSoon(), false); From 63a0e310f3e552ffa19c3f893b2b64b10075b882 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Fri, 29 Dec 2023 09:54:11 -0500 Subject: [PATCH 11/18] Improved unit test for shouldPopScope. --- test/upgrader_test.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index c338a05f..e2b73e7c 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -461,8 +461,10 @@ void main() { testWidgets('test UpgradeAlert pop scope', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); - final upgrader = - Upgrader(upgraderOS: MockUpgraderOS(ios: true), client: client); + final upgrader = Upgrader( + upgraderOS: MockUpgraderOS(ios: true), + client: client, + debugLogging: true); upgrader.installPackageInfo( packageInfo: PackageInfo( From f7f0e7da00b809e61ea2f55d8e0d0d0d074aa7f0 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Fri, 29 Dec 2023 10:18:50 -0500 Subject: [PATCH 12/18] Updated device_info_plus to '>=8.1.0 <10.0.0' --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ebd55c7c..9fec2076 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: sdk: flutter # From fluttercommunity.dev: Get current device information from within the Flutter application. - device_info_plus: any + device_info_plus: '>=8.1.0 <10.0.0' # Pure Dart library for HTML5 parsing html: ^0.15.3 From bd2c9c9ff9393630b09f21b35a0c148402d180a4 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Fri, 5 Jan 2024 08:08:56 -0500 Subject: [PATCH 13/18] Updated custom alert example. Changed currentAppStoreListingURL, currentAppStoreVersion, and currentInstalledVersion from functions to getters. --- CHANGELOG.md | 6 ++++++ example/lib/main-custom-alert.dart | 9 +++++++++ lib/src/upgrader.dart | 10 +++++----- pubspec.yaml | 2 +- test/upgrader_test.dart | 16 ++++++++-------- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ee2ac1..17d43451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,13 @@ +## 9.0.0-alpha.2 + +- Changed currentAppStoreListingURL, currentAppStoreVersion, and currentInstalledVersion from functions to getters. + ## 9.0.0-alpha.1 - BREAKING: Moved UI related code outside of Upgrader and into UpgradeAlert and UpgradeCard. Also, renamed the private methods to make them public. Added and improved example code and README. +- Minimum Dart SDK 3.1.0 +- Minimum Flutter SDK 3.13.1 ## 8.4.0 diff --git a/example/lib/main-custom-alert.dart b/example/lib/main-custom-alert.dart index 71900567..e28fb457 100644 --- a/example/lib/main-custom-alert.dart +++ b/example/lib/main-custom-alert.dart @@ -43,6 +43,15 @@ class MyUpgrader extends Upgrader { bool isTooSoon() { return super.isTooSoon(); } + + @override + bool isUpdateAvailable() { + final appStoreVersion = currentAppStoreVersion; + final installedVersion = currentInstalledVersion; + print('appStoreVersion=$appStoreVersion'); + print('installedVersion=$installedVersion'); + return super.isUpdateAvailable(); + } } class MyUpgradeAlert extends UpgradeAlert { diff --git a/lib/src/upgrader.dart b/lib/src/upgrader.dart index 6cb5ca31..e1f5d763 100644 --- a/lib/src/upgrader.dart +++ b/lib/src/upgrader.dart @@ -148,11 +148,11 @@ class Upgrader with WidgetsBindingObserver { static const notInitializedExceptionMessage = 'upgrader: initialize() not called. Must be called first.'; - String? currentAppStoreListingURL() => _appStoreListingURL; + String? get currentAppStoreListingURL => _appStoreListingURL; - String? currentAppStoreVersion() => _appStoreVersion; + String? get currentAppStoreVersion => _appStoreVersion; - String? currentInstalledVersion() => _installedVersion; + String? get currentInstalledVersion => _installedVersion; String? get releaseNotes => _releaseNotes; @@ -395,9 +395,9 @@ class Upgrader with WidgetsBindingObserver { var msg = messages.message(UpgraderMessage.body)!; msg = msg.replaceAll('{{appName}}', appName()); msg = msg.replaceAll( - '{{currentAppStoreVersion}}', currentAppStoreVersion() ?? ''); + '{{currentAppStoreVersion}}', currentAppStoreVersion ?? ''); msg = msg.replaceAll( - '{{currentInstalledVersion}}', currentInstalledVersion() ?? ''); + '{{currentInstalledVersion}}', currentInstalledVersion ?? ''); return msg; } diff --git a/pubspec.yaml b/pubspec.yaml index 9fec2076..37072d76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: upgrader description: Flutter package for prompting users to upgrade when there is a newer version of the app in the store. -version: 9.0.0-alpha.1 +version: 9.0.0-alpha.2 homepage: https://github.com/larryaasen/upgrader environment: diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index e2b73e7c..045a97e3 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -110,27 +110,27 @@ void main() { expect(await upgrader.initialize(), isTrue); expect(upgrader.appName(), 'Upgrader'); - expect(upgrader.currentAppStoreVersion(), '5.6'); - expect(upgrader.currentInstalledVersion(), '1.9.9'); + expect(upgrader.currentAppStoreVersion, '5.6'); + expect(upgrader.currentInstalledVersion, '1.9.9'); expect(upgrader.isUpdateAvailable(), true); upgrader.installAppStoreVersion('1.2.3'); - expect(upgrader.currentAppStoreVersion(), '1.2.3'); + expect(upgrader.currentAppStoreVersion, '1.2.3'); expect(upgrader.isUpdateAvailable(), false); upgrader.installAppStoreVersion('6.2.3'); - expect(upgrader.currentAppStoreVersion(), '6.2.3'); + expect(upgrader.currentAppStoreVersion, '6.2.3'); expect(upgrader.isUpdateAvailable(), true); upgrader.installAppStoreVersion('1.1.1'); - expect(upgrader.currentAppStoreVersion(), '1.1.1'); + expect(upgrader.currentAppStoreVersion, '1.1.1'); expect(upgrader.isUpdateAvailable(), false); await upgrader.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(upgrader.isUpdateAvailable(), true); upgrader.installAppStoreVersion('1.1.1'); - expect(upgrader.currentAppStoreVersion(), '1.1.1'); + expect(upgrader.currentAppStoreVersion, '1.1.1'); expect(upgrader.isUpdateAvailable(), false); upgrader.installPackageInfo( @@ -160,7 +160,7 @@ void main() { upgrader.installAppStoreListingURL( 'https://itunes.apple.com/us/app/google-maps-transit-food/id585027354?mt=8&uo=4'); - expect(upgrader.currentAppStoreListingURL(), + expect(upgrader.currentAppStoreListingURL, 'https://itunes.apple.com/us/app/google-maps-transit-food/id585027354?mt=8&uo=4'); }, skip: false); @@ -1126,7 +1126,7 @@ void main() { await upgrader.initialize(); expect(upgrader.shouldDisplayUpgrade(), isFalse); expect(upgrader.appName(), isEmpty); - expect(upgrader.currentInstalledVersion(), isEmpty); + expect(upgrader.currentInstalledVersion, isEmpty); }, skip: false); testWidgets('test UpgradeAlert with GoRouter', (WidgetTester tester) async { From 4a7161ca004d32f0e42affc003c3dcc99aaa9707 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Fri, 5 Jan 2024 08:21:34 -0500 Subject: [PATCH 14/18] [371] Added key to alert dialog and alert card. --- CHANGELOG.md | 1 + example/lib/main-custom-alert.dart | 1 + lib/src/upgrade_alert.dart | 17 +++++++++++++---- lib/src/upgrade_card.dart | 6 ++++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d43451..d758ae3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 9.0.0-alpha.2 - Changed currentAppStoreListingURL, currentAppStoreVersion, and currentInstalledVersion from functions to getters. +- [371] Added key to alert dialog and alert card. ## 9.0.0-alpha.1 diff --git a/example/lib/main-custom-alert.dart b/example/lib/main-custom-alert.dart index e28fb457..cbc51b40 100644 --- a/example/lib/main-custom-alert.dart +++ b/example/lib/main-custom-alert.dart @@ -66,6 +66,7 @@ class MyUpgradeAlert extends UpgradeAlert { class MyUpgradeAlertState extends UpgradeAlertState { @override void showTheDialog({ + Key? key, required BuildContext context, required String? title, required String message, diff --git a/lib/src/upgrade_alert.dart b/lib/src/upgrade_alert.dart index e7af06b3..d1d448fc 100644 --- a/lib/src/upgrade_alert.dart +++ b/lib/src/upgrade_alert.dart @@ -208,6 +208,7 @@ class UpgradeAlertState extends State { /// Show the alert dialog. void showTheDialog({ + Key? key = const Key('upgrader_alert_dialog'), required BuildContext context, required String? title, required String message, @@ -231,6 +232,7 @@ class UpgradeAlertState extends State { return WillPopScope( onWillPop: () async => onWillPop(), child: alertDialog( + key, title ?? '', message, releaseNotes, @@ -260,8 +262,14 @@ class UpgradeAlertState extends State { return false; } - Widget alertDialog(String title, String message, String? releaseNotes, - BuildContext context, bool cupertino, UpgraderMessages messages) { + Widget alertDialog( + Key? key, + String title, + String message, + String? releaseNotes, + BuildContext context, + bool cupertino, + UpgraderMessages messages) { // If installed version is below minimum app version, or is a critical update, // disable ignore and later buttons. final isBlocked = widget.upgrader.blocked(); @@ -313,8 +321,9 @@ class UpgradeAlertState extends State { return cupertino ? CupertinoAlertDialog( - title: textTitle, content: content, actions: actions) - : AlertDialog(title: textTitle, content: content, actions: actions); + key: key, title: textTitle, content: content, actions: actions) + : AlertDialog( + key: key, title: textTitle, content: content, actions: actions); } Widget button(bool cupertino, String? text, BuildContext context, diff --git a/lib/src/upgrade_card.dart b/lib/src/upgrade_card.dart index 0ec15e68..369a5b51 100644 --- a/lib/src/upgrade_card.dart +++ b/lib/src/upgrade_card.dart @@ -92,7 +92,8 @@ class UpgradeCardState extends State { snapshot.data != null && snapshot.data!) { if (widget.upgrader.shouldDisplayUpgrade()) { - return buildUpgradeCard(context); + return buildUpgradeCard( + context, const Key('upgrader_alert_card')); } else { if (widget.upgrader.debugLogging) { print('upgrader: UpgradeCard will not display'); @@ -104,7 +105,7 @@ class UpgradeCardState extends State { } /// Build the UpgradeCard widget. - Widget buildUpgradeCard(BuildContext context) { + Widget buildUpgradeCard(BuildContext context, Key? key) { final appMessages = widget.upgrader.determineMessages(context); final title = appMessages.message(UpgraderMessage.title); final message = widget.upgrader.body(appMessages); @@ -140,6 +141,7 @@ class UpgradeCardState extends State { } return Card( + key: key, margin: widget.margin, child: AlertStyleWidget( title: Text(title ?? ''), From ec838bae1232d58246b56fd3b4e2cab0d042079d Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Fri, 5 Jan 2024 08:21:46 -0500 Subject: [PATCH 15/18] [371] Added key to alert dialog and alert card. --- example/lib/main-custom-card.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/main-custom-card.dart b/example/lib/main-custom-card.dart index 03116bc6..57289a65 100644 --- a/example/lib/main-custom-card.dart +++ b/example/lib/main-custom-card.dart @@ -64,7 +64,7 @@ class MyUpgradeCard extends UpgradeCard { class MyUpgradeCardState extends UpgradeCardState { @override - Widget buildUpgradeCard(BuildContext context) { + Widget buildUpgradeCard(BuildContext context, Key? key) { final appMessages = widget.upgrader.determineMessages(context); final title = appMessages.message(UpgraderMessage.title); return Card( From 2ee99bec19fd0baa3315842f4947eaf8ac75cef6 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sat, 6 Jan 2024 09:17:37 -0500 Subject: [PATCH 16/18] Updated unit test. --- test/upgrader_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index 045a97e3..d7720914 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -240,6 +240,7 @@ void main() { expect(find.text(upgrader.messages!.buttonTitleLater), findsOneWidget); expect(find.text(upgrader.messages!.buttonTitleUpdate), findsOneWidget); expect(find.text(upgrader.messages!.releaseNotes), findsOneWidget); + expect(find.byKey(const Key('upgrader_alert_dialog')), findsOneWidget); await tester.tap(find.text(upgrader.messages!.buttonTitleUpdate)); await tester.pumpAndSettle(); From 66f37664d28f0f56b176596a4706da50f374940b Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sun, 14 Jan 2024 10:44:36 -0500 Subject: [PATCH 17/18] [371] Added the parameter `dialogKey` to `UpgraderAlert` that is used by the alert dialog. --- CHANGELOG.md | 4 + example/lib/main-custom-alert.dart | 1 + example/lib/main_dialog_key.dart | 40 ++++++ lib/src/upgrade_alert.dart | 7 +- pubspec.yaml | 2 +- test/test_utils.dart | 12 ++ test/upgrade_card_test.dart | 199 +++++++++++++++++++++++++++++ test/upgrader_test.dart | 191 +-------------------------- 8 files changed, 268 insertions(+), 188 deletions(-) create mode 100644 example/lib/main_dialog_key.dart create mode 100644 test/test_utils.dart create mode 100644 test/upgrade_card_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index d758ae3e..16c9d575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.0.0-alpha.3 + +- [371] Added the parameter `dialogKey` to `UpgraderAlert` that is used by the alert dialog. + ## 9.0.0-alpha.2 - Changed currentAppStoreListingURL, currentAppStoreVersion, and currentInstalledVersion from functions to getters. diff --git a/example/lib/main-custom-alert.dart b/example/lib/main-custom-alert.dart index cbc51b40..8aa45fa8 100644 --- a/example/lib/main-custom-alert.dart +++ b/example/lib/main-custom-alert.dart @@ -78,6 +78,7 @@ class MyUpgradeAlertState extends UpgradeAlertState { context: context, builder: (BuildContext context) { return AlertDialog( + key: key, title: const Text('Update?'), content: const SingleChildScrollView( child: ListBody( diff --git a/example/lib/main_dialog_key.dart b/example/lib/main_dialog_key.dart new file mode 100644 index 00000000..4f4418ce --- /dev/null +++ b/example/lib/main_dialog_key.dart @@ -0,0 +1,40 @@ +// Copyright (c) 2024 Larry Aasen. All rights reserved. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:upgrader/upgrader.dart'; + +final dialogKey = GlobalKey(debugLabel: 'gloabl_upgrader_alert_dialog'); + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Only call clearSavedSettings() during testing to reset internal values. + await Upgrader.clearSavedSettings(); // REMOVE this for release builds + + final log = + () => print('$dialogKey mounted=${dialogKey.currentContext?.mounted}'); + unawaited(Future.delayed(Duration(seconds: 0)).then((value) => log())); + unawaited(Future.delayed(Duration(seconds: 3)).then((value) => log())); + unawaited(Future.delayed(Duration(seconds: 4)).then((value) => log())); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Upgrader Example', + home: UpgradeAlert( + dialogKey: dialogKey, + child: Scaffold( + appBar: AppBar(title: Text('Upgrader Example')), + body: Center(child: Text('Checking...')), + )), + ); + } +} diff --git a/lib/src/upgrade_alert.dart b/lib/src/upgrade_alert.dart index d1d448fc..ec64db7d 100644 --- a/lib/src/upgrade_alert.dart +++ b/lib/src/upgrade_alert.dart @@ -29,6 +29,7 @@ class UpgradeAlert extends StatefulWidget { this.showLater = true, this.showReleaseNotes = true, this.cupertinoButtonTextStyle, + this.dialogKey, this.navigatorKey, this.child, }) : upgrader = upgrader ?? Upgrader.sharedInstance; @@ -71,6 +72,9 @@ class UpgradeAlert extends StatefulWidget { /// [UpgradeDialogStyle.cupertino]. Optional. final TextStyle? cupertinoButtonTextStyle; + /// The [Key] assigned to the dialog when it is shown. + final GlobalKey? dialogKey; + /// For use by the Router architecture as part of the RouterDelegate. final GlobalKey? navigatorKey; @@ -138,6 +142,7 @@ class UpgradeAlertState extends State { Future.delayed(const Duration(milliseconds: 0), () { showTheDialog( + key: widget.dialogKey ?? const Key('upgrader_alert_dialog'), context: context, title: appMessages.message(UpgraderMessage.title), message: widget.upgrader.body(appMessages), @@ -208,7 +213,7 @@ class UpgradeAlertState extends State { /// Show the alert dialog. void showTheDialog({ - Key? key = const Key('upgrader_alert_dialog'), + Key? key, required BuildContext context, required String? title, required String message, diff --git a/pubspec.yaml b/pubspec.yaml index 37072d76..c7e22d07 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: upgrader description: Flutter package for prompting users to upgrade when there is a newer version of the app in the store. -version: 9.0.0-alpha.2 +version: 9.0.0-alpha.3 homepage: https://github.com/larryaasen/upgrader environment: diff --git a/test/test_utils.dart b/test/test_utils.dart new file mode 100644 index 00000000..f5f95110 --- /dev/null +++ b/test/test_utils.dart @@ -0,0 +1,12 @@ +// Copyright (c) 2024 Larry Aasen. All rights reserved. + +import 'package:flutter/material.dart'; + +Widget wrapper(Widget child) { + return MaterialApp( + home: Scaffold( + body: child, + appBar: AppBar(title: const Text('Upgrader test')), + ), + ); +} diff --git a/test/upgrade_card_test.dart b/test/upgrade_card_test.dart new file mode 100644 index 00000000..d84c7f09 --- /dev/null +++ b/test/upgrade_card_test.dart @@ -0,0 +1,199 @@ +// Copyright (c) 2024 Larry Aasen. All rights reserved. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:upgrader/upgrader.dart'; + +import 'mock_itunes_client.dart'; +import 'test_utils.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late SharedPreferences preferences; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + preferences = await SharedPreferences.getInstance(); + }); + + tearDown(() async { + await preferences.clear(); + return true; + }); + testWidgets('test UpgradeCard no update', (WidgetTester tester) async { + expect(Upgrader.sharedInstance.isTooSoon(), false); + + final upgradeCard = wrapper(UpgradeCard()); + await tester.pumpWidget(upgradeCard); + + // Pump the UI + await tester.pumpAndSettle(); + + expect(find.text('IGNORE'), findsNothing); + expect(find.text('LATER'), findsNothing); + expect(find.text('UPDATE'), findsNothing); + expect(find.text('Release Notes'), findsNothing); + }); + + testWidgets('test UpgradeCard upgrade', (WidgetTester tester) async { + final client = MockITunesSearchClient.setupMockClient(); + final upgrader = Upgrader( + upgraderOS: MockUpgraderOS(ios: true), + client: client, + debugLogging: true); + + upgrader.installPackageInfo( + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.larryaasen.upgrader', + version: '0.9.9', + buildNumber: '400')); + upgrader.initialize().then((value) {}); + await tester.pumpAndSettle(); + + expect(upgrader.isTooSoon(), false); + + var called = false; + var notCalled = true; + final upgradeCard = wrapper( + UpgradeCard( + upgrader: upgrader, + onUpdate: () { + called = true; + return true; + }, + onIgnore: () { + notCalled = false; + return true; + }, + onLater: () { + notCalled = false; + }, + ), + ); + await tester.pumpWidget(upgradeCard); + + // Pump the UI so the upgrade card is displayed + await tester.pumpAndSettle(); + + expect(upgrader.messages, isNull); + upgrader.messages = UpgraderMessages(); + expect(upgrader.messages, isNotNull); + + expect(find.text(upgrader.messages!.releaseNotes), findsOneWidget); + expect(find.text(upgrader.releaseNotes!), findsOneWidget); + await tester.tap(find.text(upgrader.messages!.buttonTitleUpdate)); + await tester.pumpAndSettle(); + + expect(called, true); + expect(notCalled, true); + expect(find.text(upgrader.messages!.buttonTitleUpdate), findsNothing); + }, skip: false); + + testWidgets('test UpgradeCard ignore', (WidgetTester tester) async { + final client = MockITunesSearchClient.setupMockClient(); + final upgrader = Upgrader( + upgraderOS: MockUpgraderOS(ios: true), + client: client, + debugLogging: true); + + upgrader.installPackageInfo( + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.larryaasen.upgrader', + version: '0.9.9', + buildNumber: '400')); + upgrader.initialize().then((value) {}); + await tester.pumpAndSettle(); + + expect(upgrader.isTooSoon(), false); + + var called = false; + var notCalled = true; + final upgradeCard = wrapper( + UpgradeCard( + upgrader: upgrader, + onUpdate: () { + notCalled = false; + return true; + }, + onIgnore: () { + called = true; + return true; + }, + onLater: () { + notCalled = false; + }, + ), + ); + await tester.pumpWidget(upgradeCard); + + // Pump the UI so the upgrade card is displayed + await tester.pumpAndSettle(); + + expect(upgrader.messages, isNull); + upgrader.messages = UpgraderMessages(); + expect(upgrader.messages, isNotNull); + + await tester.tap(find.text(upgrader.messages!.buttonTitleIgnore)); + await tester.pumpAndSettle(); + + expect(called, true); + expect(notCalled, true); + expect(find.text(upgrader.messages!.buttonTitleIgnore), findsNothing); + }, skip: false); + + testWidgets('test UpgradeCard later', (WidgetTester tester) async { + final client = MockITunesSearchClient.setupMockClient(); + final upgrader = Upgrader( + upgraderOS: MockUpgraderOS(ios: true), + client: client, + debugLogging: true); + + upgrader.installPackageInfo( + packageInfo: PackageInfo( + appName: 'Upgrader', + packageName: 'com.larryaasen.upgrader', + version: '0.9.9', + buildNumber: '400')); + upgrader.initialize().then((value) {}); + await tester.pumpAndSettle(); + + expect(upgrader.isTooSoon(), false); + + var called = false; + var notCalled = true; + final upgradeCard = wrapper( + UpgradeCard( + upgrader: upgrader, + onUpdate: () { + notCalled = false; + return true; + }, + onIgnore: () { + notCalled = false; + return true; + }, + onLater: () { + called = true; + }, + ), + ); + await tester.pumpWidget(upgradeCard); + + // Pump the UI so the upgrade card is displayed + await tester.pumpAndSettle(const Duration(milliseconds: 5000)); + + expect(upgrader.messages, isNull); + upgrader.messages = UpgraderMessages(); + expect(upgrader.messages, isNotNull); + + await tester.tap(find.text(upgrader.messages!.buttonTitleLater)); + await tester.pumpAndSettle(); + + expect(called, true); + expect(notCalled, true); + expect(find.text(upgrader.messages!.buttonTitleLater), findsNothing); + }, skip: false); +} diff --git a/test/upgrader_test.dart b/test/upgrader_test.dart index d7720914..4b455da2 100644 --- a/test/upgrader_test.dart +++ b/test/upgrader_test.dart @@ -15,6 +15,7 @@ import 'appcast_test.dart'; import 'fake_appcast.dart'; import 'mock_itunes_client.dart'; import 'mock_play_store_client.dart'; +import 'test_utils.dart'; // FYI: Platform.operatingSystem can be "macos" or "linux" in a unit test. // FYI: defaultTargetPlatform is TargetPlatform.android in a unit test. @@ -202,8 +203,10 @@ void main() { var called = false; var notCalled = true; + final dialogKey = GlobalKey(debugLabel: 'gloabl_upgrader_alert_dialog'); final upgradeAlert = wrapper( UpgradeAlert( + dialogKey: dialogKey, upgrader: upgrader, onUpdate: () { called = true; @@ -240,7 +243,7 @@ void main() { expect(find.text(upgrader.messages!.buttonTitleLater), findsOneWidget); expect(find.text(upgrader.messages!.buttonTitleUpdate), findsOneWidget); expect(find.text(upgrader.messages!.releaseNotes), findsOneWidget); - expect(find.byKey(const Key('upgrader_alert_dialog')), findsOneWidget); + expect(find.byKey(dialogKey), findsOneWidget); await tester.tap(find.text(upgrader.messages!.buttonTitleUpdate)); await tester.pumpAndSettle(); @@ -340,6 +343,7 @@ void main() { expect(find.text(upgrader.messages!.buttonTitleIgnore), findsOneWidget); expect(find.text(upgrader.messages!.buttonTitleLater), findsOneWidget); expect(find.text(upgrader.messages!.buttonTitleUpdate), findsOneWidget); + expect(find.byKey(const Key('upgrader_alert_dialog')), findsOneWidget); await tester.tap(find.text(upgrader.messages!.buttonTitleUpdate)); await tester.pumpAndSettle(); @@ -519,182 +523,6 @@ void main() { expect(find.text('Release Notes'), findsNothing); }); - testWidgets('test UpgradeCard no update', (WidgetTester tester) async { - expect(Upgrader.sharedInstance.isTooSoon(), false); - - final upgradeCard = wrapper(UpgradeCard()); - await tester.pumpWidget(upgradeCard); - - // Pump the UI - await tester.pumpAndSettle(); - - expect(find.text('IGNORE'), findsNothing); - expect(find.text('LATER'), findsNothing); - expect(find.text('UPDATE'), findsNothing); - expect(find.text('Release Notes'), findsNothing); - }); - - testWidgets('test UpgradeCard upgrade', (WidgetTester tester) async { - final client = MockITunesSearchClient.setupMockClient(); - final upgrader = Upgrader( - upgraderOS: MockUpgraderOS(ios: true), - client: client, - debugLogging: true); - - upgrader.installPackageInfo( - packageInfo: PackageInfo( - appName: 'Upgrader', - packageName: 'com.larryaasen.upgrader', - version: '0.9.9', - buildNumber: '400')); - upgrader.initialize().then((value) {}); - await tester.pumpAndSettle(); - - expect(upgrader.isTooSoon(), false); - - var called = false; - var notCalled = true; - final upgradeCard = wrapper( - UpgradeCard( - upgrader: upgrader, - onUpdate: () { - called = true; - return true; - }, - onIgnore: () { - notCalled = false; - return true; - }, - onLater: () { - notCalled = false; - }, - ), - ); - await tester.pumpWidget(upgradeCard); - - // Pump the UI so the upgrade card is displayed - await tester.pumpAndSettle(); - - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); - - expect(find.text(upgrader.messages!.releaseNotes), findsOneWidget); - expect(find.text(upgrader.releaseNotes!), findsOneWidget); - await tester.tap(find.text(upgrader.messages!.buttonTitleUpdate)); - await tester.pumpAndSettle(); - - expect(called, true); - expect(notCalled, true); - expect(find.text(upgrader.messages!.buttonTitleUpdate), findsNothing); - }, skip: false); - - testWidgets('test UpgradeCard ignore', (WidgetTester tester) async { - final client = MockITunesSearchClient.setupMockClient(); - final upgrader = Upgrader( - upgraderOS: MockUpgraderOS(ios: true), - client: client, - debugLogging: true); - - upgrader.installPackageInfo( - packageInfo: PackageInfo( - appName: 'Upgrader', - packageName: 'com.larryaasen.upgrader', - version: '0.9.9', - buildNumber: '400')); - upgrader.initialize().then((value) {}); - await tester.pumpAndSettle(); - - expect(upgrader.isTooSoon(), false); - - var called = false; - var notCalled = true; - final upgradeCard = wrapper( - UpgradeCard( - upgrader: upgrader, - onUpdate: () { - notCalled = false; - return true; - }, - onIgnore: () { - called = true; - return true; - }, - onLater: () { - notCalled = false; - }, - ), - ); - await tester.pumpWidget(upgradeCard); - - // Pump the UI so the upgrade card is displayed - await tester.pumpAndSettle(); - - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); - - await tester.tap(find.text(upgrader.messages!.buttonTitleIgnore)); - await tester.pumpAndSettle(); - - expect(called, true); - expect(notCalled, true); - expect(find.text(upgrader.messages!.buttonTitleIgnore), findsNothing); - }, skip: false); - - testWidgets('test UpgradeCard later', (WidgetTester tester) async { - final client = MockITunesSearchClient.setupMockClient(); - final upgrader = Upgrader( - upgraderOS: MockUpgraderOS(ios: true), - client: client, - debugLogging: true); - - upgrader.installPackageInfo( - packageInfo: PackageInfo( - appName: 'Upgrader', - packageName: 'com.larryaasen.upgrader', - version: '0.9.9', - buildNumber: '400')); - upgrader.initialize().then((value) {}); - await tester.pumpAndSettle(); - - expect(upgrader.isTooSoon(), false); - - var called = false; - var notCalled = true; - final upgradeCard = wrapper( - UpgradeCard( - upgrader: upgrader, - onUpdate: () { - notCalled = false; - return true; - }, - onIgnore: () { - notCalled = false; - return true; - }, - onLater: () { - called = true; - }, - ), - ); - await tester.pumpWidget(upgradeCard); - - // Pump the UI so the upgrade card is displayed - await tester.pumpAndSettle(const Duration(milliseconds: 5000)); - - expect(upgrader.messages, isNull); - upgrader.messages = UpgraderMessages(); - expect(upgrader.messages, isNotNull); - - await tester.tap(find.text(upgrader.messages!.buttonTitleLater)); - await tester.pumpAndSettle(); - - expect(called, true); - expect(notCalled, true); - expect(find.text(upgrader.messages!.buttonTitleLater), findsNothing); - }, skip: false); - testWidgets('test upgrader minAppVersion', (WidgetTester tester) async { final client = MockITunesSearchClient.setupMockClient(); final upgrader = Upgrader( @@ -1254,12 +1082,3 @@ class MyUpgraderMessages extends UpgraderMessages { @override String get releaseNotes => 'ddd'; } - -Widget wrapper(Widget child) { - return MaterialApp( - home: Scaffold( - body: child, - appBar: AppBar(title: const Text('Upgrader test')), - ), - ); -} From b3cbd836ea30bfe5958af8c143fc398fa6d85358 Mon Sep 17 00:00:00 2001 From: Larry Aasen Date: Sun, 14 Jan 2024 18:37:47 -0500 Subject: [PATCH 18/18] Updated examples. --- example/lib/main-custom-alert.dart | 5 --- example/lib/main_alert_theme.dart | 59 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 example/lib/main_alert_theme.dart diff --git a/example/lib/main-custom-alert.dart b/example/lib/main-custom-alert.dart index 8aa45fa8..0aff7224 100644 --- a/example/lib/main-custom-alert.dart +++ b/example/lib/main-custom-alert.dart @@ -9,11 +9,6 @@ void main() async { // Only call clearSavedSettings() during testing to reset internal values. await Upgrader.clearSavedSettings(); // REMOVE this for release builds - // On Android, the default behavior will be to use the Google Play Store - // version of the app. - // On iOS, the default behavior will be to use the App Store version of - // the app, so update the Bundle Identifier in example/ios/Runner with a - // valid identifier already in the App Store. runApp(MyApp()); } diff --git a/example/lib/main_alert_theme.dart b/example/lib/main_alert_theme.dart new file mode 100644 index 00000000..5137003e --- /dev/null +++ b/example/lib/main_alert_theme.dart @@ -0,0 +1,59 @@ +// Copyright (c) 2024 Larry Aasen. All rights reserved. + +import 'package:flutter/material.dart'; +import 'package:upgrader/upgrader.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Only call clearSavedSettings() during testing to reset internal values. + await Upgrader.clearSavedSettings(); // REMOVE this for release builds + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Upgrader Example', + home: MyUpgradeAlert( + child: Scaffold( + appBar: AppBar(title: Text('Upgrader Alert Theme Example')), + body: Center(child: Text('Checking...')), + )), + ); + } +} + +class MyUpgradeAlert extends UpgradeAlert { + MyUpgradeAlert({super.upgrader, super.child}); + + /// Override the [createState] method to provide a custom class + /// with overridden methods. + @override + UpgradeAlertState createState() => MyUpgradeAlertState(); +} + +class MyUpgradeAlertState extends UpgradeAlertState { + @override + Widget alertDialog( + Key? key, + String title, + String message, + String? releaseNotes, + BuildContext context, + bool cupertino, + UpgraderMessages messages) { + return Theme( + data: ThemeData( + dialogTheme: DialogTheme( + titleTextStyle: TextStyle(color: Colors.red, fontSize: 48.0)), + ), + child: super.alertDialog( + key, title, message, releaseNotes, context, cupertino, messages), + ); + } +}