diff --git a/packages/package_info_plus/package_info_plus/example/.metadata b/packages/package_info_plus/package_info_plus/example/.metadata index 21bd4e0bdc..aa90aa8049 100644 --- a/packages/package_info_plus/package_info_plus/example/.metadata +++ b/packages/package_info_plus/package_info_plus/example/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - channel: stable + revision: "ba393198430278b6595976de84fe170f553cc728" + channel: "stable" project_type: app @@ -13,11 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: android + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: ios + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: linux + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: macos + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: web + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 - platform: windows - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 # User provided section diff --git a/packages/package_info_plus/package_info_plus/example/build.yaml b/packages/package_info_plus/package_info_plus/example/build.yaml new file mode 100644 index 0000000000..f0808f935f --- /dev/null +++ b/packages/package_info_plus/package_info_plus/example/build.yaml @@ -0,0 +1,13 @@ +targets: + $default: + sources: + - $package$ + - lib/$lib$ + - lib/**.dart + - test/**.dart + - integration_test/**.dart + builders: + mockito|mockBuilder: + generate_for: + - test/**.dart + - integration_test/**.dart \ No newline at end of file diff --git a/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_web_test.dart b/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_web_test.dart index 294b8a9a45..e01210bc47 100644 --- a/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_web_test.dart +++ b/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_web_test.dart @@ -2,7 +2,9 @@ library package_info_plus_web_test; import 'dart:convert'; +import 'dart:ui_web' as ui_web; +import 'package:clock/clock.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:integration_test/integration_test.dart'; @@ -13,7 +15,7 @@ import 'package:package_info_plus/src/package_info_plus_web.dart'; import 'package_info_plus_test.dart' as common_tests; import 'package_info_plus_web_test.mocks.dart'; -@GenerateMocks([http.Client]) +@GenerateMocks([http.Client, ui_web.AssetManager]) void main() { common_tests.main(); @@ -29,17 +31,28 @@ void main() { 'build_signature': '', }; + // ignore: constant_identifier_names + const VERSION_2_JSON = { + 'app_name': 'package_info_example', + 'build_number': '2', + 'package_name': 'io.flutter.plugins.packageinfoexample', + 'version': '2.0', + 'installerStore': null, + 'build_signature': '', + }; + late PackageInfoPlusWebPlugin plugin; late MockClient client; - - setUp(() { - client = MockClient(); - plugin = PackageInfoPlusWebPlugin(client); - }); + late MockAssetManager assetManagerMock; group( 'Package Info Web', () { + setUp(() { + client = MockClient(); + plugin = PackageInfoPlusWebPlugin(client); + }); + testWidgets( 'Get correct values when response status is 200', (tester) async { @@ -78,6 +91,35 @@ void main() { }, ); + testWidgets( + 'Get correct values when using a custom base URL', + (tester) async { + const String baseUrl = 'https://www.example.com/'; + final DateTime now = DateTime.now(); + final Clock fakeClock = Clock(() => now); + + await withClock(fakeClock, () async { + final int cache = now.millisecondsSinceEpoch; + + when(client.get( + Uri.parse('${baseUrl}version.json?cachebuster=$cache'), + )).thenAnswer( + (_) => Future.value( + http.Response(jsonEncode(VERSION_JSON), 200), + ), + ); + + final versionMap = await plugin.getAll(baseUrl: baseUrl); + + expect(versionMap.appName, VERSION_JSON['app_name']); + expect(versionMap.version, VERSION_JSON['version']); + expect(versionMap.buildNumber, VERSION_JSON['build_number']); + expect(versionMap.packageName, VERSION_JSON['package_name']); + expect(versionMap.buildSignature, VERSION_JSON['build_signature']); + }); + }, + ); + testWidgets( 'Get correct versionJsonUrl for http and https', (tester) async { @@ -177,4 +219,89 @@ void main() { }); }, ); + + group('Package Info Web (using MockAssetManager)', () { + setUp(() { + client = MockClient(); + assetManagerMock = MockAssetManager(); + plugin = PackageInfoPlusWebPlugin(client, assetManagerMock); + }); + + testWidgets( + 'Get correct values when using the AssetManager baseUrl', + (tester) async { + const String baseUrl = 'https://an.example.com/using-asset-manager/'; + const String assetsDir = 'assets'; + final DateTime now = DateTime.now(); + final Clock fakeClock = Clock(() => now); + + when(assetManagerMock.assetsDir).thenReturn(assetsDir); + when(assetManagerMock.getAssetUrl('')) + .thenReturn('$baseUrl$assetsDir/'); + + await withClock(fakeClock, () async { + final int cache = now.millisecondsSinceEpoch; + + when(client.get( + Uri.parse('${baseUrl}version.json?cachebuster=$cache'), + )).thenAnswer( + (_) => Future.value( + http.Response(jsonEncode(VERSION_JSON), 200), + ), + ); + + final versionMap = await plugin.getAll(); + + expect(versionMap.appName, VERSION_JSON['app_name']); + expect(versionMap.version, VERSION_JSON['version']); + expect(versionMap.buildNumber, VERSION_JSON['build_number']); + expect(versionMap.packageName, VERSION_JSON['package_name']); + expect(versionMap.buildSignature, VERSION_JSON['build_signature']); + }); + }, + ); + + testWidgets( + 'Has preference for the custom base URL over the other 2 locations', + (tester) async { + const String customBaseUrl = 'https://www.example.com/with-path/'; + const String managerBaseUrl = 'https://www.asset-manager.com/path/'; + const String assetsDir = 'assets'; + final DateTime now = DateTime.now(); + final Clock fakeClock = Clock(() => now); + + when(assetManagerMock.assetsDir).thenReturn(assetsDir); + when(assetManagerMock.getAssetUrl('')) + .thenReturn('$managerBaseUrl$assetsDir/'); + + await withClock(fakeClock, () async { + final int cache = now.millisecondsSinceEpoch; + + when(client.get( + Uri.parse('${customBaseUrl}version.json?cachebuster=$cache'), + )).thenAnswer( + (_) => Future.value( + http.Response(jsonEncode(VERSION_JSON), 200), + ), + ); + + when(client.get( + Uri.parse('${managerBaseUrl}version.json?cachebuster=$cache'), + )).thenAnswer( + (_) => Future.value( + http.Response(jsonEncode(VERSION_2_JSON), 200), + ), + ); + + final versionMap = await plugin.getAll(baseUrl: customBaseUrl); + + expect(versionMap.appName, VERSION_JSON['app_name']); + expect(versionMap.version, VERSION_JSON['version']); + expect(versionMap.buildNumber, VERSION_JSON['build_number']); + expect(versionMap.packageName, VERSION_JSON['package_name']); + expect(versionMap.buildSignature, VERSION_JSON['build_signature']); + }); + }, + ); + }); } diff --git a/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_web_test.mocks.dart b/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_web_test.mocks.dart index 33a76e432c..bd10e30c8e 100644 --- a/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_web_test.mocks.dart +++ b/packages/package_info_plus/package_info_plus/example/integration_test/package_info_plus_web_test.mocks.dart @@ -1,108 +1,350 @@ -// Mocks generated by Mockito 5.0.14 from annotations -// in package_info_plus_web/test/package_info_plus_web_test_disabled.dart. +// Mocks generated by Mockito 5.4.4 from annotations +// in package_info_plus_example/integration_test/package_info_plus_web_test.dart. // Do not manually edit this file. -// ignore_for_file: camel_case_types, unnecessary_overrides +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; +import 'dart:convert' as _i4; +import 'dart:typed_data' as _i6; +import 'dart:ui_web' as _i7; -import 'dart:async' as i5; -import 'dart:convert' as i6; -import 'dart:typed_data' as i7; - -import 'package:http/src/base_request.dart' as i8; -import 'package:http/src/client.dart' as i4; -import 'package:http/src/response.dart' as i2; -import 'package:http/src/streamed_response.dart' as i3; -import 'package:mockito/mockito.dart' as i1; +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i5; +// ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeResponse_0 extends i1.Fake implements i2.Response {} +class _FakeStreamedResponse_1 extends _i1.SmartFake + implements _i2.StreamedResponse { + _FakeStreamedResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeStreamedResponse_1 extends i1.Fake implements i3.StreamedResponse {} +class _FakeObject_2 extends _i1.SmartFake implements Object { + _FakeObject_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} /// A class which mocks [Client]. /// /// See the documentation for Mockito's code generation for more information. -class MockClient extends i1.Mock implements i4.Client { +class MockClient extends _i1.Mock implements _i2.Client { MockClient() { - i1.throwOnMissingStub(this); + _i1.throwOnMissingStub(this); } @override - i5.Future head(Uri? url, {Map? headers}) => - (super.noSuchMethod(Invocation.method(#head, [url], {#headers: headers}), - returnValue: Future.value(_FakeResponse_0())) - as i5.Future); + _i3.Future<_i2.Response> head( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + @override - i5.Future get(Uri? url, {Map? headers}) => - (super.noSuchMethod(Invocation.method(#get, [url], {#headers: headers}), - returnValue: Future.value(_FakeResponse_0())) - as i5.Future); + _i3.Future<_i2.Response> get( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + @override - i5.Future post(Uri? url, - {Map? headers, - Object? body, - i6.Encoding? encoding}) => + _i3.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => (super.noSuchMethod( - Invocation.method(#post, [url], - {#headers: headers, #body: body, #encoding: encoding}), - returnValue: Future.value(_FakeResponse_0())) - as i5.Future); + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + @override - i5.Future put(Uri? url, - {Map? headers, - Object? body, - i6.Encoding? encoding}) => + _i3.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => (super.noSuchMethod( - Invocation.method(#put, [url], - {#headers: headers, #body: body, #encoding: encoding}), - returnValue: Future.value(_FakeResponse_0())) - as i5.Future); + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + @override - i5.Future patch(Uri? url, - {Map? headers, - Object? body, - i6.Encoding? encoding}) => + _i3.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => (super.noSuchMethod( - Invocation.method(#patch, [url], - {#headers: headers, #body: body, #encoding: encoding}), - returnValue: Future.value(_FakeResponse_0())) - as i5.Future); + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + @override - i5.Future delete(Uri? url, - {Map? headers, - Object? body, - i6.Encoding? encoding}) => + _i3.Future read( + Uri? url, { + Map? headers, + }) => (super.noSuchMethod( - Invocation.method(#delete, [url], - {#headers: headers, #body: body, #encoding: encoding}), - returnValue: Future.value(_FakeResponse_0())) - as i5.Future); + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future.value(_i5.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future); + @override - i5.Future read(Uri? url, {Map? headers}) => - (super.noSuchMethod(Invocation.method(#read, [url], {#headers: headers}), - returnValue: Future.value('')) as i5.Future); + _i3.Future<_i6.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #readBytes, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + ) as _i3.Future<_i6.Uint8List>); + @override - i5.Future readBytes(Uri? url, {Map? headers}) => + _i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => (super.noSuchMethod( - Invocation.method(#readBytes, [url], {#headers: headers}), - returnValue: Future.value(i7.Uint8List(0))) - as i5.Future); + Invocation.method( + #send, + [request], + ), + returnValue: + _i3.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + this, + Invocation.method( + #send, + [request], + ), + )), + ) as _i3.Future<_i2.StreamedResponse>); + @override - i5.Future send(i8.BaseRequest? request) => - (super.noSuchMethod(Invocation.method(#send, [request]), - returnValue: - Future.value(_FakeStreamedResponse_1())) - as i5.Future); + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [AssetManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAssetManager extends _i1.Mock implements _i7.AssetManager { + MockAssetManager() { + _i1.throwOnMissingStub(this); + } + + @override + String get assetsDir => (super.noSuchMethod( + Invocation.getter(#assetsDir), + returnValue: _i5.dummyValue( + this, + Invocation.getter(#assetsDir), + ), + ) as String); + @override - void close() => super.noSuchMethod(Invocation.method(#close, []), - returnValueForMissingStub: null); + String getAssetUrl(String? asset) => (super.noSuchMethod( + Invocation.method( + #getAssetUrl, + [asset], + ), + returnValue: _i5.dummyValue( + this, + Invocation.method( + #getAssetUrl, + [asset], + ), + ), + ) as String); + + @override + _i3.Future loadAsset(String? asset) => (super.noSuchMethod( + Invocation.method( + #loadAsset, + [asset], + ), + returnValue: _i3.Future.value(_FakeObject_2( + this, + Invocation.method( + #loadAsset, + [asset], + ), + )), + ) as _i3.Future); + @override - String toString() => super.toString(); + _i3.Future<_i6.ByteData> load(String? asset) => (super.noSuchMethod( + Invocation.method( + #load, + [asset], + ), + returnValue: _i3.Future<_i6.ByteData>.value(_i6.ByteData(0)), + ) as _i3.Future<_i6.ByteData>); } diff --git a/packages/package_info_plus/package_info_plus/example/pubspec.yaml b/packages/package_info_plus/package_info_plus/example/pubspec.yaml index c76b8dc829..da6fd3668f 100644 --- a/packages/package_info_plus/package_info_plus/example/pubspec.yaml +++ b/packages/package_info_plus/package_info_plus/example/pubspec.yaml @@ -7,6 +7,7 @@ environment: sdk: '>=2.18.0 <4.0.0' dependencies: + clock: ^1.1.1 flutter: sdk: flutter http: ">=0.13.5 <2.0.0" diff --git a/packages/package_info_plus/package_info_plus/example/web/icons/Icon-maskable-192.png b/packages/package_info_plus/package_info_plus/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000..eb9b4d76e5 Binary files /dev/null and b/packages/package_info_plus/package_info_plus/example/web/icons/Icon-maskable-192.png differ diff --git a/packages/package_info_plus/package_info_plus/example/web/icons/Icon-maskable-512.png b/packages/package_info_plus/package_info_plus/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000..d69c56691f Binary files /dev/null and b/packages/package_info_plus/package_info_plus/example/web/icons/Icon-maskable-512.png differ diff --git a/packages/package_info_plus/package_info_plus/example/web/index.html b/packages/package_info_plus/package_info_plus/example/web/index.html index a0c14606fc..45cf2ca304 100644 --- a/packages/package_info_plus/package_info_plus/example/web/index.html +++ b/packages/package_info_plus/package_info_plus/example/web/index.html @@ -1,6 +1,21 @@ + + + @@ -16,18 +31,29 @@ example + + + + - - diff --git a/packages/package_info_plus/package_info_plus/lib/package_info_plus.dart b/packages/package_info_plus/package_info_plus/lib/package_info_plus.dart index 5cf05c6179..df0e9e1b40 100644 --- a/packages/package_info_plus/package_info_plus/lib/package_info_plus.dart +++ b/packages/package_info_plus/package_info_plus/lib/package_info_plus.dart @@ -33,12 +33,54 @@ class PackageInfo { /// Retrieves package information from the platform. /// The result is cached. - static Future fromPlatform() async { + /// + /// The [baseUrl] parameter is for web use only and the other platforms will + /// ignore it. + /// + /// ## Web platform + /// + /// In a web environment, the package uses the `version.json` file that it is + /// generated in the build process. + /// + /// The package will try to locate this file in 3 ways: + /// + /// * If you provide the optional custom [baseUrl] parameter, it will be + /// used as the first option where to search. Example: + /// + /// ```dart + /// await PackageInfo.fromPlatform(baseUrl: 'https://cdn.domain.com/with/some/path/'); + /// ``` + /// + /// With this, the package will try to search the file in `https://cdn.domain.com/with/some/path/version.json` + /// + /// * The second option where it will search is the [assetBase] parameter + /// that you can pass to the Flutter Web Engine when you initialize it. + /// + /// ```javascript + /// _flutter.loader.loadEntrypoint({ + /// onEntrypointLoaded: async function(engineInitializer) { + /// let appRunner = await engineInitializer.initializeEngine({ + /// assetBase: "https://cdn.domain.com/with/some/path/" + /// }); + /// appRunner.runApp(); + /// } + /// }); + /// ``` + /// + /// For more information about the Flutter Web Engine initialization see here: + /// https://docs.flutter.dev/platform-integration/web/initialization#initializing-the-engine + /// + /// * Finally, if none of the previous locations return the `version.json` file, + /// the package will use the browser window base URL to resolve its location. + static Future fromPlatform({String? baseUrl}) async { if (_fromPlatform != null) { return _fromPlatform!; } - final platformData = await PackageInfoPlatform.instance.getAll(); + final platformData = await PackageInfoPlatform.instance.getAll( + baseUrl: baseUrl, + ); + _fromPlatform = PackageInfo( appName: platformData.appName, packageName: platformData.packageName, diff --git a/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_linux.dart b/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_linux.dart index 4cf589a518..581d648a8d 100644 --- a/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_linux.dart +++ b/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_linux.dart @@ -15,7 +15,7 @@ class PackageInfoPlusLinuxPlugin extends PackageInfoPlatform { /// Returns a map with the following keys: /// appName, packageName, version, buildNumber @override - Future getAll() async { + Future getAll({String? baseUrl}) async { final versionJson = await _getVersionJson(); return PackageInfoData( appName: versionJson['app_name'] ?? '', diff --git a/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_web.dart b/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_web.dart index 093b0cebaf..0be152e430 100644 --- a/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_web.dart +++ b/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_web.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'dart:ui_web'; +import 'package:clock/clock.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:http/http.dart'; import 'package:package_info_plus_platform_interface/package_info_data.dart'; @@ -11,9 +13,11 @@ import 'package:web/web.dart' as web; /// This class implements the `package:package_info_plus` functionality for the web. class PackageInfoPlusWebPlugin extends PackageInfoPlatform { final Client? _client; + final AssetManager _assetManager; - /// Create plugin with http client. - PackageInfoPlusWebPlugin([this._client]); + /// Create plugin with http client and asset manager for testing purposes. + PackageInfoPlusWebPlugin([this._client, AssetManager? assetManagerMock]) + : _assetManager = assetManagerMock ?? assetManager; /// Registers this class as the default instance of [PackageInfoPlatform]. static void registerWith(Registrar registrar) { @@ -49,11 +53,13 @@ class PackageInfoPlusWebPlugin extends PackageInfoPlatform { } @override - Future getAll() async { - final cacheBuster = DateTime.now().millisecondsSinceEpoch; - final url = versionJsonUrl(web.window.document.baseURI, cacheBuster); - final response = _client == null ? await get(url) : await _client.get(url); - final versionMap = _getVersionMap(response); + Future getAll({String? baseUrl}) async { + final int cacheBuster = clock.now().millisecondsSinceEpoch; + final Map versionMap = + await _getVersionMap(baseUrl, cacheBuster) ?? + await _getVersionMap(_assetManager.baseUrl, cacheBuster) ?? + await _getVersionMap(web.window.document.baseURI, cacheBuster) ?? + {}; return PackageInfoData( appName: versionMap['app_name'] ?? '', @@ -65,19 +71,50 @@ class PackageInfoPlusWebPlugin extends PackageInfoPlatform { ); } - Map _getVersionMap(Response response) { + Future?> _getVersionMap( + String? baseUrl, + int cacheBuster, + ) async { + if (baseUrl?.isNotEmpty == true) { + final Uri url = versionJsonUrl(baseUrl!, cacheBuster); + final Response response = await _getResponse(url); + + return _decodeVersionMap(response); + } + + return null; + } + + Future _getResponse(Uri uri) async { + return _client == null ? await get(uri) : await _client.get(uri); + } + + Map? _decodeVersionMap(Response response) { if (response.statusCode == 200) { try { return jsonDecode(response.body); } catch (_) { - return {}; + return null; } } else { - return {}; + return null; } } } +extension _AssetManager on AssetManager { + /// Get the base URL configured in the Flutter Web Engine initialization + /// + /// The AssetManager has the base URL as private ([AssetManager._baseUrl] property), + /// so we need to do some little hack to get it. If AssetManager adds in some + /// moment a public API to get the base URL, this extension can be replaced by that API. + /// + /// @see https://docs.flutter.dev/platform-integration/web/initialization#initializing-the-engine + String get baseUrl { + return getAssetUrl('').replaceAll('$assetsDir/', ''); + } +} + extension _UriOrigin on Uri { /// Get origin. /// diff --git a/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_windows.dart b/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_windows.dart index 5bf1deafd4..7316d752c5 100644 --- a/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_windows.dart +++ b/packages/package_info_plus/package_info_plus/lib/src/package_info_plus_windows.dart @@ -18,7 +18,7 @@ class PackageInfoPlusWindowsPlugin extends PackageInfoPlatform { /// Returns a map with the following keys: /// appName, packageName, version, buildNumber @override - Future getAll() { + Future getAll({String? baseUrl}) { String resolvedExecutable = Platform.resolvedExecutable; /// Workaround for https://github.com/dart-lang/sdk/issues/52309 diff --git a/packages/package_info_plus/package_info_plus/pubspec.yaml b/packages/package_info_plus/package_info_plus/pubspec.yaml index 8b5b4a7a3b..40a7f262f7 100644 --- a/packages/package_info_plus/package_info_plus/pubspec.yaml +++ b/packages/package_info_plus/package_info_plus/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: # win32 is compatible across v4 and v5 for Win32 only (not COM) win32: ">=4.0.0 <6.0.0" + clock: ^1.1.1 dev_dependencies: flutter_lints: ">=2.0.1 <4.0.0" diff --git a/packages/package_info_plus/package_info_plus_platform_interface/lib/method_channel_package_info.dart b/packages/package_info_plus/package_info_plus_platform_interface/lib/method_channel_package_info.dart index 604f67e8b8..0c609b546b 100644 --- a/packages/package_info_plus/package_info_plus_platform_interface/lib/method_channel_package_info.dart +++ b/packages/package_info_plus/package_info_plus_platform_interface/lib/method_channel_package_info.dart @@ -9,7 +9,7 @@ const MethodChannel _channel = /// An implementation of [PackageInfoPlatform] that uses method channels. class MethodChannelPackageInfo extends PackageInfoPlatform { @override - Future getAll() async { + Future getAll({String? baseUrl}) async { final map = await _channel.invokeMapMethod('getAll'); return PackageInfoData( appName: map!['appName'] ?? '', diff --git a/packages/package_info_plus/package_info_plus_platform_interface/lib/package_info_platform_interface.dart b/packages/package_info_plus/package_info_plus_platform_interface/lib/package_info_platform_interface.dart index 96789d25b8..09c850f0d6 100644 --- a/packages/package_info_plus/package_info_plus_platform_interface/lib/package_info_platform_interface.dart +++ b/packages/package_info_plus/package_info_plus_platform_interface/lib/package_info_platform_interface.dart @@ -30,7 +30,7 @@ abstract class PackageInfoPlatform extends PlatformInterface { } ///Returns a map with the following keys : appName,packageName,version,buildNumber - Future getAll() { + Future getAll({String? baseUrl}) { throw UnimplementedError('getAll() has not been implemented.'); } }