diff --git a/packages/games_services_watchos/CHANGELOG.md b/packages/games_services_watchos/CHANGELOG.md index 27aaa0a..f525154 100644 --- a/packages/games_services_watchos/CHANGELOG.md +++ b/packages/games_services_watchos/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.2.1 + +* Example: `games_services`' own example app, unchanged, with its logo and + its MIT licence (`example/LICENSE`), replaces a hand-written demo and its + layout test (`AUTHORING.md` §3). The integration test is now + `watchos_games_services_test.dart`. + ## 0.2.0 * Implements `games_services_platform_interface` 5.3.0 (`^5.3.0`, was diff --git a/packages/games_services_watchos/PORTING_REPORT.md b/packages/games_services_watchos/PORTING_REPORT.md index 34b6b9a..5780117 100644 --- a/packages/games_services_watchos/PORTING_REPORT.md +++ b/packages/games_services_watchos/PORTING_REPORT.md @@ -7,8 +7,8 @@ | Implementation | ✅ Working (GameKit FFI) — sign-in and submission; reads unconfirmed on device | | watchOS capability | Partial — sign-in, the player stream and leaderboards; no Game Center UI, achievements, saved games or access point | | Host unit tests | ✅ pass (FFI bindings faked) | -| Upstream integration test | ○ none exists upstream — `games_services` ships no `test/` or `integration_test/`; ours covers registration, FFI linkage, the player stream and how each signed-out call fails, ✅ 7 pass on the watchOS 26.5 Simulator (0.2.0, 12 October 2026) | -| Unified demo | ✅ example included (`example/`), builds and starts on the watchOS 26.5 Simulator; its widget test (`example/test/watchos_layout_test.dart`, 11 pass) checks the layout on three watch screens in both safe areas | +| Upstream integration test | ○ none exists upstream — `games_services` ships no `test/` or `integration_test/`; ours, `watchos_games_services_test.dart`, covers registration, FFI linkage, the player stream and how each signed-out call fails, ✅ 7 pass on the watchOS 27.0 Simulator (2026-10-24) | +| Example | ✅ `games_services`' own example app, unchanged; it starts on the watchOS 27.0 Simulator with no exception in its first 15 seconds and no overflow | This package is published on pub.dev. The table is deliberately honest: sign-in and submission have been observed working on a physical Apple Watch @@ -16,6 +16,16 @@ Series 10, and reading entries has not. 0.2.0 changes only Dart code: its sign-in and submission results and its player stream are checked by the host tests and on the Simulator, and have not run on a watch yet. +## Example + +`games_services`' own example app, ported unchanged with `flutter-watchos +plugin port --include-example`. The porter copies `lib/` only, so the +example's `assets/logo.png` and the `assets:` entry of its pubspec were +copied from `games_services` 5.3.0 by hand. The example is MIT-licensed +upstream code; `example/LICENSE` carries that licence. There are no watchOS +deviations. It replaces a hand-written demo and its layout widget test +(`AUTHORING.md` §3). + ## What was ported `games_services` is federated over `games_services_platform_interface`, so diff --git a/packages/games_services_watchos/README.md b/packages/games_services_watchos/README.md index ef67d22..14b9cf7 100644 --- a/packages/games_services_watchos/README.md +++ b/packages/games_services_watchos/README.md @@ -133,6 +133,16 @@ These members of `games_services_platform_interface` 5.3.0 throw or fail on watc | `saveGame` / `loadGame` / `deleteGame` / `getSavedGames` | fails with `UnimplementedError` | `GKSavedGame` does not exist on watchOS | | `showSavedGames` | fails with `UnimplementedError` | the Play Games saved-games screen; Android only | +## Example + +The example is `games_services`' own example app, unchanged, ported with +`flutter-watchos plugin port --include-example`, with its `assets/logo.png`, +which the porter does not copy. Its MIT licence is in `example/LICENSE`. There +are no watchOS deviations. `games_services` has no integration test; +`example/integration_test/watchos_games_services_test.dart` covers the +registration, the FFI linkage, the player stream and how each signed-out call +fails. + ## Status Sign-in and score submission have been observed working on a physical Apple diff --git a/packages/games_services_watchos/example/LICENSE b/packages/games_services_watchos/example/LICENSE new file mode 100644 index 0000000..c2d3a5b --- /dev/null +++ b/packages/games_services_watchos/example/LICENSE @@ -0,0 +1,7 @@ +Copyright 2022 Abedalkareem Omreyh + +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. \ No newline at end of file diff --git a/packages/games_services_watchos/example/assets/logo.png b/packages/games_services_watchos/example/assets/logo.png new file mode 100644 index 0000000..1127714 Binary files /dev/null and b/packages/games_services_watchos/example/assets/logo.png differ diff --git a/packages/games_services_watchos/example/integration_test/games_services_watchos_test.dart b/packages/games_services_watchos/example/integration_test/watchos_games_services_test.dart similarity index 100% rename from packages/games_services_watchos/example/integration_test/games_services_watchos_test.dart rename to packages/games_services_watchos/example/integration_test/watchos_games_services_test.dart diff --git a/packages/games_services_watchos/example/lib/main.dart b/packages/games_services_watchos/example/lib/main.dart index 2e08575..184ab68 100644 --- a/packages/games_services_watchos/example/lib/main.dart +++ b/packages/games_services_watchos/example/lib/main.dart @@ -1,110 +1,378 @@ -// Copyright 2026 The FlutterWatch Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show PlatformException; -import 'package:flutter_watchos/flutter_watchos.dart'; +import 'package:flutter/services.dart' show rootBundle; import 'package:games_services/games_services.dart'; -/// Replace with a leaderboard id from your own App Store Connect record; a -/// simulator has no Game Center account, so the interesting states here are -/// the failures, and they are the point of the example. -const String kLeaderboardId = 'your.leaderboard.id'; +void main() => runApp(const App()); -void main() => runApp(const ExampleApp()); - -class ExampleApp extends StatelessWidget { - const ExampleApp({super.key}); +class App extends StatefulWidget { + const App({Key? key}) : super(key: key); @override - Widget build(BuildContext context) => - const MaterialApp(home: Scaffold(body: _Body())); + AppState createState() => AppState(); } -class _Body extends StatefulWidget { - const _Body(); - +class AppState extends State { @override - State<_Body> createState() => _BodyState(); -} + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData( + appBarTheme: const AppBarTheme( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + )), + ), + home: Scaffold( + appBar: AppBar( + title: const Text('Games Services plugin example'), + ), + body: StreamBuilder( + stream: GameAuth.player, + builder: (context, snapshot) { + final player = snapshot.data; + return SingleChildScrollView( + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Image.asset( + "assets/logo.png", + width: 200, + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: player == null + ? ElevatedButton( + onPressed: _signIn, + style: ElevatedButton.styleFrom( + minimumSize: const Size(150, 40), + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ), + child: const Text('Sign In'), + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + if (player.iconImage != null) + CircleAvatar( + foregroundImage: MemoryImage( + base64Decode(player.iconImage!), + ), + ) + else + CircleAvatar( + child: Text(player.displayName[0]), + ), + const SizedBox(width: 16.0), + Expanded( + child: Text( + 'playerID: ${player.playerID}\n' + 'displayName: ${player.displayName}'), + ), + ], + ), + const SizedBox(height: 16.0), + Theme( + data: ThemeData( + elevatedButtonTheme: + ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + minimumSize: const Size(150, 40), + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ), + ), + ), + child: SingleChildScrollView( + child: Wrap( + spacing: 20, + runSpacing: 10, + children: [ + ElevatedButton( + onPressed: _showAchievements, + child: const Text( + 'Show Achievements'), + ), + ElevatedButton( + onPressed: _showLeaderboards, + child: const Text( + 'Show Leaderboards'), + ), + ElevatedButton( + onPressed: _submitScore, + child: const Text('Submit Score'), + ), + ElevatedButton( + onPressed: _unlockAchievement, + child: const Text( + 'Unlock Achievement'), + ), + ElevatedButton( + onPressed: _loadAchievement, + child: const Text( + 'Load Achievement'), + ), + ElevatedButton( + onPressed: _resetAchievement, + child: const Text( + 'Reset Achievement'), + ), + ElevatedButton( + onPressed: _loadLeaderboardScores, + child: const Text( + 'Load Leaderboard Scores'), + ), + ElevatedButton( + onPressed: _incrementAchievement, + child: const Text( + 'Increment Achievement (Android only)'), + ), + ElevatedButton( + onPressed: _showAccessPoint, + child: const Text( + 'Show AccessPoint (iOS only)'), + ), + ElevatedButton( + onPressed: _hideAccessPoint, + child: const Text( + 'Hide AccessPoint (iOS only)'), + ), + ElevatedButton( + onPressed: _getPlayerScore, + child: const Text( + 'Get player score'), + ), + ElevatedButton( + onPressed: _getPlayerScoreObject, + child: const Text( + 'Load Player Centered Scores'), + ), + ElevatedButton( + onPressed: + _loadPreviousOccurrence, + child: const Text( + 'Load Previous Occurrence (iOS only)'), + ), + if (Platform.isAndroid) + ElevatedButton( + onPressed: _showSavedGames, + child: const Text( + 'Show Saved Games')), + ElevatedButton( + onPressed: _getSavedGames, + child: + const Text('Get saved games'), + ), + ElevatedButton( + onPressed: _saveGame, + child: const Text('Save game'), + ), + ElevatedButton( + onPressed: _loadGame, + child: const Text('Load game'), + ), + ElevatedButton( + onPressed: _deleteGame, + child: const Text( + 'Delete saved game'), + ), + ElevatedButton( + onPressed: + _fetchIdentityVerificationSignature, + child: const Text( + 'Fetch Identity Verification (iOS and MacOS)'), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + })), + ); + } -class _BodyState extends State<_Body> { - final List _log = []; + void _signIn() async { + final result = await GameAuth.signIn(); + print(result); + } - void _say(String line) => setState(() => _log.insert(0, line)); + void _getPlayerScore() async { + final result = await Player.getPlayerScore(); + print(result); + } - // As on iOS with games_services 5.x, signIn and submitScore complete with - // a message on success and throw a PlatformException on failure. - Future _signIn() async { - try { - _say(await GameAuth.signIn() ?? 'signed in'); - } on PlatformException catch (e) { - _say('sign-in failed: ${e.message}'); - } + void _getPlayerScoreObject() async { + final result = await Leaderboards.getPlayerScoreObject( + iOSLeaderboardID: "ios_leaderboard_id", + androidLeaderboardID: "android_leaderboard_id", + scope: PlayerScope.global, + timeScope: TimeScope.allTime); + print(result); } - Future _submit() async { - try { - await GamesServices.submitScore( - score: Score(iOSLeaderboardID: kLeaderboardId, value: 10), - ); - _say('submitted 10'); - } on PlatformException catch (e) { - _say('submit failed: ${e.message}'); + void _loadPreviousOccurrence() async { + final result = await Leaderboards.loadPreviousOccurrence( + iOSLeaderboardID: "ios_leaderboard_id", + androidLeaderboardID: "android_leaderboard_id", + timeScope: TimeScope.allTime); + if (result != null) { + print('Previous score found:'); + print(' Score: ${result.rawScore}'); + print(' Rank: ${result.rank}'); + print(' Display Score: ${result.displayScore}'); + print( + ' Timestamp: ${DateTime.fromMillisecondsSinceEpoch(result.timestampMillis)}'); + print(' Player: ${result.scoreHolder.displayName}'); + } else { + print('No previous occurrence found'); } } - Future _load() async { - final List? rows = - await Leaderboards.loadLeaderboardScores( - iOSLeaderboardID: kLeaderboardId, - scope: PlayerScope.global, - timeScope: TimeScope.allTime, - maxResults: 5, + void _showAccessPoint() async { + final result = await Player.showAccessPoint(AccessPointLocation.topLeading); + print(result); + } + + void _hideAccessPoint() async { + final result = await Player.hideAccessPoint(); + print(result); + } + + void _resetAchievement() async { + final result = await Achievements.resetAchievements(); + print(result); + } + + void _incrementAchievement() async { + final result = await Achievements.increment( + achievement: Achievement(androidID: 'android_id', steps: 50)); + print(result); + } + + void _unlockAchievement() async { + final result = await Achievements.unlock( + achievement: Achievement( + androidID: 'android_id', iOSID: 'ios_id', percentComplete: 100)); + print(result); + } + + void _loadAchievement() async { + final result = await Achievements.loadAchievements(); + print(result); + } + + void _loadLeaderboardScores() async { + final result = await Leaderboards.loadLeaderboardScores( + iOSLeaderboardID: "ios_leaderboard_id", + androidLeaderboardID: "android_leaderboard_id", + scope: PlayerScope.global, + timeScope: TimeScope.allTime, + maxResults: 10); + print(result); + } + + void _submitScore() async { + final result = await Leaderboards.submitScore( + score: Score( + androidLeaderboardID: 'android_leaderboard_id', + iOSLeaderboardID: 'ios_leaderboard_id', + value: 5)); + print(result); + } + + void _showLeaderboards() async { + final result = await Leaderboards.showLeaderboards( + androidLeaderboardID: 'android_leaderboard_id', + iOSLeaderboardID: 'ios_leaderboard_id'); + print(result); + } + + void _showAchievements() async { + final result = await Achievements.showAchievements(); + print(result); + } + + void _showSavedGames() async { + final result = await SaveGame.showSavedGames(title: 'Saved Games'); + print(result); + } + + void _getSavedGames() async { + final result = await SaveGame.getSavedGames(); + print(result); + } + + void _saveGame() async { + final data = jsonEncode(GameData(96, "sword").toJson()); + // A cover image is required to pass Google's Play Games Services quality + // checklist. Here we reuse the bundled logo as a sample cover image. + final coverImage = + (await rootBundle.load("assets/logo.png")).buffer.asUint8List(); + final result = await SaveGame.saveGame( + data: data, + name: "slot1", + coverImage: coverImage, + description: "Level 96, sword equipped", + playedTime: const Duration(minutes: 42), ); - // Null and empty mean different things, and the example says which. - if (rows == null) { - _say('load failed'); - } else if (rows.isEmpty) { - _say('no scores'); + print(result); + } + + void _loadGame() async { + final result = await SaveGame.loadGame(name: "slot1"); + if (result != null) { + final json = jsonDecode(result) as Map; + final gameData = GameData.fromJson(json); + print("Player progress ${gameData.progress}"); + print("Player weapon ${gameData.weapon}"); + } + } + + void _deleteGame() async { + final result = await SaveGame.deleteGame(name: "slot1"); + print(result); + } + + void _fetchIdentityVerificationSignature() async { + final result = await GameAuth.fetchIdentityVerificationSignature(); + if (result != null) { + print('Identity Verification Signature:'); + print(' Public Key URL: ${result.publicKeyURL}'); + print(' Signature: ${result.signature}'); + print(' Salt: ${result.salt}'); + print(' Timestamp: ${result.timestamp}'); } else { - _say(rows.map((r) => '${r.rank}. ${r.displayScore}').join(' ')); + print('Identity verification not available (iOS and MacOS)'); } } +} - @override - Widget build(BuildContext context) { - return ListView( - // No SafeArea, so the list fills the screen the way a native watchOS - // list does: rows scroll under the clock and down to the bottom edge. - // The insets go into the list's padding instead. The default safe area - // leaves the clock out of MediaQuery's padding, so the top comes from - // WatchStatusBar.heightOf, the clock's band, in either safe area; the - // other sides take MediaQuery's insets, so the rows stay clear of the - // rounded corners and the last one scrolls clear of the bottom edge. - padding: MediaQuery.paddingOf(context) - .copyWith(top: WatchStatusBar.heightOf(context)) + - const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - children: [ - // GameAuth.player gives the current sign-in state at once, and a new - // one when a call changes it. - StreamBuilder( - stream: GameAuth.player, - builder: (BuildContext context, AsyncSnapshot player) { - final String? name = player.data?.displayName; - return Text( - name == null ? 'signed out' : 'signed in as $name', - style: const TextStyle(fontSize: 12), - ); - }, - ), - TextButton(onPressed: _signIn, child: const Text('Sign in')), - TextButton(onPressed: _submit, child: const Text('Submit 10')), - TextButton(onPressed: _load, child: const Text('Load scores')), - for (final String line in _log) - Text(line, style: const TextStyle(fontSize: 11)), - ], - ); +class GameData { + int progress; + String weapon; + GameData(this.progress, this.weapon); + + factory GameData.fromJson(Map json) { + return GameData(json["progress"], json["weapon"]); } + + Map toJson() => {'progress': progress, 'weapon': weapon}; } diff --git a/packages/games_services_watchos/example/pubspec.yaml b/packages/games_services_watchos/example/pubspec.yaml index bf3c543..719796a 100644 --- a/packages/games_services_watchos/example/pubspec.yaml +++ b/packages/games_services_watchos/example/pubspec.yaml @@ -9,9 +9,8 @@ environment: dependencies: flutter: sdk: flutter - # WatchStatusBar.heightOf, where the list starts below the clock. - flutter_watchos: ^0.1.0 games_services: ^5.3.0 + # integration_test/watchos_games_services_test.dart imports it. games_services_platform_interface: ^5.3.0 games_services_watchos: path: ../ @@ -25,3 +24,5 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - assets/logo.png diff --git a/packages/games_services_watchos/example/test/watchos_layout_test.dart b/packages/games_services_watchos/example/test/watchos_layout_test.dart deleted file mode 100644 index f26764f..0000000 --- a/packages/games_services_watchos/example/test/watchos_layout_test.dart +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2026 The FlutterWatch Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// -// Host-side: the example against fake GameKit bindings and a fake clock band, -// so it needs no watch. It lays the example out on three watch screens in -// both safe areas, and checks what the example shows when a call fails, as -// every call does on a Simulator with no Game Center account, and when the -// player signs in. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_watchos/flutter_watchos.dart'; -import 'package:games_services_example/main.dart'; -import 'package:games_services_watchos/games_services_watchos.dart'; - -/// A watch screen in points, with the insets watchOS reports for it while the -/// clock is shown, and the corner inset the host reports instead in the -/// default safe area, as measured on the watchOS Simulator (the values -/// flutter_watchos's own example test uses). -class _Watch { - const _Watch( - this.name, - this.size, { - required this.top, - required this.bottom, - required this.corner, - }); - - final String name; - final Size size; - - /// The top inset watchOS reports: the clock's band. - final double top; - - /// The bottom inset watchOS reports. - final double bottom; - - /// The inset on all four sides in the default `corners` safe area. - final double corner; -} - -/// The left and right insets watchOS reports, the same on every watch. -const double _sides = 2; - -const List<_Watch> _watches = <_Watch>[ - _Watch('40mm', Size(162, 197), top: 40, bottom: 19, corner: 9), - _Watch('42mm', Size(187, 223), top: 48, bottom: 31, corner: 13), - _Watch('Ultra 3', Size(211, 257), top: 56.5, bottom: 40, corner: 17), -]; - -/// The safe area the host reports, selected by the app's -/// `FlutterWatchOSSafeArea` key. -enum _Mode { - /// The default: the corner inset on all four sides, clock left out. - corners, - - /// `platform`: the insets watchOS reports, clock included. - platform, - - /// `platform` under a host that does not report the clock band. - platformUnreported, -} - -/// Stands in for the clock band the watch host reports. -class _ClockBand extends WatchOSNativeBindings { - _ClockBand(this.band) : super.forTesting(); - - final double band; - - @override - double get clockBandHeight => band; -} - -/// GameKit with or without a signed-in account. Without one, as on a -/// Simulator, every call fails. -class _GameKit extends GamesServicesWatchosBindings { - _GameKit({this.hasAccount = false}) : super.forTesting(); - - final bool hasAccount; - GcAuthState _state = GcAuthState.idle; - - @override - GcAuthState get authState => _state; - - @override - String? get lastError => 'The player is not signed in.'; - - @override - String? get playerAlias => _state == GcAuthState.signedIn ? 'Gardener' : null; - - @override - Future signIn({Duration timeout = const Duration(seconds: 20)}) async { - // GameKit answers a launch's sign-in once. - if (_state == GcAuthState.idle) { - _state = hasAccount ? GcAuthState.signedIn : GcAuthState.failed; - } - return _state == GcAuthState.signedIn; - } - - @override - Future submitScore( - int score, - String leaderboardId, { - Duration timeout = const Duration(seconds: 15), - }) async => - _state == GcAuthState.signedIn; - - @override - Future?> topScores( - String leaderboardId, { - int count = 10, - Duration timeout = const Duration(seconds: 20), - }) async => - _state == GcAuthState.signedIn ? const [] : null; -} - -void main() { - setUp(() { - GamesServicesWatchos.bindingsOverride = _GameKit(); - GamesServicesWatchos.registerWith(); - }); - - tearDown(() { - GamesServicesWatchos.bindingsOverride = null; - WatchStatusBar.bindingsOverride = null; - }); - - for (final _Watch watch in _watches) { - for (final _Mode mode in _Mode.values) { - testWidgets( - '${watch.name}, ${mode.name}: the list fills the screen, starts ' - 'below the clock and insets its rows', (WidgetTester tester) async { - final double side = mode == _Mode.corners ? watch.corner : _sides; - final double top = mode == _Mode.corners ? watch.corner : watch.top; - final double bottom = - mode == _Mode.corners ? watch.corner : watch.bottom; - // The host reports the clock's band in both safe areas; a host that - // does not report it leaves heightOf the view's top padding, which in - // `platform` is the band too. - if (mode != _Mode.platformUnreported) { - WatchStatusBar.bindingsOverride = _ClockBand(watch.top); - } - - const double pixelRatio = 2; - tester.view.devicePixelRatio = pixelRatio; - tester.view.physicalSize = watch.size * pixelRatio; - final FakeViewPadding insets = FakeViewPadding( - left: side * pixelRatio, - top: top * pixelRatio, - right: side * pixelRatio, - bottom: bottom * pixelRatio, - ); - tester.view.padding = insets; - tester.view.viewPadding = insets; - addTearDown(tester.view.reset); - - await tester.pumpWidget(const ExampleApp()); - await tester.pumpAndSettle(); - - // No SafeArea shrinks the list: it covers the whole screen, so its - // rows can scroll under the clock and down to the bottom edge. - expect(tester.getRect(find.byType(ListView)), Offset.zero & watch.size); - - // The first row starts 6 below the clock's band in both safe areas, - // and 10 inside the side insets. - final Rect status = tester.getRect(find.text('signed out')); - expect(status.top, watch.top + 6); - expect(status.left, side + 10); - expect(status.right, watch.size.width - side - 10); - - // At rest, no button is in the clock's band. - for (final Element button in find.byType(TextButton).evaluate()) { - expect( - tester.getRect(find.byWidget(button.widget)).top, - greaterThanOrEqualTo(watch.top), - ); - } - - // Enough lines in the log to scroll on every screen, then scroll to - // the end. One jump is not enough: the list builds its rows lazily - // and learns its full length only once the last ones are built. - for (int i = 0; i < 6; i++) { - await tester.tap(find.text('Sign in')); - await tester.pumpAndSettle(); - } - final ScrollPosition position = - tester.state(find.byType(Scrollable)).position; - double extent = -1; - for (int jumps = 0; - jumps < 10 && position.maxScrollExtent != extent; - jumps++) { - extent = position.maxScrollExtent; - position.jumpTo(extent); - await tester.pump(); - } - expect(position.maxScrollExtent, greaterThan(0)); - expect(position.pixels, position.maxScrollExtent); - - // The last row, the oldest line of the log, ends 6 above the bottom - // inset. - final Rect last = tester.getRect( - find.textContaining('sign-in failed').last, - ); - expect(last.bottom, watch.size.height - bottom - 6); - }); - } - } - - testWidgets('a failed sign-in, submission and read show in the log', - (WidgetTester tester) async { - await tester.pumpWidget(const ExampleApp()); - await tester.pumpAndSettle(); - expect(find.text('signed out'), findsOneWidget); - - await tester.tap(find.text('Sign in')); - await tester.pumpAndSettle(); - expect( - find.text('sign-in failed: The player is not signed in.'), - findsOneWidget, - ); - - await tester.tap(find.text('Submit 10')); - await tester.pumpAndSettle(); - expect( - find.text('submit failed: Not signed in to Game Center'), - findsOneWidget, - ); - - await tester.tap(find.text('Load scores')); - await tester.pumpAndSettle(); - expect(find.text('load failed'), findsOneWidget); - expect(find.text('signed out'), findsOneWidget); - }); - - testWidgets('a sign-in shows the player', (WidgetTester tester) async { - GamesServicesWatchos.bindingsOverride = _GameKit(hasAccount: true); - await tester.pumpWidget(const ExampleApp()); - await tester.pumpAndSettle(); - expect(find.text('signed out'), findsOneWidget); - - await tester.tap(find.text('Sign in')); - await tester.pumpAndSettle(); - expect(find.text('Player authenticated successfully'), findsOneWidget); - expect(find.text('signed in as Gardener'), findsOneWidget); - - await tester.tap(find.text('Load scores')); - await tester.pumpAndSettle(); - expect(find.text('no scores'), findsOneWidget); - }); -} diff --git a/packages/games_services_watchos/pubspec.yaml b/packages/games_services_watchos/pubspec.yaml index 23939d5..0810bc9 100644 --- a/packages/games_services_watchos/pubspec.yaml +++ b/packages/games_services_watchos/pubspec.yaml @@ -2,7 +2,7 @@ name: games_services_watchos description: >- watchOS implementation of the games_services plugin: Game Center leaderboards over GameKit, bridged with dart:ffi. -version: 0.2.0 +version: 0.2.1 homepage: https://flutterwatch.dev repository: https://github.com/flutterwatch/plugins/tree/main/packages/games_services_watchos issue_tracker: https://github.com/flutterwatch/plugins/issues diff --git a/tool/words/forbidden_words_allow.txt b/tool/words/forbidden_words_allow.txt index 8d13002..b04b2e6 100644 --- a/tool/words/forbidden_words_allow.txt +++ b/tool/words/forbidden_words_allow.txt @@ -25,6 +25,7 @@ packages/in_app_purchase_watchos/lib/in_app_purchase_watchos.dart | rawPrice: (p # which is the CLI's, so each entry names its packages. packages/audioplayers_watchos/**/LICENSE | Permission is hereby granted, free of charge, | The MIT licence text of an upstream plugin, which stays verbatim. packages/geolocator_watchos/LICENSE | Permission is hereby granted, free of charge, | The MIT licence text of an upstream plugin, which stays verbatim. +packages/games_services_watchos/example/LICENSE | Permission is hereby granted, free of charge, | The MIT licence text of the upstream example, which stays verbatim. packages/audioplayers_watchos/**/*.dart | calloc.free( | package:ffi's calloc.free, which releases native memory; a Dart API call. packages/firebase_*_watchos/**/*.dart | calloc.free( | package:ffi's calloc.free, which releases native memory; a Dart API call. packages/flutter_watch_link/**/*.dart | calloc.free( | package:ffi's calloc.free, which releases native memory; a Dart API call.