Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ env:
FLUTTER_VERSION: '3.41.7'

jobs:

analyze:
runs-on: ubuntu-latest
if: github.event.repository.private == false
Expand All @@ -29,6 +28,18 @@ jobs:
- run: flutter pub get
- run: flutter analyze --fatal-infos

test:
runs-on: ubuntu-latest
if: github.event.repository.private == false
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: ${{env.FLUTTER_VERSION}}
- run: flutter pub get
- run: flutter test

format:
runs-on: ubuntu-latest
if: github.event.repository.private == false
Expand Down Expand Up @@ -103,9 +114,8 @@ jobs:
id: lychee
uses: lycheeverse/lychee-action@v2
with: # Don't fail for now but then create an issue - useful?
args:
--exclude-file .lycheeignore
--no-progress
# 20260909 jesscmoore .lycheeignore excluded by default
args: --no-progress
'*.md'
'./**/*.dart'
'assets/**/*.md'
Expand Down
4 changes: 4 additions & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,7 @@ https://opensource.org/license/gpl-3-0
http://xmlns.com/foaf/0.1/
http://purl.org/dc/terms/
https://solidcommunity.au/predicates/*

# Example app changelog

https://raw.githubusercontent.com/anusii/version_widget/refs/heads/main/NO_SUCH_FILE.md
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ utilised by the flutter version_widget package.

## 1.1 Review and Consolidate

