diff --git a/docs/friends.md b/docs/friends.md new file mode 100644 index 00000000..68524368 --- /dev/null +++ b/docs/friends.md @@ -0,0 +1,65 @@ +# Friends + +## iOS/MacOS Setup + +On iOS and MacOS, you must provide a reason to access a player’s friends by adding the `NSGKFriendListUsageDescription` key to `Info.plist`. + +There is no additional setup required for Android. + +## Check for access to the players friends list + +Get the access status to the player's friends list. This can be useful for presenting a different UI if friends list access is unavailable or to delay the request to access the friends list once value is established. It is not strictly necessary to use this as the call to `loadFriends` will automatically request access to the friends list on first call or throw an error if the access has been denied. + +```dart +final FriendsAccess friendsAccess = await Friends.access; +``` + +## Get the player's friends list + +Return a list of `PlayerData` objects representing the users platform friends. + +```dart +final friends = await Friends.loadFriends(); +``` + +## Show the player's friends list (iOS only) + +Show the platform friends list UI. Friends access is not required. + +```dart +Friends.showFriendsList(); +``` + +## Show a player's profile + +Show the platform UI displaying a player's profile. Passing in the current player's ID will display their profile. + +```dart +Friends.showPlayerProfile(playerID); +``` + +On Android, the UI will contain a comparison of the current player's profile with the profile corresponding to the provided `playerID`. If the players are not platform friends, an option to add as a friend will be avaiable. If in game nicknames differ from the platfrom `displayName`s, these can be provided to display in the profile comparison UI. If provided, the `localInGameName` will also be included in the friend request. + +```dart +Friends.showPlayerProfile( + playerID, + playerInGameName: 'otherPlayersNickname', + localInGameName: 'currentPlayersNickname', +); +``` + +## Send a platform friend request (iOS only) + +Launch the platform friend request UI. + +```dart +Friends.sendFriendRequest(); +``` + +## Search for a platform player by name (Android only) + +Launch the platform player search UI. Returns a `PlayerData` object representing the selected player or `null` if canceled. + +```dart +final player = Friends.searchForPlayer(); +``` diff --git a/docs/leaderboard_and_achievements.md b/docs/leaderboard_and_achievements.md index 4615c578..c73d8aa0 100644 --- a/docs/leaderboard_and_achievements.md +++ b/docs/leaderboard_and_achievements.md @@ -4,7 +4,7 @@ Display the device's default achievements screen. -``` dart +```dart Achievements.showAchievements(); ``` @@ -12,7 +12,7 @@ Achievements.showAchievements(); Get achievements as a list. Use this to build a custom UI. -``` dart +```dart final result = await Achievements.loadAchievements(); ``` @@ -23,7 +23,7 @@ final result = await Achievements.loadAchievements(); **Example with parameters:** -``` dart +```dart final result = await Achievements.loadAchievements( forceRefresh: true, ignoreImages: true, // Skip image loading for better performance @@ -32,18 +32,20 @@ final result = await Achievements.loadAchievements( ## Unlock achievement -Unlock an ```Achievement```. -The ```Achievement``` takes three parameters: +Unlock an `Achievement`. +The `Achievement` takes three parameters: -- ```androidID``` the achievement id for Google Play Games. -- ```iOSID``` the achievement id for Game Center. -- ```percentComplete``` the completion percentage of the achievement, this parameter is optional on iOS/macOS. -- ```steps``` the achievement steps for Google Play Games (as seen in the next section). +- `androidID` the achievement id for Google Play Games. +- `iOSID` the achievement id for Game Center. +- `percentComplete` the completion percentage of the achievement, this parameter is optional on iOS/macOS. +- `steps` the achievement steps for Google Play Games (as seen in the next section). -``` dart -Achievements.unlock(achievement: Achievement(androidID: 'android_id', - iOSID: 'ios_id', - percentComplete: 100)); +```dart +Achievements.unlock(achievement: Achievement( + androidID: 'android_id', + iOSID: 'ios_id', + percentComplete: 100, +)); ``` ## Increment (Android Only) @@ -51,59 +53,89 @@ Achievements.unlock(achievement: Achievement(androidID: 'android_id', Increment the steps for a Google Play Games achievement. ```dart -final result = await Achievements.increment(achievement: Achievement(androidID: 'android_id', steps: 50)); -print(result); +final result = await Achievements.increment( + achievement: Achievement(androidID: 'android_id', steps: 50), +); ``` ## Show leaderboards Display the device's default leaderboards screen. If a leaderboard ID is provided, it will display the specific leaderboard, otherwise it will show the list of all leaderboards. -``` dart - Leaderboards.showLeaderboards(iOSLeaderboardID: 'ios_leaderboard_id', androidLeaderboardID: 'android_leaderboard_id'); +```dart +Leaderboards.showLeaderboards( + iOSLeaderboardID: 'ios_leaderboard_id', + androidLeaderboardID: 'android_leaderboard_id', +); ``` ## Load leaderboard scores Get leaderboard scores as a list. Use this to build a custom UI. -``` dart +```dart final result = await Leaderboards.loadLeaderboardScores( - iOSLeaderboardID: "ios_leaderboard_id", - androidLeaderboardID: "android_leaderboard_id", - // Returns a list centered around the player's rank on the leaderboard. (Defaults to false) - playerCentered: false, - scope: PlayerScope.global, - timeScope: TimeScope.allTime, - maxResults: 10); + iOSLeaderboardID: "ios_leaderboard_id", + androidLeaderboardID: "android_leaderboard_id", + // Returns a list centered around the player's rank on the leaderboard. + // Defaults to false + playerCentered: false, + scope: PlayerScope.global, + timeScope: TimeScope.allTime, + maxResults: 10, +); ``` ## Load previous occurrence (iOS only) Load the previous occurrence of the player's score from a leaderboard. This returns the score data that precedes the player's current best score, which is useful for tracking score progression over time. -``` dart +```dart final previousScore = await Leaderboards.loadPreviousOccurrence( - iOSLeaderboardID: "ios_leaderboard_id", - timeScope: TimeScope.allTime); + iOSLeaderboardID: 'ios_leaderboard_id', + timeScope: TimeScope.allTime +); if (previousScore != null) { print('Previous score: ${previousScore.rawScore}'); - print('Achieved on: ${DateTime.fromMillisecondsSinceEpoch(previousScore.timestampMillis)}'); + final timestamp = DateTime.fromMillisecondsSinceEpoch( + previousScore.timestampMillis, + ); + print('Achieved on: $timestamp'); } ``` ## Submit score -Submit a ```Score``` to specific leaderboard. -The ```Score``` class takes three parameters: +Submit a `Score` to specific leaderboard. +The `Score` class takes three parameters: + +- `androidLeaderboardID`: the leaderboard ID for Google Play Games. +- `iOSLeaderboardID` the leaderboard ID for Game Center. +- `value` the score. + +```dart +Leaderboards.submitScore(score: Score( + androidLeaderboardID: 'android_leaderboard_id', + iOSLeaderboardID: 'ios_leaderboard_id', + value: 5, +)); +``` + +## Handling private profiles (Android only) + +On Google Play Games, a player's profile may be marked private, keeping their scores from appearing on public leaderboards. In fact, this is the default setting for new accounts. Fortunately, we can determine if this is the case and handle it appropriately. + +If a `ScoreObject` for the player exists but the `ScoreObject.rank` == -1, then the score is not visible on the public leaderboard. We can adjust our UI accordingly, as well as guide the player to their profile to adjust their privacy settings. -- ```androidLeaderboardID```: the leaderboard ID for Google Play Games. -- ```iOSLeaderboardID``` the leaderboard ID for Game Center. -- ```value``` the score. +```dart +final score = await Leaderboards.getPlayerScoreObject( + scope: PlayerScope.global, + timeScope: TimeScope.allTime, + androidLeaderboardID: leaderboardID, +); -``` dart -Leaderboards.submitScore(score: Score(androidLeaderboardID: 'android_leaderboard_id', - iOSLeaderboardID: 'ios_leaderboard_id', - value: 5)); +if (score != null && score.rank == -1) { + Player.viewProfile(); +} ``` diff --git a/docs/player.md b/docs/player.md index 2e20785c..dcb4102d 100644 --- a/docs/player.md +++ b/docs/player.md @@ -34,8 +34,8 @@ Get the current player's score for a specific leaderboard. ```dart final playerScore = Player.getPlayerScore( - iOSLeaderboardID = 'ios_leaderboard_id', - androidLeaderboardID = 'android_leaderboard_id', + iOSLeaderboardID: 'ios_leaderboard_id', + androidLeaderboardID: 'android_leaderboard_id', ); ``` diff --git a/games_services/android/src/main/kotlin/com/abedalkareem/games_services/Friends.kt b/games_services/android/src/main/kotlin/com/abedalkareem/games_services/Friends.kt new file mode 100644 index 00000000..e8f8414b --- /dev/null +++ b/games_services/android/src/main/kotlin/com/abedalkareem/games_services/Friends.kt @@ -0,0 +1,234 @@ +package com.abedalkareem.games_services + +import android.app.Activity +import android.content.Intent +import com.abedalkareem.games_services.models.PlayerData +import com.abedalkareem.games_services.util.AppImageLoader +import com.abedalkareem.games_services.util.PluginError +import com.abedalkareem.games_services.util.errorCode +import com.abedalkareem.games_services.util.errorMessage +import com.google.android.gms.games.PlayGames +import com.google.android.gms.games.Player +import com.google.android.gms.games.PlayersClient +import com.google.gson.Gson +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import android.util.Log +import com.abedalkareem.games_services.util.Messages +import com.google.android.gms.games.FriendsResolutionRequiredException +import io.flutter.plugin.common.PluginRegistry + +class Friends(private var activityPluginBinding: ActivityPluginBinding) : + PluginRegistry.ActivityResultListener { + + //region Variables + private val imageLoader = AppImageLoader() + private val playersClient: PlayersClient + get() { + return PlayGames.getPlayersClient(activityPluginBinding.activity) + } + + private var maxResults: Int? = null + private var forceRefresh: Boolean? = null + private var result: MethodChannel.Result? = null + private var errorMessage: String? = null + private var hasLoadedFriends: Boolean = false + //endregion + + //region Public Methods + fun getFriendsAccessStatus(result: MethodChannel.Result, forceRefresh: Boolean = false) { + playersClient.getCurrentPlayer(forceRefresh).addOnSuccessListener { annotatedData -> + val player = annotatedData.get() + if (player == null && forceRefresh) { + result.error( + PluginError.NotAuthenticated.errorCode(), + PluginError.NotAuthenticated.errorMessage(), + null + ) + return@addOnSuccessListener + } + Log.i("GamesServices", "forceRefresh: " + forceRefresh + "\n" + player.toString()) + Log.i("GamesServices", "status: " + player?.getCurrentPlayerInfo()?.friendsListVisibilityStatus.toString()) + when (player?.currentPlayerInfo?.friendsListVisibilityStatus) { + Player.FriendsListVisibilityStatus.REQUEST_REQUIRED -> result.success("notDetermined") + Player.FriendsListVisibilityStatus.FEATURE_UNAVAILABLE -> result.success("denied") + Player.FriendsListVisibilityStatus.VISIBLE -> result.success("granted") + else -> if (forceRefresh) result.success("unknown") else getFriendsAccessStatus(result, true) + } + }.addOnFailureListener { + result.error(PluginError.NotAuthenticated.errorCode(), it.localizedMessage, null) + } + } + + fun loadFriends( + activity: Activity?, + pageSize: Int, + forceRefresh: Boolean, + result: MethodChannel.Result + ) { + activity ?: return + + (if (hasLoadedFriends && !forceRefresh) playersClient.loadMoreFriends(pageSize) + else playersClient.loadFriends(pageSize, forceRefresh)).addOnSuccessListener { annotatedData -> + val data = annotatedData.get() + if (data == null) { + result.error( + PluginError.FailedToLoadFriends.errorCode(), + PluginError.FailedToLoadFriends.errorMessage(), + null + ) + return@addOnSuccessListener + } + val handler = CoroutineExceptionHandler { _, exception -> + result.error( + PluginError.FailedToLoadFriends.errorCode(), + exception.localizedMessage, + null + ) + } + + CoroutineScope(Dispatchers.Main + handler).launch { + val friends = mutableListOf() + for (item in data) { + val playerIconImage = if (item.iconImageUri != null) + item.iconImageUri.let { imageLoader.loadImageFromUri(activity, it!!) } + else null + friends.add(PlayerData( + item.displayName, + item.playerId, + playerIconImage + )) + } + val gson = Gson() + val string = gson.toJson(friends) ?: "" + data.release() + hasLoadedFriends = true + result.success(string) + } + } + .addOnFailureListener { + if (it is FriendsResolutionRequiredException) { + this.maxResults = maxResults + this.forceRefresh = forceRefresh + this.errorMessage = it.localizedMessage + this.result = result + val pendingIntent = it.resolution + activityPluginBinding.addActivityResultListener(this) + activity.startIntentSenderForResult( + pendingIntent.intentSender, + 26703, + null, + 0, + 0, + 0 + ) + Log.i("GamesServices", "Friends list access requested") + } else { + result.error( + PluginError.FailedToLoadFriends.errorCode(), + it.localizedMessage, + null + ) + } + } + } + + fun viewPlayerProfile( + activity: Activity?, + playerId: String, + playerInGameName: String, + localInGameName: String, + result: MethodChannel.Result + ) { + activity ?: return + + (if (playerInGameName.isEmpty()) playersClient.getCompareProfileIntent(playerId) + else playersClient.getCompareProfileIntentWithAlternativeNameHints(playerId, playerInGameName, localInGameName) + ).addOnSuccessListener { intent -> + activity?.startActivityForResult(intent, 0) + result.success(Messages.SUCCESS) + }.addOnFailureListener { + result.error(PluginError.FailedToLoadPlayer.errorCode(), it.message, null) + } + } + + fun searchForPlayer(activity: Activity?, result: MethodChannel.Result) { + activity ?: return + + playersClient.playerSearchIntent.addOnSuccessListener { intent -> + this.result = result + activityPluginBinding.addActivityResultListener(this) + activity?.startActivityForResult(intent, 9005) + }.addOnFailureListener { + result.error(PluginError.FailedToLoadPlayer.errorCode(), it.message, null) + } + } + + // handle result from friends list permission request OR player search + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean { + activityPluginBinding.removeActivityResultListener(this) + if (requestCode == 26703) { + // retry loadFriends if permission granted, otherwise throw the original error + if (resultCode == -1) { + val max = maxResults + val refresh = forceRefresh + val res = result + if (max != null && refresh != null && res != null) { + loadFriends(activityPluginBinding.activity, max, refresh, res) + } + } else { + result?.error( + PluginError.FailedToLoadFriends.errorCode(), + errorMessage, + null, + ) + } + maxResults = null + forceRefresh = null + result = null + errorMessage = null + return true + } else if (requestCode == 9005) { + if (resultCode == Activity.RESULT_OK && intent != null) { + val player = intent.getParcelableArrayListExtra(PlayersClient.EXTRA_PLAYER_SEARCH_RESULTS)?.first() + val result = this.result + this.result = null + if (player == null) { + result?.success(null) + } else { + val handler = CoroutineExceptionHandler { _, exception -> + result?.error( + PluginError.FailedToLoadPlayer.errorCode(), + exception.localizedMessage, + null + ) + } + + CoroutineScope(Dispatchers.Main + handler).launch { + val iconImage = if (player.iconImageUri != null) + player.iconImageUri.let { imageLoader.loadImageFromUri(activityPluginBinding.activity, it!!) } + else null + val playerData = PlayerData( + player.displayName, + player.playerId, + iconImage + ) + val gson = Gson() + val string = gson.toJson(playerData) ?: "" + result?.success(string) + } + } + } else { + result?.success(null) + } + return true + } else { + return false + } + } + //endregion + } \ No newline at end of file diff --git a/games_services/android/src/main/kotlin/com/abedalkareem/games_services/GamesServicesPlugin.kt b/games_services/android/src/main/kotlin/com/abedalkareem/games_services/GamesServicesPlugin.kt index f66529ca..ff23db02 100644 --- a/games_services/android/src/main/kotlin/com/abedalkareem/games_services/GamesServicesPlugin.kt +++ b/games_services/android/src/main/kotlin/com/abedalkareem/games_services/GamesServicesPlugin.kt @@ -28,6 +28,7 @@ class GamesServicesPlugin : FlutterPlugin, private var leaderboards: Leaderboards? = null private var achievements: Achievements? = null private var saveGame: SaveGame? = null + private var friends: Friends? = null private var auth: Auth? = null //endregion @@ -60,6 +61,7 @@ class GamesServicesPlugin : FlutterPlugin, leaderboards = null achievements = null saveGame = null + friends = null auth = null } @@ -68,6 +70,7 @@ class GamesServicesPlugin : FlutterPlugin, leaderboards = Leaderboards(activityPluginBinding) achievements = Achievements(activityPluginBinding) saveGame = SaveGame(activityPluginBinding) + friends = Friends(activityPluginBinding) auth = Auth(activityPluginBinding) // streamHandler is set here instead of `setupChannels` to ensure // auth is initialized before being set @@ -206,6 +209,27 @@ class GamesServicesPlugin : FlutterPlugin, val name = call.argument("name") ?: "" saveGame?.deleteGame(name, result) } + + Method.GetFriendsAccessStatus -> { + friends?.getFriendsAccessStatus(result) + } + + Method.LoadFriends -> { + val maxResults = call.argument("pageSize") ?: 0 + val forceRefresh = call.argument("forceRefresh") ?: false + friends?.loadFriends(activity, maxResults, forceRefresh, result) + } + + Method.ViewPlayerProfile -> { + val playerId = call.argument("playerID") ?: "" + val playerInGameName = call.argument("playerInGameName") ?: "" + val localInGameName = call.argument("localInGameName") ?: "" + friends?.viewPlayerProfile(activity, playerId, playerInGameName, localInGameName, result) + } + + Method.SearchForPlayer -> { + friends?.searchForPlayer(activity, result) + } } } //endregion diff --git a/games_services/android/src/main/kotlin/com/abedalkareem/games_services/Leaderboards.kt b/games_services/android/src/main/kotlin/com/abedalkareem/games_services/Leaderboards.kt index 9bf600ed..cc2677c8 100644 --- a/games_services/android/src/main/kotlin/com/abedalkareem/games_services/Leaderboards.kt +++ b/games_services/android/src/main/kotlin/com/abedalkareem/games_services/Leaderboards.kt @@ -259,8 +259,7 @@ class Leaderboards(private var activityPluginBinding: ActivityPluginBinding) : } } - //region onActivityResult for showLeaderboards Method - // handle result from friends list permission request + // handle result from friends list permission request and [showLeaderboards] method override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean { activityPluginBinding.removeActivityResultListener(this) return if (requestCode == 26703) { @@ -307,5 +306,4 @@ class Leaderboards(private var activityPluginBinding: ActivityPluginBinding) : } } //endregion - //endregion } diff --git a/games_services/android/src/main/kotlin/com/abedalkareem/games_services/models/Method.kt b/games_services/android/src/main/kotlin/com/abedalkareem/games_services/models/Method.kt index aef703ff..b6b66ba1 100644 --- a/games_services/android/src/main/kotlin/com/abedalkareem/games_services/models/Method.kt +++ b/games_services/android/src/main/kotlin/com/abedalkareem/games_services/models/Method.kt @@ -3,7 +3,9 @@ package com.abedalkareem.games_services.models enum class Method { Unlock, Increment, SubmitScore, ShowLeaderboards, ShowAchievements, LoadAchievements, SignIn, GetAuthCode, GetPlayerHiResImage, GetPlayerScore, - GetPlayerScoreObject, SaveGame, LoadGame, GetSavedGames, DeleteGame, LoadLeaderboardScores + GetPlayerScoreObject, SaveGame, LoadGame, GetSavedGames, DeleteGame, + LoadLeaderboardScores, GetFriendsAccessStatus, LoadFriends, ViewPlayerProfile, + SearchForPlayer } fun Method.value(): String { diff --git a/games_services/android/src/main/kotlin/com/abedalkareem/games_services/util/PluginError.kt b/games_services/android/src/main/kotlin/com/abedalkareem/games_services/util/PluginError.kt index 7c593f82..a2828d3f 100644 --- a/games_services/android/src/main/kotlin/com/abedalkareem/games_services/util/PluginError.kt +++ b/games_services/android/src/main/kotlin/com/abedalkareem/games_services/util/PluginError.kt @@ -5,7 +5,8 @@ enum class PluginError { FailedToShowAchievements, FailedToIncrementAchievements, FailedToLoadAchievements, FailedToAuthenticate, FailedToGetAuthCode, NotAuthenticated, NotSupportedForThisOSVersion, FailedToSaveGame, FailedToLoadGame, FailedToGetSavedGames, LeaderboardNotFound, - FailedToDeleteSavedGame, FailedToLoadLeaderboardScores + FailedToDeleteSavedGame, FailedToLoadLeaderboardScores, FailedToLoadFriends, FailedToLoadPlayer, + FailedToLaunchPlayerSearch } fun PluginError.errorCode(): String { @@ -81,6 +82,18 @@ fun PluginError.errorCode(): String { PluginError.FailedToLoadLeaderboardScores -> { return "failed_to_load_leaderboard_scores" } + + PluginError.FailedToLoadFriends -> { + return "failed_to_load_friends" + } + + PluginError.FailedToLoadPlayer -> { + return "failed_to_load_player" + } + + PluginError.FailedToLaunchPlayerSearch -> { + return "failed_to_launch_player_search" + } } } @@ -131,7 +144,7 @@ fun PluginError.errorMessage(): String { } PluginError.NotAuthenticated -> { - return "Player not authenticated, Please make sure to call signIn() first" + return "Player not authenticated. Please make sure to call signIn() first" } PluginError.FailedToSaveGame -> { @@ -153,5 +166,17 @@ fun PluginError.errorMessage(): String { PluginError.FailedToLoadLeaderboardScores -> { return "Failed to load leaderboard scores" } + + PluginError.FailedToLoadFriends -> { + return "Failed to load friends" + } + + PluginError.FailedToLoadPlayer -> { + return "Failed to find player by ID" + } + + PluginError.FailedToLaunchPlayerSearch -> { + return "Failed to launch player search" + } } } diff --git a/games_services/darwin/Classes/Friends.swift b/games_services/darwin/Classes/Friends.swift new file mode 100644 index 00000000..9f733fab --- /dev/null +++ b/games_services/darwin/Classes/Friends.swift @@ -0,0 +1,120 @@ +import GameKit +#if os(iOS) || os(tvOS) +import Flutter +#else +import FlutterMacOS +#endif + +class Friends: BaseGamesServices { + + func showFriendsList(result: @escaping FlutterResult) { + if #available(iOS 15.0, macOS 12.0, *) { + let viewController = GKGameCenterViewController(state: GKGameCenterViewControllerState.localPlayerFriendsList) + viewController.gameCenterDelegate = self + self.viewController?.show(viewController) + result(Messages.success) + } else { + result(PluginError.notSupportedForThisOSVersion.flutterError()) + } + } + + func getFriendsAccessStatus(result: @escaping FlutterResult) { + if #available(iOS 14.5, macOS 11.3, *) { + Task { + do { + let authorizationStatus = try await GKLocalPlayer.local.loadFriendsAuthorizationStatus() + switch(authorizationStatus) { + case .notDetermined: + result("notDetermined") + case .denied, .restricted: + result("denied") + case .authorized: + result("granted") + default: + result("unknown") + } + } catch { + result(PluginError.missingDescriptionKey.flutterError()) + } + } + } else { + result(PluginError.notSupportedForThisOSVersion.flutterError()) + } + } + + func loadFriends(result: @escaping FlutterResult) { + if #available(iOS 14.5, macOS 11.3, *) { + Task { + do { + let friends = try await GKLocalPlayer.local.loadFriends() + var items = [PlayerData]() + for player in friends { +#if os(macOS) + let imageData = try? await player.loadPhoto(for: .normal).tiffRepresentation +#else + let imageData = try? await player.loadPhoto(for: .normal).pngData() +#endif + let playerIconImage = imageData?.base64EncodedString() + items.append(PlayerData( + displayName: player.displayName, + playerID: player.gamePlayerID, + teamPlayerID: player.teamPlayerID, + iconImage: playerIconImage + )) + } + if let data = try? JSONEncoder().encode(items) { + let string = String(data: data, encoding: String.Encoding.utf8) + result(string) + } else { + result(PluginError.failedToLoadFriends.flutterError()) + } + } catch { + result(PluginError.failedToLoadFriends.flutterError()) + } + } + } else { + result(PluginError.notSupportedForThisOSVersion.flutterError()) + } + } + + func viewPlayerProfile(playerID: String, result: @escaping FlutterResult) { + if (GKLocalPlayer.local.gamePlayerID == playerID) { + let viewController = GKGameCenterViewController(state: GKGameCenterViewControllerState.localPlayerProfile) + viewController.gameCenterDelegate = self + self.viewController?.show(viewController) + result(Messages.success) + } + + if #available(iOS 18.0, macOS 15.0, *) { + GKPlayer.loadPlayers(forIdentifiers: [playerID], withCompletionHandler: { players, error in + guard let player = players?.first, error == nil else { + result(PluginError.failedToLoadPlayer.flutterError()) + return + } + let viewController = GKGameCenterViewController(player: player) + viewController.gameCenterDelegate = self + self.viewController?.show(viewController) + result(Messages.success) + }) + } else { + result(PluginError.notSupportedForThisOSVersion.flutterError()) + } + } + + func sendFriendRequest(result: @escaping FlutterResult) { + if #available(iOS 15.0, macOS 12.0, *) { + do { + guard let viewController = self.viewController as? ViewController else { + result(PluginError.failedToSendFriendRequest.flutterError()) + return + } + try GKLocalPlayer.local.presentFriendRequestCreator(from: viewController) + result(Messages.success) + } catch { + result(PluginError.failedToSendFriendRequest.flutterError()) + } + } else { + result(PluginError.notSupportedForThisOSVersion.flutterError()) + } + } +} \ No newline at end of file diff --git a/games_services/darwin/Classes/Models/Method.swift b/games_services/darwin/Classes/Models/Method.swift index e841217d..eddd1c9a 100644 --- a/games_services/darwin/Classes/Models/Method.swift +++ b/games_services/darwin/Classes/Models/Method.swift @@ -20,4 +20,9 @@ enum Method: String { case loadLeaderboardScores = "loadLeaderboardScores" case loadPreviousOccurrence = "loadPreviousOccurrence" case fetchIdentityVerificationSignature = "fetchIdentityVerificationSignature" + case showFriendsList = "showFriendsList" + case getFriendsAccessStatus = "getFriendsAccessStatus" + case loadFriends = "loadFriends" + case viewPlayerProfile = "viewPlayerProfile" + case sendFriendRequest = "sendFriendRequest" } diff --git a/games_services/darwin/Classes/SwiftGamesServicesPlugin.swift b/games_services/darwin/Classes/SwiftGamesServicesPlugin.swift index 6b2cdc57..37458ee0 100644 --- a/games_services/darwin/Classes/SwiftGamesServicesPlugin.swift +++ b/games_services/darwin/Classes/SwiftGamesServicesPlugin.swift @@ -14,6 +14,7 @@ public class SwiftGamesServicesPlugin: NSObject, FlutterPlugin { private let saveGame = SaveGame() private let achievements = Achievements() private let leaderboards = Leaderboards() + private let friends = Friends() // MARK: - FlutterPlugin @@ -96,6 +97,17 @@ public class SwiftGamesServicesPlugin: NSObject, FlutterPlugin { saveGame.deleteGame(name: name, result: result) case .fetchIdentityVerificationSignature: auth.fetchIdentityVerificationSignature(result: result) + case .showFriendsList: + friends.showFriendsList(result: result) + case .getFriendsAccessStatus: + friends.getFriendsAccessStatus(result: result) + case .loadFriends: + friends.loadFriends(result: result) + case .viewPlayerProfile: + let playerID = (arguments?["playerID"] as? String) ?? "" + friends.viewPlayerProfile(playerID: playerID, result: result) + case .sendFriendRequest: + friends.sendFriendRequest(result: result) } } diff --git a/games_services/darwin/Classes/Util/Error.swift b/games_services/darwin/Classes/Util/Error.swift index 7b493d47..f3c2b38e 100644 --- a/games_services/darwin/Classes/Util/Error.swift +++ b/games_services/darwin/Classes/Util/Error.swift @@ -17,38 +17,46 @@ enum PluginError: String { var errorDescription: String? { switch self { - case .failedToSendScore: - return "Failed to send the score" - case .failedToGetScore: - return "Failed to get the score" - case .failedToSendAchievement: - return "Failed to send the achievement" - case .failedToAuthenticate: - return "Failed to authenticate" - case .failedToGetPlayerProfileImage: - return "Failed to get player profile image" - case .notSupportedForThisOSVersion: - return "Not supported for this OS version" - case .leaderboardNotFound: - return "Leaderboard not found" - case .failedToSaveGame: - return "Failed to save game" - case .failedToLoadGame: - return "Failed to load game" - case .failedToGetSavedGames: - return "Failed to get saved games" - case .failedToDeleteSavedGame: - return "Failed to delete saved game" - case .failedToLoadAchievements: - return "Failed to get the achievements list" - case .failedToResetAchievements: - return "Failed to reset achievements" - case .failedToLoadLeaderboardScores: - return "Failed to load leaderboard scores" - case .failedToLoadPreviousOccurrence: - return "Failed to load previous occurrence" - case .failedToFetchIdentityVerification: - return "Failed to fetch identity verification signature" + case .failedToSendScore: + return "Failed to send the score" + case .failedToGetScore: + return "Failed to get the score" + case .failedToSendAchievement: + return "Failed to send the achievement" + case .failedToAuthenticate: + return "Failed to authenticate" + case .failedToGetPlayerProfileImage: + return "Failed to get player profile image" + case .notSupportedForThisOSVersion: + return "Not supported for this OS version" + case .leaderboardNotFound: + return "Leaderboard not found" + case .failedToSaveGame: + return "Failed to save game" + case .failedToLoadGame: + return "Failed to load game" + case .failedToGetSavedGames: + return "Failed to get saved games" + case .failedToDeleteSavedGame: + return "Failed to delete saved game" + case .failedToLoadAchievements: + return "Failed to get the achievements list" + case .failedToResetAchievements: + return "Failed to reset achievements" + case .failedToLoadLeaderboardScores: + return "Failed to load leaderboard scores" + case .failedToLoadPreviousOccurrence: + return "Failed to load previous occurrence" + case .failedToFetchIdentityVerification: + return "Failed to fetch identity verification signature" + case .missingDescriptionKey: + return "Please add the NSGKFriendListUsageDescription key in Info.plist" + case .failedToLoadFriends: + return "Failed to load friends" + case .failedToLoadPlayer: + return "Failed to find player by ID" + case .failedToSendFriendRequest: + return "Failed to send friend request" } } @@ -68,6 +76,10 @@ enum PluginError: String { case failedToLoadLeaderboardScores = "failed_to_load_leaderboard_scores" case failedToLoadPreviousOccurrence = "failed_to_load_previous_occurrence" case failedToFetchIdentityVerification = "failed_to_fetch_identity_verification" + case missingDescriptionKey = "missing_description_key" + case failedToLoadFriends = "failed_to_load_friends" + case failedToLoadPlayer = "failed_to_load_player" + case failedToSendFriendRequest = "failed_to_send_friend_request" func flutterError() -> FlutterError { return FlutterError(code: rawValue, diff --git a/games_services/darwin/games_services.podspec b/games_services/darwin/games_services.podspec index c32c36eb..a6a84211 100644 --- a/games_services/darwin/games_services.podspec +++ b/games_services/darwin/games_services.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'games_services' - s.version = '4.1.0' + s.version = '5.1.0' s.summary = 'A Flutter plugin to support Game Center and Google Play Games services.' s.description = <<-DESC A Flutter plugin to support Game Center and Google Play Games services. diff --git a/games_services/example/pubspec.lock b/games_services/example/pubspec.lock index 2797ff4d..a7e88d49 100644 --- a/games_services/example/pubspec.lock +++ b/games_services/example/pubspec.lock @@ -44,23 +44,22 @@ packages: path: ".." relative: true source: path - version: "5.0.0" + version: "5.1.0" games_services_platform_interface: dependency: transitive description: - name: games_services_platform_interface - sha256: e05528712f33d2e6026d9e1c084eccaf03f04549b91f7fd73c2a220db0f30909 - url: "https://pub.dev" - source: hosted - version: "5.0.0" + path: "../../games_services_platform_interface" + relative: true + source: path + version: "5.1.0" lints: dependency: transitive description: name: lints - sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.0" material_color_utilities: dependency: transitive description: diff --git a/games_services/lib/games_services.dart b/games_services/lib/games_services.dart index bfca98ab..1d5aeaa6 100644 --- a/games_services/lib/games_services.dart +++ b/games_services/lib/games_services.dart @@ -1,6 +1,7 @@ export 'package:games_services_platform_interface/models.dart'; export 'src/achievements.dart'; +export 'src/friends.dart'; export 'src/game_auth.dart'; export 'src/games_services.dart'; export 'src/leaderboards.dart'; diff --git a/games_services/lib/src/achievements.dart b/games_services/lib/src/achievements.dart index 6f36732c..b1f0071d 100644 --- a/games_services/lib/src/achievements.dart +++ b/games_services/lib/src/achievements.dart @@ -6,9 +6,8 @@ import 'package:games_services_platform_interface/models.dart'; abstract class Achievements { /// Open the device's default achievements screen. - static Future showAchievements() async { - return await GamesServicesPlatform.instance.showAchievements(); - } + static Future showAchievements() => + GamesServicesPlatform.instance.showAchievements(); /// Get achievements as a list. Use this to build a custom UI. /// To show the device's default achievements screen use [showAchievements]. @@ -21,18 +20,14 @@ abstract class Achievements { }) async { String? result = await GamesServicesPlatform.instance.loadAchievements( forceRefresh: forceRefresh, ignoreImages: ignoreImages); - if (result != null) { - Iterable items = json.decode(result) as List; - return List.from( - items.map((model) => AchievementItemData.fromJson(model)).toList()); - } - return null; + if (result == null) return null; + final items = json.decode(result) as List; + return items.map((model) => AchievementItemData.fromJson(model)).toList(); } /// It will reset the achievements. Not available on Android. - static Future resetAchievements() async { - return await GamesServicesPlatform.instance.resetAchievements(); - } + static Future resetAchievements() => + GamesServicesPlatform.instance.resetAchievements(); /// Unlock an [achievement]. /// [Achievement] takes three parameters: @@ -40,10 +35,8 @@ abstract class Achievements { /// [iOSID] the achievement ID for Game Center. /// [percentComplete] the completion percentage of the achievement, /// this parameter is optional on iOS/macOS. - static Future unlock({required Achievement achievement}) async { - return await GamesServicesPlatform.instance - .unlock(achievement: achievement); - } + static Future unlock({required Achievement achievement}) => + GamesServicesPlatform.instance.unlock(achievement: achievement); /// Increment an [achievement]. /// [Achievement] takes two parameters: @@ -51,8 +44,6 @@ abstract class Achievements { /// [steps] If the achievement is of the incremental type /// you can use this method to increment the steps. /// * only for Android (see https://developers.google.com/games/services/android/achievements#unlocking_achievements). - static Future increment({required Achievement achievement}) async { - return await GamesServicesPlatform.instance - .increment(achievement: achievement); - } + static Future increment({required Achievement achievement}) => + GamesServicesPlatform.instance.increment(achievement: achievement); } diff --git a/games_services/lib/src/friends.dart b/games_services/lib/src/friends.dart new file mode 100644 index 00000000..42de48a9 --- /dev/null +++ b/games_services/lib/src/friends.dart @@ -0,0 +1,70 @@ +import 'dart:convert'; + +import 'package:games_services/games_services.dart'; +import 'package:games_services_platform_interface/game_services_platform_interface.dart'; + +abstract class Friends { + /// Open the default friends list screen on iOS 15.0+ and MacOS 12.0+. + /// Does nothing on Android. + static Future showFriendsList() => + GamesServicesPlatform.instance.showFriendsList(); + + /// Check for access to the player's friends list. Useful for presenting + /// differing UIs based on access. Calling [loadFriends] automatically request + /// access if needed and will throw an error if access is denied. + /// + /// Supported on Android, iOS 14.5+ and macOS 11.3+. + static Future get access => + GamesServicesPlatform.instance.friendsAccess; + + /// Get the current player's friends list. + /// + /// `pageSize` will guarantee a minimum list size (or the full list if it is less) + /// on Android. The list size may be larger than `pageSize` if the data is already + /// cached. `forceRefresh` will invalidate the cache on Android, fetching + /// the latest results. These arguments have no affect on iOS. + /// + /// Supported on Android, iOS 14.5+ and macOS 11.3+. + static Future?> loadFriends({ + int pageSize = 25, + bool forceRefresh = false, + }) async { + final response = await GamesServicesPlatform.instance + .loadFriends(pageSize: pageSize, forceRefresh: forceRefresh); + if (response != null) { + Iterable items = json.decode(response) as List; + return List.from( + items.map((model) => PlayerData.fromJson(model))); + } + return null; + } + + /// View a player's profile. + /// + /// On Android, this presents a player comparison profile + /// between the current player and the player identified by the `playerID`. The + /// `playerInGameName` and `localInGameName` will be presented on this screen to + /// allow in game nicknames to carry over to Google Play Games, and any friend + /// request sent from this view will include the `localInGameName`. + /// + /// Supported on Android, iOS 18.0+ and macOS 15.0+. + static Future viewPlayerProfile({ + required String playerID, + String? playerInGameName, + String? localInGameName, + }) => + GamesServicesPlatform.instance.viewPlayerProfile( + playerID: playerID, + playerInGameName: playerInGameName, + localInGameName: localInGameName); + + /// Launch a player search UI. Only available on Android. Throws an + /// [UnimplementedError] on other platforms. + static Future searchForPlayer() => + GamesServicesPlatform.instance.searchForPlayer(); + + /// Launch the friend request UI. Only available on iOS 15.0+ and MacOS 12.0+. + /// Throws an [UnimplementedError] on other platforms. + static Future sendFriendRequest() => + GamesServicesPlatform.instance.sendFriendRequest(); +} diff --git a/games_services/lib/src/games_services.dart b/games_services/lib/src/games_services.dart index 482f6801..2b7e240d 100644 --- a/games_services/lib/src/games_services.dart +++ b/games_services/lib/src/games_services.dart @@ -6,48 +6,39 @@ export 'package:games_services_platform_interface/models.dart'; /// A helper class that contains all of the library's functions. /// This is a support class for apps that use pre-3.0 versions of the library. -/// Please consider using [GameAuth] for authentication, [Achievements] for anything related to Achievements, -/// [Leaderboards] for anything related to Leaderboards, [Player] for anything related to Player, -/// and [SaveGame] for anything related to game saves. +/// Please consider using the following specialized classes as needed: +/// [GameAuth], [Achievements], [Leaderboards], [Player], [SaveGame], and [Friends]. class GamesServices { /// Stream of the currently authenticated player. If not null, the player /// is signed in & games_services functionality is available. static Stream get player => GameAuth.player; /// Check if the current player is underage (always false on Android). - static Future get playerIsUnderage async { - return await Player.isUnderage; - } + static Future get playerIsUnderage => Player.isUnderage; /// Check if the current player is restricted from joining multiplayer games (always false on Android). - static Future get playerIsMultiplayerGamingRestricted async { - return await Player.isMultiplayerGamingRestricted; - } + static Future get playerIsMultiplayerGamingRestricted => + Player.isMultiplayerGamingRestricted; /// Check if the current player is restricted from using personalized communication on /// the device (always false on Android). - static Future get playerIsPersonalizedCommunicationRestricted async { - return await Player.isPersonalizedCommunicationRestricted; - } + static Future get playerIsPersonalizedCommunicationRestricted => + Player.isPersonalizedCommunicationRestricted; /// Sign the user into Game Center or Google Play Games. This must be called before /// taking any action (such as submitting a score or unlocking an achievement). - static Future signIn() async { - return await GameAuth.signIn(); - } + static Future signIn() => GameAuth.signIn(); /// Check to see if the user is currently signed into Game Center or Google Play Games. static Future get isSignedIn => GameAuth.isSignedIn; /// Retrieve a Google Play Games `server_auth_code` to be used by a backend, /// such as Firebase, to authenticate the user. `null` on other platforms. - static Future getAuthCode(String clientID) async => - await GameAuth.getAuthCode(clientID); + static Future getAuthCode(String clientID) => + GameAuth.getAuthCode(clientID); /// Open the device's default achievements screen. - static Future showAchievements() async { - return await Achievements.showAchievements(); - } + static Future showAchievements() => Achievements.showAchievements(); /// Get achievements as a list. Use this to build a custom UI. /// To show the device's default achievements screen use [showAchievements]. @@ -57,15 +48,13 @@ class GamesServices { static Future?> loadAchievements({ bool forceRefresh = false, bool ignoreImages = false, - }) async { - return await Achievements.loadAchievements( - forceRefresh: forceRefresh, ignoreImages: ignoreImages); - } + }) => + Achievements.loadAchievements( + forceRefresh: forceRefresh, ignoreImages: ignoreImages); /// It will reset the achievements. - static Future resetAchievements() async { - return await Achievements.resetAchievements(); - } + static Future resetAchievements() => + Achievements.resetAchievements(); /// Unlock an [achievement]. /// [Achievement] takes three parameters: @@ -74,9 +63,8 @@ class GamesServices { /// [Achievement.percentComplete] the completion percentage of the achievement, /// this parameter is optional on iOS/macOS. /// [Achievement.showsCompletionBanner] for iOS only, defaults to true - static Future unlock({required Achievement achievement}) async { - return await Achievements.unlock(achievement: achievement); - } + static Future unlock({required Achievement achievement}) => + Achievements.unlock(achievement: achievement); /// Increment an [achievement]. /// [Achievement] takes two parameters: @@ -84,20 +72,18 @@ class GamesServices { /// [Achievement.steps] If the achievement is of the incremental type /// you can use this method to increment the steps. /// * only for Android (see https://developers.google.com/games/services/android/achievements#unlocking_achievements). - static Future increment({required Achievement achievement}) async { - return await Achievements.increment(achievement: achievement); - } + static Future increment({required Achievement achievement}) => + Achievements.increment(achievement: achievement); /// Open the device's default leaderboards screen. If a leaderboard ID is provided, /// it will display the specific leaderboard, otherwise it will show the list of all leaderboards. static Future showLeaderboards({ String iOSLeaderboardID = "", String androidLeaderboardID = "", - }) async { - return await Leaderboards.showLeaderboards( - iOSLeaderboardID: iOSLeaderboardID, - androidLeaderboardID: androidLeaderboardID); - } + }) => + Leaderboards.showLeaderboards( + iOSLeaderboardID: iOSLeaderboardID, + androidLeaderboardID: androidLeaderboardID); /// Get leaderboard scores as a list. Use this to build a custom UI. /// To show the device's default leaderboards screen use [showLeaderboards]. @@ -112,91 +98,115 @@ class GamesServices { required TimeScope timeScope, bool forceRefresh = false, required int maxResults, - }) async { - return await Leaderboards.loadLeaderboardScores( - iOSLeaderboardID: iOSLeaderboardID, - androidLeaderboardID: androidLeaderboardID, - playerCentered: playerCentered, - scope: scope, - timeScope: timeScope, - maxResults: maxResults, - forceRefresh: forceRefresh); - } + }) => + Leaderboards.loadLeaderboardScores( + iOSLeaderboardID: iOSLeaderboardID, + androidLeaderboardID: androidLeaderboardID, + playerCentered: playerCentered, + scope: scope, + timeScope: timeScope, + maxResults: maxResults, + forceRefresh: forceRefresh); /// Submit a [score] to specific leaderboard. /// [Score] takes three parameters: /// [Score.androidID] the leaderboard ID for Google Play Games. /// [Score.iOSID] the leaderboard ID for Game Center. /// [Score.value] the score. - static Future submitScore({required Score score}) async { - return await Leaderboards.submitScore(score: score); - } + static Future submitScore({required Score score}) => + Leaderboards.submitScore(score: score); /// Get the current player's ID. /// On iOS/macOS the player ID is unique for your game but not other games. - static Future getPlayerID() async { - return await Player.getPlayerID(); - } + static Future getPlayerID() => Player.getPlayerID(); /// Get the current player's score for a specific leaderboard. static Future getPlayerScore({ String iOSLeaderboardID = "", String androidLeaderboardID = "", - }) async { - return await Player.getPlayerScore( - iOSLeaderboardID: iOSLeaderboardID, - androidLeaderboardID: androidLeaderboardID); - } + }) => + Player.getPlayerScore( + iOSLeaderboardID: iOSLeaderboardID, + androidLeaderboardID: androidLeaderboardID); /// Get the current player's name. /// On iOS/macOS the player's alias is provided. - static Future getPlayerName() async { - return await Player.getPlayerName(); - } + static Future getPlayerName() => Player.getPlayerName(); /// Get the player's icon-size profile image as a base64 encoded String. - static Future getPlayerIconImage() async { - return await Player.getPlayerIconImage(); - } + static Future getPlayerIconImage() => Player.getPlayerIconImage(); /// Get the player's hi-res profile image as a base64 encoded String. - static Future getPlayerHiResImage() async { - return await Player.getPlayerHiResImage(); - } + static Future getPlayerHiResImage() => Player.getPlayerHiResImage(); /// Show the Game Center Access Point for the current player. - static Future showAccessPoint(AccessPointLocation location) async { - return await Player.showAccessPoint(location); - } + static Future showAccessPoint(AccessPointLocation location) => + Player.showAccessPoint(location); /// Hide the Game Center Access Point. - static Future hideAccessPoint() async { - return await Player.hideAccessPoint(); - } + static Future hideAccessPoint() => Player.hideAccessPoint(); /// Save game with [data] and a unique [name]. /// The [name] must be between 1 and 100 non-URL-reserved characters (a-z, A-Z, 0-9, or the symbols "-", ".", "_", or "~"). static Future saveGame( - {required String data, required String name}) async { - return await SaveGame.saveGame(data: data, name: name); - } + {required String data, required String name}) => + SaveGame.saveGame(data: data, name: name); /// Load game with [name]. - static Future loadGame({required String name}) async { - return await SaveGame.loadGame(name: name); - } + static Future loadGame({required String name}) => + SaveGame.loadGame(name: name); /// Get all saved games. /// /// The `forceRefresh` argument will invalidate the cache on Android, fetching /// the latest results. It has no affect on iOS. - static Future?> getSavedGames( - {bool forceRefresh = false}) async { - return await SaveGame.getSavedGames(forceRefresh: forceRefresh); - } + static Future?> getSavedGames({bool forceRefresh = false}) => + SaveGame.getSavedGames(forceRefresh: forceRefresh); /// Delete game with [name]. - static Future deleteGame({required String name}) async { - return await SaveGame.deleteGame(name: name); - } + static Future deleteGame({required String name}) => + SaveGame.deleteGame(name: name); + + /// Open the default friends list screen on iOS/MacOS. Does nothing on Android. + static Future showFriendsList() => Friends.showFriendsList(); + + /// Check for access to the player's friends list. Useful for presenting + /// differing UIs based on access. Calling [loadFriends] automatically request + /// access if needed and will throw an error if access is denied. + static Future get friendsAccess => Friends.access; + + /// Get the current player's friends list. + /// + /// The `forceRefresh` argument will invalidate the cache on Android, fetching + /// the latest results. It has no affect on iOS. + static Future?> loadFriends({ + required int maxResults, + bool forceRefresh = false, + }) => + Friends.loadFriends(pageSize: maxResults, forceRefresh: forceRefresh); + + /// View a player's profile. Passing in the current player's ID will display + /// their profile. + /// + /// On Android, this presents a player comparison profile + /// between the current player and the player identified by the `playerID`. The + /// `playerInGameName` and `localInGameName` will be presented on this screen to + /// allow in game nicknames to carry over to Google Play Games, and any friend + /// request sent from this view will include the `localInGameName`. + static Future viewPlayerProfile( + {required String playerID, + String? playerInGameName, + String? localInGameName}) => + Friends.viewPlayerProfile( + playerID: playerID, + playerInGameName: playerInGameName, + localInGameName: localInGameName); + + /// Launch a player search UI. Only available on Android. Throws an + /// [UnimplementedError] on other platforms. + static Future searchForPlayer() => Friends.searchForPlayer(); + + /// Launch the friend request UI. Only available on iOS/MacOS. Throws an + /// [UnimplementedError] on other platforms. + static Future sendFriendRequest() => Friends.sendFriendRequest(); } diff --git a/games_services/lib/src/leaderboards.dart b/games_services/lib/src/leaderboards.dart index 8e02b1af..d8c1a599 100644 --- a/games_services/lib/src/leaderboards.dart +++ b/games_services/lib/src/leaderboards.dart @@ -8,14 +8,13 @@ abstract class Leaderboards { /// Open the device's default leaderboards screen. If a leaderboard ID is provided, /// it will display the specific leaderboard, otherwise it will show the list of all leaderboards. static Future showLeaderboards({ - String? iOSLeaderboardID = "", - String? androidLeaderboardID = "", - }) async { - return await GamesServicesPlatform.instance.showLeaderboards( - iOSLeaderboardID: iOSLeaderboardID, - androidLeaderboardID: androidLeaderboardID, - ); - } + String iOSLeaderboardID = "", + String androidLeaderboardID = "", + }) => + GamesServicesPlatform.instance.showLeaderboards( + iOSLeaderboardID: iOSLeaderboardID, + androidLeaderboardID: androidLeaderboardID, + ); /// Get leaderboard scores as a list. Use this to build a custom UI. /// To show the device's default leaderboards screen use [showLeaderboards]. @@ -23,8 +22,8 @@ abstract class Leaderboards { /// The `forceRefresh` argument will invalidate the cache on Android, fetching /// the latest results. It has no affect on iOS. static Future?> loadLeaderboardScores({ - String? iOSLeaderboardID = "", - String? androidLeaderboardID = "", + String iOSLeaderboardID = "", + String androidLeaderboardID = "", bool playerCentered = false, required PlayerScope scope, required TimeScope timeScope, @@ -40,18 +39,15 @@ abstract class Leaderboards { forceRefresh: forceRefresh, maxResults: maxResults, ); - if (response != null) { - Iterable items = json.decode(response) as List; - return List.from( - items.map((model) => LeaderboardScoreData.fromJson(model)).toList()); - } - return null; + if (response == null) return null; + final items = json.decode(response) as List; + return items.map((model) => LeaderboardScoreData.fromJson(model)).toList(); } /// Get leaderboard score data for the current player static Future getPlayerScoreObject({ - String? iOSLeaderboardID = "", - String? androidLeaderboardID = "", + String iOSLeaderboardID = "", + String androidLeaderboardID = "", required PlayerScope scope, required TimeScope timeScope, }) async { @@ -62,18 +58,19 @@ abstract class Leaderboards { scope: scope, timeScope: timeScope, ); - - return LeaderboardScoreData.fromJson(json.decode(response ?? "")); + if (response == null) return null; + return LeaderboardScoreData.fromJson(json.decode(response)); } /// Load the previous occurrence of the player's score from a leaderboard. /// Returns the score data that precedes the player's current best score. /// /// This is useful for tracking score progression over time. + /// /// Currently only supported on iOS 14.0+ and macOS 11.0+. static Future loadPreviousOccurrence({ - String? iOSLeaderboardID = "", - String? androidLeaderboardID = "", + String iOSLeaderboardID = "", + String androidLeaderboardID = "", required TimeScope timeScope, }) async { final String? response = @@ -82,20 +79,15 @@ abstract class Leaderboards { iOSLeaderboardID: iOSLeaderboardID, timeScope: timeScope, ); - - if (response == null) { - return null; - } - + if (response == null) return null; return LeaderboardScoreData.fromJson(json.decode(response)); } - /// Submit a [score] to specific leaderboard. + /// Submit a score to specific leaderboard. /// [Score] takes three parameters: - /// [androidLeaderboardID] the leaderboard ID for Google Play Games. - /// [iOSLeaderboardID] the leaderboard ID for Game Center. - /// [value] the score. - static Future submitScore({required Score score}) async { - return await GamesServicesPlatform.instance.submitScore(score: score); - } + /// `androidLeaderboardID` the leaderboard ID for Google Play Games. + /// `iOSLeaderboardID` the leaderboard ID for Game Center. + /// `value` the score. + static Future submitScore({required Score score}) => + GamesServicesPlatform.instance.submitScore(score: score); } diff --git a/games_services/lib/src/models/achievement_item_data.dart b/games_services/lib/src/models/achievement_item_data.dart index b953aee4..45075dbb 100644 --- a/games_services/lib/src/models/achievement_item_data.dart +++ b/games_services/lib/src/models/achievement_item_data.dart @@ -33,16 +33,15 @@ class AchievementItemData { required this.unlocked, }); - factory AchievementItemData.fromJson(Map json) { - return AchievementItemData( - id: json["id"], - name: json["name"], - description: json["description"], - unlockedImage: (json["unlockedImage"] as String?)?.replaceAll("\n", ""), - lockedImage: (json["lockedImage"] as String?)?.replaceAll("\n", ""), - totalSteps: json["totalSteps"], - completedSteps: json["completedSteps"], - unlocked: json["unlocked"], - ); - } + factory AchievementItemData.fromJson(Map json) => + AchievementItemData( + id: json["id"], + name: json["name"], + description: json["description"], + unlockedImage: (json["unlockedImage"] as String?)?.replaceAll("\n", ""), + lockedImage: (json["lockedImage"] as String?)?.replaceAll("\n", ""), + totalSteps: json["totalSteps"], + completedSteps: json["completedSteps"], + unlocked: json["unlocked"], + ); } diff --git a/games_services/lib/src/models/leaderboard_score_data.dart b/games_services/lib/src/models/leaderboard_score_data.dart index 72086802..ec74137e 100644 --- a/games_services/lib/src/models/leaderboard_score_data.dart +++ b/games_services/lib/src/models/leaderboard_score_data.dart @@ -1,11 +1,23 @@ import 'player_data.dart'; class LeaderboardScoreData { + /// The player's position on the leaderboard. final int rank; + + /// The formatted string representation of the score provided by the platform. + /// May contain additional formatting. final String displayScore; + + /// The player's score as an integer. final int rawScore; + + /// The timestamp in milliseconds representing when the score was achieved. final int timestampMillis; + + /// Data about the player that holds the score and rank. final PlayerData scoreHolder; + + /// Corresponds to `context` on iOS and 'scoreTag` on Android. final String? token; // provided to maintain backwards compatibility @@ -23,14 +35,13 @@ class LeaderboardScoreData { this.token, }); - factory LeaderboardScoreData.fromJson(Map json) { - return LeaderboardScoreData( - rank: json["rank"], - displayScore: json["displayScore"], - rawScore: json["rawScore"], - timestampMillis: json["timestampMillis"], - scoreHolder: PlayerData.fromJson(json["scoreHolder"]), - token: (json["token"] as String?)?.replaceAll("\n", ""), - ); - } + factory LeaderboardScoreData.fromJson(Map json) => + LeaderboardScoreData( + rank: json["rank"], + displayScore: json["displayScore"], + rawScore: json["rawScore"], + timestampMillis: json["timestampMillis"], + scoreHolder: PlayerData.fromJson(json["scoreHolder"]), + token: (json["token"] as String?)?.replaceAll("\n", ""), + ); } diff --git a/games_services/lib/src/models/player_data.dart b/games_services/lib/src/models/player_data.dart index 2f6bf936..1badc11f 100644 --- a/games_services/lib/src/models/player_data.dart +++ b/games_services/lib/src/models/player_data.dart @@ -9,4 +9,7 @@ export 'package:games_services_platform_interface/models.dart' show PlayerData; extension PlayerDataX on PlayerData { /// Retrieve player's high resolution profile photo. Future get hiResImage => Player.getPlayerHiResImage(); + + /// View the current player's profile. + Future viewProfile() => Player.viewProfile(); } diff --git a/games_services/lib/src/models/saved_game.dart b/games_services/lib/src/models/saved_game.dart index b26518e0..7fa909fb 100644 --- a/games_services/lib/src/models/saved_game.dart +++ b/games_services/lib/src/models/saved_game.dart @@ -11,10 +11,8 @@ class SavedGame { SavedGame(this.name, this.modificationDate, this.deviceName); - factory SavedGame.fromJson(Map json) { - return SavedGame( - json["name"], json["modificationDate"], json["deviceName"]); - } + factory SavedGame.fromJson(Map json) => + SavedGame(json["name"], json["modificationDate"], json["deviceName"]); Map toJson() => { "name": name, diff --git a/games_services/lib/src/player.dart b/games_services/lib/src/player.dart index 5cd7b80f..5caf50cf 100644 --- a/games_services/lib/src/player.dart +++ b/games_services/lib/src/player.dart @@ -23,45 +23,34 @@ abstract class Player { ); /// Show the Game Center Access Point for the current player. - static Future showAccessPoint(AccessPointLocation location) async { - return await GamesServicesPlatform.instance.showAccessPoint(location); - } + static Future showAccessPoint(AccessPointLocation location) => + GamesServicesPlatform.instance.showAccessPoint(location); /// Hide the Game Center Access Point. - static Future hideAccessPoint() async { - return await GamesServicesPlatform.instance.hideAccessPoint(); - } + static Future hideAccessPoint() => + GamesServicesPlatform.instance.hideAccessPoint(); /// Get the current player's ID. /// On iOS/macOS the player ID is unique for your game but not other games. static Future getPlayerID() async { final player = await _currentPlayer; - if (player == null) { - throw _notAuthenticatedError; - } else { - return player.playerID; - } + if (player == null) throw _notAuthenticatedError; + return player.playerID; } /// Get the current player's name. /// On iOS/macOS the player's alias is provided. static Future getPlayerName() async { final player = await _currentPlayer; - if (player == null) { - throw _notAuthenticatedError; - } else { - return player.displayName; - } + if (player == null) throw _notAuthenticatedError; + return player.displayName; } /// Get the player's icon-size profile image as a base64 encoded String. static Future getPlayerIconImage() async { final player = await _currentPlayer; - if (player == null) { - throw _notAuthenticatedError; - } else { - return player.iconImage; - } + if (player == null) throw _notAuthenticatedError; + return player.iconImage; } /// Get the player's hi-res profile image as a base64 encoded String. @@ -71,43 +60,41 @@ abstract class Player { /// Get the current player's score for a specific leaderboard. static Future getPlayerScore({ - String? iOSLeaderboardID = "", - String? androidLeaderboardID = "", - }) async { - return await GamesServicesPlatform.instance.getPlayerScore( - iOSLeaderboardID: iOSLeaderboardID, - androidLeaderboardID: androidLeaderboardID, - ); - } + String iOSLeaderboardID = "", + String androidLeaderboardID = "", + }) => + GamesServicesPlatform.instance.getPlayerScore( + iOSLeaderboardID: iOSLeaderboardID, + androidLeaderboardID: androidLeaderboardID, + ); /// Check if the current player is underage (always false on Android). static Future get isUnderage async { final player = await _currentPlayer; - if (player == null) { - throw _notAuthenticatedError; - } else { - return player.isUnderage; - } + if (player == null) throw _notAuthenticatedError; + return player.isUnderage; } /// Check if the current player is restricted from joining multiplayer games (always false on Android). static Future get isMultiplayerGamingRestricted async { final player = await _currentPlayer; - if (player == null) { - throw _notAuthenticatedError; - } else { - return player.isMultiplayerGamingRestricted; - } + if (player == null) throw _notAuthenticatedError; + return player.isMultiplayerGamingRestricted; } /// Check if the current player is restricted from using personalized communication on /// the device (always false on Android). static Future get isPersonalizedCommunicationRestricted async { final player = await _currentPlayer; - if (player == null) { - throw _notAuthenticatedError; - } else { - return player.isPersonalizedCommunicationRestricted; - } + if (player == null) throw _notAuthenticatedError; + return player.isPersonalizedCommunicationRestricted; + } + + /// View the current player's profile. + static Future viewProfile() async { + final player = await _currentPlayer; + if (player == null) throw _notAuthenticatedError; + return GamesServicesPlatform.instance + .viewPlayerProfile(playerID: player.playerID ?? ''); } } diff --git a/games_services/lib/src/save_game.dart b/games_services/lib/src/save_game.dart index e15a40d6..d822e499 100644 --- a/games_services/lib/src/save_game.dart +++ b/games_services/lib/src/save_game.dart @@ -9,15 +9,12 @@ abstract class SaveGame { static Future saveGame({ required String data, required String name, - }) async { - return await GamesServicesPlatform.instance - .saveGame(data: data, name: name); - } + }) => + GamesServicesPlatform.instance.saveGame(data: data, name: name); /// Load game with [name]. - static Future loadGame({required String name}) async { - return await GamesServicesPlatform.instance.loadGame(name: name); - } + static Future loadGame({required String name}) => + GamesServicesPlatform.instance.loadGame(name: name); /// Get all saved games. /// @@ -28,17 +25,12 @@ abstract class SaveGame { }) async { final result = await GamesServicesPlatform.instance .getSavedGames(forceRefresh: forceRefresh); - if (result == null) { - return null; - } - final List jsonArray = jsonDecode(result); - final savedGames = - jsonArray.map((json) => SavedGame.fromJson(json)).toList(); - return savedGames; + if (result == null) return null; + final items = jsonDecode(result) as List; + return items.map((json) => SavedGame.fromJson(json)).toList(); } /// Delete game with [name]. - static Future deleteGame({required String name}) async { - return await GamesServicesPlatform.instance.deleteGame(name: name); - } + static Future deleteGame({required String name}) => + GamesServicesPlatform.instance.deleteGame(name: name); } diff --git a/games_services/pubspec.lock b/games_services/pubspec.lock index 9a8c2fa9..bb89a2a9 100644 --- a/games_services/pubspec.lock +++ b/games_services/pubspec.lock @@ -33,19 +33,18 @@ packages: games_services_platform_interface: dependency: "direct main" description: - name: games_services_platform_interface - sha256: e05528712f33d2e6026d9e1c084eccaf03f04549b91f7fd73c2a220db0f30909 - url: "https://pub.dev" - source: hosted - version: "5.0.0" + path: "../games_services_platform_interface" + relative: true + source: path + version: "5.1.0" lints: dependency: transitive description: name: lints - sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.0" material_color_utilities: dependency: transitive description: diff --git a/games_services/pubspec.yaml b/games_services/pubspec.yaml index 28964307..8f374ba7 100644 --- a/games_services/pubspec.yaml +++ b/games_services/pubspec.yaml @@ -1,6 +1,6 @@ name: games_services description: A new Flutter plugin to support game center and google play games services. -version: 5.0.1 +version: 5.1.0 homepage: https://github.com/Abedalkareem/games_services repository: https://github.com/Abedalkareem/games_services issue_tracker: https://github.com/Abedalkareem/games_services/issues @@ -13,8 +13,8 @@ dependencies: flutter: sdk: flutter - games_services_platform_interface: ^5.0.1 - # path: ../games_services_platform_interface/ + games_services_platform_interface: # ^5.1.0 + path: ../games_services_platform_interface/ #uncomment in time of development. # git: # url: https://github.com/Abedalkareem/games_services diff --git a/games_services_platform_interface/lib/game_services_platform_interface.dart b/games_services_platform_interface/lib/game_services_platform_interface.dart index 6d6b0234..ed519d9b 100644 --- a/games_services_platform_interface/lib/game_services_platform_interface.dart +++ b/games_services_platform_interface/lib/game_services_platform_interface.dart @@ -1,10 +1,10 @@ -import 'dart:async'; - import 'package:games_services_platform_interface/models.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'src/game_services_platform_impl.dart'; +const _unimplementedMessage = "not implemented."; + abstract class GamesServicesPlatform extends PlatformInterface { /// Constructs a GamesServicesPlatform. GamesServicesPlatform() : super(token: _token); @@ -34,9 +34,8 @@ abstract class GamesServicesPlatform extends PlatformInterface { /// [steps] If the achievement is of the incremental type /// you can use this method to increment the steps. /// * only for Android (see https://developers.google.com/games/services/android/achievements#unlocking_achievements). - Future increment({required Achievement achievement}) async { - throw UnimplementedError("not implemented."); - } + Future increment({required Achievement achievement}) => + throw UnimplementedError(_unimplementedMessage); /// Unlock an [achievement]. /// [Achievement] takes three parameters: @@ -44,46 +43,40 @@ abstract class GamesServicesPlatform extends PlatformInterface { /// [iOSID] the achievement ID for Game Center. /// [percentComplete] the completion percentage of the achievement, /// this parameter is optional on iOS/macOS. - Future unlock({required Achievement achievement}) async { - throw UnimplementedError("not implemented."); - } + Future unlock({required Achievement achievement}) => + throw UnimplementedError(_unimplementedMessage); /// Submit a [score] to a specific leaderboard. /// [Score] takes three parameters: /// [androidLeaderboardID] the leaderboard ID for Google Play Games. /// [iOSLeaderboardID] the leaderboard ID for Game Center. /// [value] the score. - Future submitScore({required Score score}) async { - throw UnimplementedError("not implemented."); - } + Future submitScore({required Score score}) => + throw UnimplementedError(_unimplementedMessage); /// Open the device's default achievements screen. - Future showAchievements() async { - throw UnimplementedError("not implemented."); - } + Future showAchievements() => + throw UnimplementedError(_unimplementedMessage); /// Open the device's default leaderboards screen. If a leaderboard ID is provided, /// it will display the specific leaderboard, otherwise it will show the list of all leaderboards. Future showLeaderboards({ String iOSLeaderboardID = "", String androidLeaderboardID = "", - }) async { - throw UnimplementedError("not implemented."); - } + }) => + throw UnimplementedError(_unimplementedMessage); /// Get achievements as json data. /// To show the device's default achievements screen use [showAchievements]. Future loadAchievements({ bool forceRefresh = false, bool ignoreImages = false, - }) async { - throw UnimplementedError("not implemented."); - } + }) => + throw UnimplementedError(_unimplementedMessage); /// Reset achievements. - Future resetAchievements() async { - throw UnimplementedError("not implemented."); - } + Future resetAchievements() => + throw UnimplementedError(_unimplementedMessage); /// Get leaderboard scores as json data. /// To show the device's default leaderboards screen use [showLeaderboards]. @@ -95,9 +88,8 @@ abstract class GamesServicesPlatform extends PlatformInterface { required TimeScope timeScope, required int maxResults, bool forceRefresh = false, - }) async { - throw UnimplementedError("not implemented."); - } + }) => + throw UnimplementedError(_unimplementedMessage); /// Get leaderboard scores as a json data for current player. /// To show the prebuilt system screen use [showLeaderboards]. @@ -106,9 +98,8 @@ abstract class GamesServicesPlatform extends PlatformInterface { String androidLeaderboardID = "", required PlayerScope scope, required TimeScope timeScope, - }) async { - throw UnimplementedError("not implemented."); - } + }) => + throw UnimplementedError(_unimplementedMessage); /// Load the previous occurrence of the player's score from a leaderboard. /// Returns the score data that precedes the player's current best score. @@ -119,23 +110,19 @@ abstract class GamesServicesPlatform extends PlatformInterface { String iOSLeaderboardID = "", String androidLeaderboardID = "", required TimeScope timeScope, - }) async { - throw UnimplementedError("not implemented."); - } + }) => + throw UnimplementedError(_unimplementedMessage); /// Get the current player's score for a specific leaderboard. Future getPlayerScore({ String iOSLeaderboardID = "", String androidLeaderboardID = "", - }) async { - throw UnimplementedError("not implemented."); - } + }) => + throw UnimplementedError(_unimplementedMessage); /// Sign the user into Game Center or Google Play Games. This must be called before /// taking any action (such as submitting a score or unlocking an achievement). - Future signIn() async { - throw UnimplementedError("not implemented."); - } + Future signIn() => throw UnimplementedError(_unimplementedMessage); /// Retrieve Google Play Games [server_auth_code] to be used by an auth provider, /// such as Firebase, to authenticate the user. [null] on other platforms. @@ -143,49 +130,84 @@ abstract class GamesServicesPlatform extends PlatformInterface { String clientID, { bool forceRefreshToken = false, }) => - throw UnimplementedError("not implemented."); + throw UnimplementedError(_unimplementedMessage); /// Show the Game Center Access Point for the current player. - Future showAccessPoint(AccessPointLocation location) async { - throw UnimplementedError("not implemented."); - } + Future showAccessPoint(AccessPointLocation location) => + throw UnimplementedError(_unimplementedMessage); /// Hide the Game Center Access Point. - Future hideAccessPoint() async { - throw UnimplementedError("not implemented."); - } + Future hideAccessPoint() => + throw UnimplementedError(_unimplementedMessage); /// Get the current player's hi-res profile image as a base64 encoded String. - Future getPlayerHiResImage() async { - throw UnimplementedError("not implemented."); - } + Future getPlayerHiResImage() => + throw UnimplementedError(_unimplementedMessage); /// Save game with [data] and a unique [name]. /// The [name] must be between 1 and 100 non-URL-reserved characters (a-z, A-Z, 0-9, or the symbols "-", ".", "_", or "~"). - Future saveGame({required String data, required String name}) async { - throw UnimplementedError("not implemented."); - } + Future saveGame({required String data, required String name}) => + throw UnimplementedError(_unimplementedMessage); /// Load game with [name]. - Future loadGame({required String name}) async { - throw UnimplementedError("not implemented."); - } + Future loadGame({required String name}) => + throw UnimplementedError(_unimplementedMessage); /// Delete game with [name]. - Future deleteGame({required String name}) async { - throw UnimplementedError("not implemented."); - } + Future deleteGame({required String name}) => + throw UnimplementedError(_unimplementedMessage); /// Get all saved games. - Future getSavedGames({bool forceRefresh = false}) async { - throw UnimplementedError("not implemented."); - } + Future getSavedGames({bool forceRefresh = false}) => + throw UnimplementedError(_unimplementedMessage); /// Fetch identity verification signature from Game Center (iOS and MacOS). /// Returns identity verification data including public key URL, signature, salt, and timestamp. /// Only available on iOS and MacOS, returns null on other platforms. - Future - fetchIdentityVerificationSignature() async { - throw UnimplementedError("not implemented."); - } + Future fetchIdentityVerificationSignature() => + throw UnimplementedError(_unimplementedMessage); + + /// Open the default friends list screen on iOS/MacOS. Does nothing on Android. + Future showFriendsList() => + throw UnimplementedError(_unimplementedMessage); + + /// Check for access to the player's friends list. Useful for presenting + /// differing UIs based on access. Calling [loadFriends] automatically request + /// access if needed and will throw an error if access is denied. + Future get friendsAccess => + throw UnimplementedError(_unimplementedMessage); + + /// Get the current player's friends list. + /// + /// The `forceRefresh` argument will invalidate the cache on Android, fetching + /// the latest results. It has no affect on iOS. + Future loadFriends({ + required int pageSize, + bool forceRefresh = false, + }) => + throw UnimplementedError(_unimplementedMessage); + + /// View a player's profile. + /// + /// On Android, this presents a player comparison profile + /// between the current player and the player identified by the `playerID`. The + /// `playerInGameName` and `localInGameName` will be presented on this screen to + /// allow in game nicknames to carry over to Google Play Games, and any friend + /// request sent from this view will include the `localInGameName`. + Future viewPlayerProfile({ + required String playerID, + String? playerInGameName, + String? localInGameName, + }) => + throw UnimplementedError(_unimplementedMessage); + + /// Launch a player search UI. Only available on Android. Throws an + /// [UnimplementedError] on other platforms. + Future searchForPlayer() => + throw UnimplementedError(_unimplementedMessage); + + /// Launch the friend request UI. Only available on iOS/MacOS. Throws an + /// [UnimplementedError] on other platforms. + Future sendFriendRequest() => + throw UnimplementedError(_unimplementedMessage); } diff --git a/games_services_platform_interface/lib/models.dart b/games_services_platform_interface/lib/models.dart index 7b1cea62..76dce63b 100644 --- a/games_services_platform_interface/lib/models.dart +++ b/games_services_platform_interface/lib/models.dart @@ -1,5 +1,6 @@ export 'src/models/access_point_location.dart'; export 'src/models/achievement.dart'; +export 'src/models/friends_access.dart'; export 'src/models/identity_verification_signature.dart'; export 'src/models/leaderboard_scope.dart'; export 'src/models/leaderboard_time_scope.dart'; diff --git a/games_services_platform_interface/lib/src/game_services_platform_impl.dart b/games_services_platform_interface/lib/src/game_services_platform_impl.dart index 25b8eb9d..feac410f 100644 --- a/games_services_platform_interface/lib/src/game_services_platform_impl.dart +++ b/games_services_platform_interface/lib/src/game_services_platform_impl.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/services.dart'; +import 'package:games_services_platform_interface/src/models/friends_access.dart'; import '../game_services_platform_interface.dart'; import 'models/access_point_location.dart'; @@ -64,60 +65,53 @@ class MethodChannelGamesServices extends GamesServicesPlatform { } @override - Future unlock({required Achievement achievement}) async { - return await _methodChannel.invokeMethod("unlock", { - "achievementID": achievement.id, - "percentComplete": achievement.percentComplete, - "showsCompletionBanner": achievement.showsCompletionBanner, - }); - } + Future unlock({required Achievement achievement}) => + _methodChannel.invokeMethod("unlock", { + "achievementID": achievement.id, + "percentComplete": achievement.percentComplete, + "showsCompletionBanner": achievement.showsCompletionBanner, + }); @override - Future submitScore({required Score score}) async { - return await _methodChannel.invokeMethod("submitScore", { - "leaderboardID": score.leaderboardID, - "value": score.value, - "token": score.token, - }); - } + Future submitScore({required Score score}) => + _methodChannel.invokeMethod("submitScore", { + "leaderboardID": score.leaderboardID, + "value": score.value, + "token": score.token, + }); @override - Future increment({required Achievement achievement}) async { - return await _methodChannel.invokeMethod("increment", { - "achievementID": achievement.id, - "steps": achievement.steps, - }); - } + Future increment({required Achievement achievement}) => + _methodChannel.invokeMethod("increment", { + "achievementID": achievement.id, + "steps": achievement.steps, + }); @override - Future showAchievements() async { - return await _methodChannel.invokeMethod("showAchievements"); - } + Future showAchievements() => + _methodChannel.invokeMethod("showAchievements"); @override Future showLeaderboards({ String iOSLeaderboardID = "", String androidLeaderboardID = "", - }) async { - return await _methodChannel.invokeMethod("showLeaderboards", { - "leaderboardID": - Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID - }); - } + }) => + _methodChannel.invokeMethod("showLeaderboards", { + "leaderboardID": + Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID + }); @override Future loadAchievements({ bool forceRefresh = false, bool ignoreImages = false, - }) async { - return await _methodChannel.invokeMethod("loadAchievements", - {"forceRefresh": forceRefresh, "ignoreImages": ignoreImages}); - } + }) => + _methodChannel.invokeMethod("loadAchievements", + {"forceRefresh": forceRefresh, "ignoreImages": ignoreImages}); @override - Future resetAchievements() async { - return await _methodChannel.invokeMethod("resetAchievements"); - } + Future resetAchievements() => + _methodChannel.invokeMethod("resetAchievements"); @override Future loadLeaderboardScores({ @@ -128,26 +122,24 @@ class MethodChannelGamesServices extends GamesServicesPlatform { String androidLeaderboardID = "", bool playerCentered = false, bool forceRefresh = false, - }) async { - return await _methodChannel.invokeMethod("loadLeaderboardScores", { - "leaderboardID": - Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID, - "playerCentered": playerCentered, - "leaderboardCollection": scope.value, - "span": timeScope.value, - "maxResults": maxResults, - "forceRefresh": forceRefresh, - }); - } + }) => + _methodChannel.invokeMethod("loadLeaderboardScores", { + "leaderboardID": + Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID, + "playerCentered": playerCentered, + "leaderboardCollection": scope.value, + "span": timeScope.value, + "maxResults": maxResults, + "forceRefresh": forceRefresh, + }); @override Future getPlayerScore( - {String iOSLeaderboardID = "", String androidLeaderboardID = ""}) async { - return await _methodChannel.invokeMethod("getPlayerScore", { - "leaderboardID": - Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID - }); - } + {String iOSLeaderboardID = "", String androidLeaderboardID = ""}) => + _methodChannel.invokeMethod("getPlayerScore", { + "leaderboardID": + Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID + }); @override Future getPlayerScoreObject({ @@ -155,32 +147,28 @@ class MethodChannelGamesServices extends GamesServicesPlatform { String androidLeaderboardID = "", required PlayerScope scope, required TimeScope timeScope, - }) async { - return await _methodChannel.invokeMethod("getPlayerScoreObject", { - "leaderboardID": - Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID, - "leaderboardCollection": scope.value, - "span": timeScope.value, - }); - } + }) => + _methodChannel.invokeMethod("getPlayerScoreObject", { + "leaderboardID": + Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID, + "leaderboardCollection": scope.value, + "span": timeScope.value, + }); @override Future loadPreviousOccurrence({ String iOSLeaderboardID = "", String androidLeaderboardID = "", required TimeScope timeScope, - }) async { - return await _methodChannel.invokeMethod("loadPreviousOccurrence", { - "leaderboardID": - Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID, - "span": timeScope.value, - }); - } + }) => + _methodChannel.invokeMethod("loadPreviousOccurrence", { + "leaderboardID": + Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID, + "span": timeScope.value, + }); @override - Future signIn() async { - return await _methodChannel.invokeMethod("signIn"); - } + Future signIn() => _methodChannel.invokeMethod("signIn"); @override Future getAuthCode( @@ -195,42 +183,33 @@ class MethodChannelGamesServices extends GamesServicesPlatform { : Future.value(null); @override - Future showAccessPoint(AccessPointLocation location) async { - return await _methodChannel.invokeMethod( - "showAccessPoint", {"location": location.toString().split(".").last}); - } + Future showAccessPoint(AccessPointLocation location) => + _methodChannel.invokeMethod( + "showAccessPoint", {"location": location.toString().split(".").last}); @override - Future hideAccessPoint() async { - return await _methodChannel.invokeMethod("hideAccessPoint"); - } + Future hideAccessPoint() => + _methodChannel.invokeMethod("hideAccessPoint"); @override - Future getPlayerHiResImage() async { - return await _methodChannel.invokeMethod("getPlayerHiResImage"); - } + Future getPlayerHiResImage() => + _methodChannel.invokeMethod("getPlayerHiResImage"); @override - Future saveGame({required String data, required String name}) async { - return await _methodChannel - .invokeMethod("saveGame", {"data": data, "name": name}); - } + Future saveGame({required String data, required String name}) => + _methodChannel.invokeMethod("saveGame", {"data": data, "name": name}); @override - Future loadGame({required String name}) async { - return await _methodChannel.invokeMethod("loadGame", {"name": name}); - } + Future loadGame({required String name}) => + _methodChannel.invokeMethod("loadGame", {"name": name}); @override - Future getSavedGames({bool forceRefresh = false}) async { - return await _methodChannel - .invokeMethod("getSavedGames", {"forceRefresh": forceRefresh}); - } + Future getSavedGames({bool forceRefresh = false}) => _methodChannel + .invokeMethod("getSavedGames", {"forceRefresh": forceRefresh}); @override - Future deleteGame({required String name}) async { - return await _methodChannel.invokeMethod("deleteGame", {"name": name}); - } + Future deleteGame({required String name}) => + _methodChannel.invokeMethod("deleteGame", {"name": name}); @override Future @@ -246,4 +225,55 @@ class MethodChannelGamesServices extends GamesServicesPlatform { return IdentityVerificationSignature.fromJson( result.cast()); } + + @override + Future showFriendsList() => + _methodChannel.invokeMethod("showFriendsList"); + + @override + Future get friendsAccess async { + final status = + await _methodChannel.invokeMethod("getFriendsAccessStatus") as String?; + switch (status) { + case 'notDetermined': + return FriendsAccess.notDetermined; + case 'denied': + return FriendsAccess.denied; + case 'granted': + return FriendsAccess.granted; + default: + return FriendsAccess.unknown; + } + } + + @override + Future loadFriends({ + required int pageSize, + bool forceRefresh = false, + }) => + _methodChannel.invokeMethod("loadFriends", { + "pageSize": pageSize, + "forceRefresh": forceRefresh, + }); + + @override + Future viewPlayerProfile( + {required String playerID, + String? playerInGameName, + String? localInGameName}) => + _methodChannel.invokeMethod("viewPlayerProfile", { + "playerID": playerID, + "playerInGameName": playerInGameName, + "localInGameName": localInGameName + }); + + @override + Future searchForPlayer() async { + final json = await _methodChannel.invokeMethod("searchForPlayer"); + return json == null ? null : PlayerData.fromJson(jsonDecode(json)); + } + + @override + Future sendFriendRequest() => + _methodChannel.invokeMethod("sendFriendRequest"); } diff --git a/games_services_platform_interface/lib/src/models/achievement.dart b/games_services_platform_interface/lib/src/models/achievement.dart index b74a7ee8..81a884b0 100644 --- a/games_services_platform_interface/lib/src/models/achievement.dart +++ b/games_services_platform_interface/lib/src/models/achievement.dart @@ -7,9 +7,7 @@ class Achievement { double percentComplete; int steps; - String get id { - return Device.isPlatformAndroid ? androidID : iOSID; - } + String get id => Device.isPlatformAndroid ? androidID : iOSID; Achievement({ this.androidID = "", diff --git a/games_services_platform_interface/lib/src/models/friends_access.dart b/games_services_platform_interface/lib/src/models/friends_access.dart new file mode 100644 index 00000000..e4ce8610 --- /dev/null +++ b/games_services_platform_interface/lib/src/models/friends_access.dart @@ -0,0 +1 @@ +enum FriendsAccess { granted, denied, notDetermined, unknown } diff --git a/games_services_platform_interface/lib/src/models/identity_verification_signature.dart b/games_services_platform_interface/lib/src/models/identity_verification_signature.dart index a1952ab0..37718162 100644 --- a/games_services_platform_interface/lib/src/models/identity_verification_signature.dart +++ b/games_services_platform_interface/lib/src/models/identity_verification_signature.dart @@ -18,21 +18,18 @@ class IdentityVerificationSignature { required this.timestamp, }); - factory IdentityVerificationSignature.fromJson(Map json) { - return IdentityVerificationSignature( - publicKeyURL: json["publicKeyURL"] as String, - signature: json["signature"] as String, - salt: json["salt"] as String, - timestamp: json["timestamp"] as int, - ); - } + factory IdentityVerificationSignature.fromJson(Map json) => + IdentityVerificationSignature( + publicKeyURL: json["publicKeyURL"] as String, + signature: json["signature"] as String, + salt: json["salt"] as String, + timestamp: json["timestamp"] as int, + ); - Map toJson() { - return { - "publicKeyURL": publicKeyURL, - "signature": signature, - "salt": salt, - "timestamp": timestamp, - }; - } + Map toJson() => { + "publicKeyURL": publicKeyURL, + "signature": signature, + "salt": salt, + "timestamp": timestamp, + }; } diff --git a/games_services_platform_interface/lib/src/models/leaderboard_time_scope.dart b/games_services_platform_interface/lib/src/models/leaderboard_time_scope.dart index 5cac04f9..6e7ec337 100644 --- a/games_services_platform_interface/lib/src/models/leaderboard_time_scope.dart +++ b/games_services_platform_interface/lib/src/models/leaderboard_time_scope.dart @@ -1,7 +1,5 @@ enum TimeScope { today, week, allTime } extension TimeScopeValue on TimeScope { - int get value { - return index; - } + int get value => index; } diff --git a/games_services_platform_interface/lib/src/models/player.dart b/games_services_platform_interface/lib/src/models/player.dart index fc8f12bb..4e017656 100644 --- a/games_services_platform_interface/lib/src/models/player.dart +++ b/games_services_platform_interface/lib/src/models/player.dart @@ -26,16 +26,14 @@ class PlayerData { this.isPersonalizedCommunicationRestricted, }); - factory PlayerData.fromJson(Map json) { - return PlayerData( - playerID: json["playerID"], - displayName: json["displayName"], - iconImage: (json["iconImage"] as String?)?.replaceAll("\n", ""), - teamPlayerID: json["teamPlayerID"], - isUnderage: json["isUnderage"], - isMultiplayerGamingRestricted: json["isMultiplayerGamingRestricted"], - isPersonalizedCommunicationRestricted: - json["isPersonalizedCommunicationRestricted"], - ); - } + factory PlayerData.fromJson(Map json) => PlayerData( + playerID: json["playerID"], + displayName: json["displayName"], + iconImage: (json["iconImage"] as String?)?.replaceAll("\n", ""), + teamPlayerID: json["teamPlayerID"], + isUnderage: json["isUnderage"], + isMultiplayerGamingRestricted: json["isMultiplayerGamingRestricted"], + isPersonalizedCommunicationRestricted: + json["isPersonalizedCommunicationRestricted"], + ); } diff --git a/games_services_platform_interface/lib/src/models/score.dart b/games_services_platform_interface/lib/src/models/score.dart index 86ab2d12..9e6f87da 100644 --- a/games_services_platform_interface/lib/src/models/score.dart +++ b/games_services_platform_interface/lib/src/models/score.dart @@ -6,9 +6,12 @@ class Score { int? value; String? token; - String? get leaderboardID { - return Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID; - } + String? get leaderboardID => + Device.isPlatformAndroid ? androidLeaderboardID : iOSLeaderboardID; - Score({this.iOSLeaderboardID, this.androidLeaderboardID, this.value, this.token}); + Score( + {this.iOSLeaderboardID, + this.androidLeaderboardID, + this.value, + this.token}); } diff --git a/games_services_platform_interface/pubspec.yaml b/games_services_platform_interface/pubspec.yaml index 37f60e15..4b02c671 100644 --- a/games_services_platform_interface/pubspec.yaml +++ b/games_services_platform_interface/pubspec.yaml @@ -1,7 +1,7 @@ name: games_services_platform_interface description: A common platform interface for the games_services plugin. homepage: https://github.com/Abedalkareem/games_services -version: 5.0.1 +version: 5.1.0 environment: sdk: '>=2.12.0 <4.0.0'