+ Report failed checks rather than assuming latest [1.1.0 20260909 jesscmoore]
+ Restore version string colours for status [1.0.10 20260512 tonypioneer]
+ Add an UPDATE button [1.0.9 20260510 tonypioneer]
+ Better tooltip formatting [1.0.8 20260429 gjw]
+ Tooltip headline rather than paragraph [1.0.7 20260429 gjw]
+ Better CHANGELOG. Rosolve CORS issue [1.0.6 20251027 tonypioneer]
+ Better CHANGELOG. Resolve CORS issue [1.0.6 20251027 tonypioneer]
+ Update tooltip text [1.0.5 20250928 gjw]
+ Support user text style for Version string [1.0.4 20250722 jesscmoore]
+ Support VersionWidget fontSize [1.0.3 20250717 gjw]
Expand All @@ -34,7 +35,7 @@ utilised by the flutter version_widget package.
+ Fixed version date display [0.0.7 20250429 kev]
+ Improved parsing extract all version-date pairs [0.0.7 20250429 kev]
+ Find the correct date for each version [0.0.7 20250429 kev]
+ Updated doc to reflect the new date matching behavior [0.0.7 20250429 kev]
+ Updated doc to reflect the new date matching behaviour [0.0.7 20250429 kev]
+ Made version parameter required in VersionWidget [0.0.6 20250428 kev]
+ Removed version extraction from changelog [0.0.6 20250428 kev]
+ Simplified changelog fetching logic [0.0.6 20250428 kev]
Expand Down
105 changes: 101 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,24 @@ VersionWidget(
- Grey text: Version is being checked
- Blue text: Version is up to date
- Red bold text: Newer version is available
- No date shown: Internet connection unavailable
- Amber text: The version could not be checked (eg unpublished changelog, CORS
block to changelog, app with no version)

A failed check is reported and does not offer an app update button. Pass
`assumeLatestOnCheckFailure: true` to restore the older, quieter behaviour.

Apps that do not report their own version — an empty
or non-numeric `version` are also now reported as a failed check.

## CHANGELOG.md Format

The widget expects the CHANGELOG.md file to have dates in the
following format. The important part is `[1.0.5 20250101` and the
first such text found is interpreted as the latest version and
timestamp. This allows, for example, the string to be `[1.0.5 20250514
fred]` as a common format to attribute changes to users.
timestamp. An author may sit on either side of the date, so both
`[1.0.5 20250514 fred]` and `[1.0.5 fred 20250514]` are read correctly.
The first form is the convention across our apps; the second is
tolerated so an app is not silently unversioned for writing it.

```markdown
## [1.0.5 20250101]
Expand All @@ -128,6 +137,55 @@ The widget will automatically find the correct release date for the
current version by matching against all version entries in the
changelog.

## Private repositories

The CHANGELOG must be published, as the widget fetches the CHANGELOG with a
plain, unauthenticated GET.

Developers with private app repositories are recommended to publish their
changelog to the same origin as the web app, to avoid CORS block issues. Ie
build your web app and then add publish CHANGELOG file.

```make
flutter build web --release
cp CHANGELOG.md build/web/CHANGELOG.md # after the build, not in web/
```

Copy it after the build keeps the file out of anything Flutter
generates from `web/` to prevent caching issues.

When the changelog genuinely cannot be made public, supply a
`changelogLoader` and fetch it yourself:

```dart
// From an authenticated backend, using a token the app already holds
// from the signed in session.

VersionWidget(
version: '1.0.5',
changelogUrl: 'https://api.example.com/changelog',
changelogLoader: (url) async {
final response = await http.get(
Uri.parse(url),
headers: {'Authorization': 'Bearer ${session.accessToken}'},
);
return response.body;
},
)

// Or bundled with the build. This populates the changelog dialogue but
// can never detect an update, since it is frozen at build time.

VersionWidget(
version: '1.0.5',
changelogUrl: 'asset',
changelogLoader: (_) => rootBundle.loadString('assets/CHANGELOG.md'),
)
```

Never compile a long lived credential. Use a token the user's own
session already provides, or make the changelog public.

## Properties

- `version` (required): The version string to display. Must be provided.
Expand All @@ -137,9 +195,21 @@ changelog.
is still consulted so the optional update button can still appear.
- `showDate` (optional): Whether to show the release date (defaults to true)
- `defaultDate` (optional): Default date to show if changelog cannot
be fetched (format: YYYYMMDD)
be fetched (format: `YYYYMMDD`)
- `isLatestTooltip` (optional): Custom message to show when version is latest
- `notLatestTooltip` (optional): Custom message to show when newer version is available
- `unknownTooltip` (optional): Custom message to show when the check could
not be completed
- `unknownColor` (optional): Colour of the version label when the check
could not be completed (defaults to a muted amber). Applied on top of
`userTextStyle` too, so pick one that stays legible on your background.
- `assumeLatestOnCheckFailure` (optional): Report a failed check as up to
date, as releases before 1.1.0 did (defaults to false)
- `changelogLoader` (optional): Supplies the CHANGELOG text instead of the
built-in HTTP GET. See Private repositories above.
- `onUpdatePressed` (optional): Called instead of launching `downloadUrl`
when the update button is tapped. Useful for a web app, where the update
is a reload rather than an installer.
- `showUpdateButton` (optional): Whether to show the discover-and-download
button when a newer version is detected (defaults to false). The
button is only rendered when this flag is enabled, a newer version
Expand All @@ -150,6 +220,33 @@ changelog.
- `updateButtonLabel` (optional): Text label shown next to the icon on
the update button (defaults to `Update`).

## Platform setup

### MacOS/iOS

MacOS and iOS builds of apps using version widget require these settings to pick
up the app version, which is used to compare against the changelog

In `Runner/Info.plist` within `macos` and `ios` folders, set:

<!-- Tabs below are verbatim from Info.plist, so keep them as tabs. -->
<!-- markdownlint-disable MD010 -->

```xml
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
```

<!-- markdownlint-enable MD010 -->

If using `xcodegen` to generate XCode files, your `macos` and `ios`
`project.yml` files must contain:

```yml
MARKETING_VERSION: '$(FLUTTER_BUILD_NAME)'
CURRENT_PROJECT_VERSION: '$(FLUTTER_BUILD_NUMBER)'
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
Expand Down
1 change: 1 addition & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ analyzer:
exclude:
- ignore/**
- ignore/
- build/**
3 changes: 3 additions & 0 deletions example/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
analyzer:
exclude:
- build/**
include: package:flutter_lints/flutter.yaml

linter:
Expand Down
29 changes: 29 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,35 @@ class MyHomePage extends StatelessWidget {
),
),
),

// Example 2b: A CHANGELOG that cannot be reached. Before
// 1.1.0 this rendered exactly like Example 1 — blue, and
// silently wrong. It now says that nothing is known.
Card(
margin: EdgeInsets.all(8),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Text(
'A changelog URL that 404s.\n'
'Expect an amber version and no date.\n'
'No Update button: no update is known of.\n'
'Hover to see why the check did not complete.',
),
SizedBox(height: 8),
VersionWidget(
version: '1.0.2',
changelogUrl:
'https://raw.githubusercontent.com/anusii/version_widget/refs/heads/main/NO_SUCH_FILE.md',
showUpdateButton: true,
downloadUrl:
'https://github.com/anusii/version_widget/releases/latest',
),
],
),
),
),
],
),
),
Expand Down
111 changes: 111 additions & 0 deletions lib/src/models/version_status.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/// The outcome of a version check.
///
// Time-stamp: <Wednesday 2026-09-09 09:24:05 +1000 Jess Moore>
///
/// Copyright (C) 2024-2026, Software Innovation Institute, ANU.
///
/// Licensed under the MIT License (the "License").
///
/// License: https://choosealicense.com/licenses/mit/.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
///
/// Authors: Jess Moore

library;

import 'package:flutter/material.dart';

/// The outcome of checking the installed version against a CHANGELOG.
///
/// The distinction that matters is between [current] and [unknown]. Before
/// version 1.1.0 a failed or unparsable check was reported as though the
/// installed version were the latest, so an app whose CHANGELOG had moved,
/// gone private, or changed format claimed to be up to date indefinitely.

enum VersionStatus {
/// The CHANGELOG request is still in flight.

checking,

/// No changelog URL was configured, so no check was attempted. This is a
/// legitimate configuration, not a failure, and renders exactly as it did
/// before the status model was introduced.

unchecked,

/// The installed version matches or exceeds the newest CHANGELOG entry.

current,

/// The CHANGELOG advertises a release newer than the installed version.

outdated,

/// The check could not be completed: the transport failed, the response
/// was not usable, or the body held no parsable version entries. Nothing
/// is known about whether a newer release exists.

unknown,
}

/// How each [VersionStatus] presents itself in the version label.

extension VersionStatusDisplay on VersionStatus {
/// The colour of the version label when the host supplies no text style.
///
/// [unknownColour] is passed in rather than hard-coded so the host can
/// choose a shade that stays legible against its own background.

Color colourWith(Color unknownColour) {
switch (this) {
case VersionStatus.checking:
return Colors.grey;
case VersionStatus.unchecked:
case VersionStatus.current:
return Colors.blue;
case VersionStatus.outdated:
return Colors.red;
case VersionStatus.unknown:
return unknownColour;
}
}

/// The weight of the version label when the host supplies no text style.
///
/// Only [VersionStatus.outdated] is bold. An unknown status is an absence
/// of information rather than an alarm, so it is not escalated to bold.

FontWeight get weight =>
this == VersionStatus.outdated ? FontWeight.bold : FontWeight.normal;

/// Whether a release date may accompany the version for this status.
///
/// A date is only meaningful once the CHANGELOG has actually been read.

bool get showsDate =>
this == VersionStatus.current || this == VersionStatus.outdated;

/// Whether the discover-and-download button may be rendered.
///
/// Only when a newer release is known to exist. Offering the button on an
/// [VersionStatus.unknown] check would assert an update we cannot see.

bool get allowsUpdateButton => this == VersionStatus.outdated;
}
Loading
Loading