@modbender/capacitor-play-games 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/CapacitorPlayGames.podspec +17 -0
- package/LICENSE +22 -0
- package/Package.swift +36 -0
- package/README.md +611 -0
- package/android/build.gradle +76 -0
- package/android/consumer-rules.pro +4 -0
- package/android/proguard-rules.pro +2 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/idleflowgames/playgames/AchievementsModule.kt +26 -0
- package/android/src/main/java/com/idleflowgames/playgames/LeaderboardsModule.kt +27 -0
- package/android/src/main/java/com/idleflowgames/playgames/Pgs.kt +48 -0
- package/android/src/main/java/com/idleflowgames/playgames/PlayGamesPlugin.kt +88 -0
- package/android/src/main/java/com/idleflowgames/playgames/SavedGamesModule.kt +119 -0
- package/android/src/main/java/com/idleflowgames/playgames/SignInModule.kt +83 -0
- package/dist/esm/definitions.d.ts +227 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +33 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +45 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +59 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +62 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/AchievementsModule.swift +73 -0
- package/ios/Plugin/LeaderboardsModule.swift +75 -0
- package/ios/Plugin/Pgs.swift +40 -0
- package/ios/Plugin/Plugin.swift +74 -0
- package/ios/Plugin/SavedGamesModule.swift +102 -0
- package/ios/Plugin/SignInModule.swift +139 -0
- package/package.json +80 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.idleflowgames.playgames
|
|
2
|
+
|
|
3
|
+
import com.getcapacitor.PluginCall
|
|
4
|
+
import com.google.android.gms.games.PlayGames
|
|
5
|
+
|
|
6
|
+
internal class AchievementsModule(plugin: PlayGamesPlugin) : PgsModule(plugin) {
|
|
7
|
+
private val client get() = PlayGames.getAchievementsClient(activity)
|
|
8
|
+
|
|
9
|
+
fun unlock(call: PluginCall) {
|
|
10
|
+
val id = call.getString("id") ?: return call.reject("missing id")
|
|
11
|
+
client.unlock(id)
|
|
12
|
+
call.resolve()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fun increment(call: PluginCall) {
|
|
16
|
+
val id = call.getString("id") ?: return call.reject("missing id")
|
|
17
|
+
val steps = call.getInt("steps") ?: return call.reject("missing steps")
|
|
18
|
+
if (steps <= 0) return call.reject("steps must be > 0")
|
|
19
|
+
client.increment(id, steps)
|
|
20
|
+
call.resolve()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fun show(call: PluginCall) {
|
|
24
|
+
plugin.launchUiIntent(client.achievementsIntent, call, "onAchievementsUiResult")
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.idleflowgames.playgames
|
|
2
|
+
|
|
3
|
+
import com.getcapacitor.PluginCall
|
|
4
|
+
import com.google.android.gms.games.PlayGames
|
|
5
|
+
|
|
6
|
+
internal class LeaderboardsModule(plugin: PlayGamesPlugin) : PgsModule(plugin) {
|
|
7
|
+
private val client get() = PlayGames.getLeaderboardsClient(activity)
|
|
8
|
+
|
|
9
|
+
fun submit(call: PluginCall) {
|
|
10
|
+
val id = call.getString("leaderboardId") ?: return call.reject("missing leaderboardId")
|
|
11
|
+
val score = call.getDouble("score") ?: return call.reject("missing score")
|
|
12
|
+
client.submitScoreImmediate(id, score.toLong())
|
|
13
|
+
.addOnSuccessListener { call.resolve() }
|
|
14
|
+
.addOnFailureListener { e ->
|
|
15
|
+
call.rejectFromException(e, "submitScore failed")
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fun show(call: PluginCall) {
|
|
20
|
+
val id = call.getString("leaderboardId") ?: return call.reject("missing leaderboardId")
|
|
21
|
+
plugin.launchUiIntent(client.getLeaderboardIntent(id), call, "onLeaderboardUiResult")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fun showAll(call: PluginCall) {
|
|
25
|
+
plugin.launchUiIntent(client.allLeaderboardsIntent, call, "onLeaderboardUiResult")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package com.idleflowgames.playgames
|
|
2
|
+
|
|
3
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
4
|
+
import com.getcapacitor.JSObject
|
|
5
|
+
import com.getcapacitor.PluginCall
|
|
6
|
+
import com.google.android.gms.common.api.ApiException
|
|
7
|
+
import com.google.android.gms.games.Player
|
|
8
|
+
import com.google.android.gms.tasks.Task
|
|
9
|
+
|
|
10
|
+
/** Shared base for the per-feature modules; carries the plugin back-reference. */
|
|
11
|
+
internal abstract class PgsModule(protected val plugin: PlayGamesPlugin) {
|
|
12
|
+
protected val activity: AppCompatActivity
|
|
13
|
+
get() = plugin.activity
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Build a JSObject with a fluent block — `jsObject { put("k", v) }`. */
|
|
17
|
+
internal inline fun jsObject(builder: JSObject.() -> Unit): JSObject =
|
|
18
|
+
JSObject().apply(builder)
|
|
19
|
+
|
|
20
|
+
/** Reject a PluginCall, exposing a GMS ApiException statusCode as the Capacitor error code. */
|
|
21
|
+
internal fun PluginCall.rejectFromException(e: Exception, fallbackMsg: String) {
|
|
22
|
+
val message = e.message ?: fallbackMsg
|
|
23
|
+
val status = (e as? ApiException)?.statusCode
|
|
24
|
+
if (status != null) reject(message, status.toString(), e) else reject(message, e)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Resolve a void-yielding Task to a PluginCall (success resolves, failure rejects). */
|
|
28
|
+
internal fun Task<*>.bind(call: PluginCall, errorMsg: String = "operation failed") {
|
|
29
|
+
addOnSuccessListener { call.resolve() }
|
|
30
|
+
addOnFailureListener { e -> call.rejectFromException(e, errorMsg) }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolve a value-yielding Task to a PluginCall, mapping the result to a JSObject. */
|
|
34
|
+
internal inline fun <T> Task<T>.bind(
|
|
35
|
+
call: PluginCall,
|
|
36
|
+
errorMsg: String = "operation failed",
|
|
37
|
+
crossinline transform: (T) -> JSObject,
|
|
38
|
+
) {
|
|
39
|
+
addOnSuccessListener { result -> call.resolve(transform(result)) }
|
|
40
|
+
addOnFailureListener { e -> call.rejectFromException(e, errorMsg) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Serialise a PGS Player as the JS-side `PlayerInfo` shape. */
|
|
44
|
+
internal fun Player.toJsObject(): JSObject = jsObject {
|
|
45
|
+
put("playerId", playerId)
|
|
46
|
+
put("displayName", displayName ?: "")
|
|
47
|
+
iconImageUri?.toString()?.let { put("avatarUrl", it) }
|
|
48
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package com.idleflowgames.playgames
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import androidx.activity.result.ActivityResult
|
|
5
|
+
import com.getcapacitor.JSObject
|
|
6
|
+
import com.getcapacitor.Plugin
|
|
7
|
+
import com.getcapacitor.PluginCall
|
|
8
|
+
import com.getcapacitor.PluginMethod
|
|
9
|
+
import com.getcapacitor.annotation.ActivityCallback
|
|
10
|
+
import com.getcapacitor.annotation.CapacitorPlugin
|
|
11
|
+
import com.google.android.gms.games.PlayGamesSdk
|
|
12
|
+
import com.google.android.gms.tasks.Task
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Capacitor 8 plugin wrapping Google Play Games Services v2. Thin dispatcher to
|
|
16
|
+
* the per-feature modules; the `@ActivityCallback` UI-result handlers live here
|
|
17
|
+
* because Capacitor requires them on the Plugin subclass.
|
|
18
|
+
*/
|
|
19
|
+
@CapacitorPlugin(name = "PlayGames")
|
|
20
|
+
class PlayGamesPlugin : Plugin() {
|
|
21
|
+
private val signIn by lazy { SignInModule(this) }
|
|
22
|
+
private val achievements by lazy { AchievementsModule(this) }
|
|
23
|
+
private val leaderboards by lazy { LeaderboardsModule(this) }
|
|
24
|
+
private val savedGames by lazy { SavedGamesModule(this) }
|
|
25
|
+
|
|
26
|
+
override fun load() {
|
|
27
|
+
super.load()
|
|
28
|
+
// Non-fatal: without Play Services, PGS calls resolve signedIn=false rather than crash.
|
|
29
|
+
runCatching { PlayGamesSdk.initialize(context) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Expose listener emission to modules; `notifyListeners` is protected. */
|
|
33
|
+
internal fun emit(event: String, data: JSObject) =
|
|
34
|
+
notifyListeners(event, data)
|
|
35
|
+
|
|
36
|
+
/** Launch a PGS UI intent (achievements / leaderboards) via the activity-result bridge. */
|
|
37
|
+
internal fun launchUiIntent(
|
|
38
|
+
intentTask: Task<Intent>,
|
|
39
|
+
call: PluginCall,
|
|
40
|
+
callbackName: String,
|
|
41
|
+
) {
|
|
42
|
+
intentTask
|
|
43
|
+
.addOnSuccessListener { intent ->
|
|
44
|
+
startActivityForResult(call, intent, callbackName)
|
|
45
|
+
}
|
|
46
|
+
.addOnFailureListener { e ->
|
|
47
|
+
call.rejectFromException(e, "PGS intent failed")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- @PluginMethod entrypoints (JS-callable surface) -------------------
|
|
52
|
+
|
|
53
|
+
@PluginMethod fun initialize(call: PluginCall) = call.resolve()
|
|
54
|
+
|
|
55
|
+
@PluginMethod fun signIn(call: PluginCall) = signIn.signIn(call)
|
|
56
|
+
@PluginMethod fun isSignedIn(call: PluginCall) = signIn.isSignedIn(call)
|
|
57
|
+
@PluginMethod fun getPlayer(call: PluginCall) = signIn.getPlayer(call)
|
|
58
|
+
@PluginMethod fun requestServerSideAccess(call: PluginCall) = signIn.requestServerSideAccess(call)
|
|
59
|
+
|
|
60
|
+
@PluginMethod fun unlockAchievement(call: PluginCall) = achievements.unlock(call)
|
|
61
|
+
@PluginMethod fun incrementAchievement(call: PluginCall) = achievements.increment(call)
|
|
62
|
+
@PluginMethod fun showAchievements(call: PluginCall) = achievements.show(call)
|
|
63
|
+
|
|
64
|
+
@PluginMethod fun submitScore(call: PluginCall) = leaderboards.submit(call)
|
|
65
|
+
@PluginMethod fun showLeaderboard(call: PluginCall) = leaderboards.show(call)
|
|
66
|
+
@PluginMethod fun showAllLeaderboards(call: PluginCall) = leaderboards.showAll(call)
|
|
67
|
+
|
|
68
|
+
@PluginMethod fun loadSnapshot(call: PluginCall) = savedGames.load(call)
|
|
69
|
+
@PluginMethod fun saveSnapshot(call: PluginCall) = savedGames.save(call)
|
|
70
|
+
@PluginMethod fun listSnapshots(call: PluginCall) = savedGames.list(call)
|
|
71
|
+
@PluginMethod fun deleteSnapshot(call: PluginCall) = savedGames.delete(call)
|
|
72
|
+
|
|
73
|
+
// ---- @ActivityCallback handlers ----------------------------------------
|
|
74
|
+
|
|
75
|
+
@Suppress("unused") // referenced by name from launchUiIntent
|
|
76
|
+
@ActivityCallback
|
|
77
|
+
private fun onAchievementsUiResult(call: PluginCall?, result: ActivityResult) =
|
|
78
|
+
resolveUiResult(call, result)
|
|
79
|
+
|
|
80
|
+
@Suppress("unused")
|
|
81
|
+
@ActivityCallback
|
|
82
|
+
private fun onLeaderboardUiResult(call: PluginCall?, result: ActivityResult) =
|
|
83
|
+
resolveUiResult(call, result)
|
|
84
|
+
|
|
85
|
+
private fun resolveUiResult(call: PluginCall?, @Suppress("UNUSED_PARAMETER") result: ActivityResult) {
|
|
86
|
+
call?.resolve()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
package com.idleflowgames.playgames
|
|
2
|
+
|
|
3
|
+
import com.getcapacitor.JSArray
|
|
4
|
+
import com.getcapacitor.JSObject
|
|
5
|
+
import com.getcapacitor.PluginCall
|
|
6
|
+
import com.google.android.gms.games.PlayGames
|
|
7
|
+
import com.google.android.gms.games.SnapshotsClient
|
|
8
|
+
import com.google.android.gms.games.snapshot.SnapshotMetadata
|
|
9
|
+
import com.google.android.gms.games.snapshot.SnapshotMetadataChange
|
|
10
|
+
import java.nio.charset.StandardCharsets
|
|
11
|
+
import java.util.concurrent.Executor
|
|
12
|
+
import java.util.concurrent.Executors
|
|
13
|
+
|
|
14
|
+
/** PGS Saved Games (Snapshots). Conflicts resolve most-recently-modified-wins (no merge). */
|
|
15
|
+
internal class SavedGamesModule(plugin: PlayGamesPlugin) : PgsModule(plugin) {
|
|
16
|
+
private val client get() = PlayGames.getSnapshotsClient(activity)
|
|
17
|
+
|
|
18
|
+
fun load(call: PluginCall) {
|
|
19
|
+
val name = call.getString("name") ?: return call.reject("missing name")
|
|
20
|
+
// Blocking snapshot I/O off the main thread (GMS Task listeners default to UI).
|
|
21
|
+
client.open(name, /* createIfNotFound = */ true, AUTO_RESOLVE)
|
|
22
|
+
.addOnSuccessListener(IO_EXECUTOR) { result ->
|
|
23
|
+
val snap = result.data
|
|
24
|
+
if (snap == null) {
|
|
25
|
+
call.resolve(jsObject { put("snapshot", JSObject.NULL) })
|
|
26
|
+
return@addOnSuccessListener
|
|
27
|
+
}
|
|
28
|
+
val bytes = snap.snapshotContents.readFully() ?: ByteArray(0)
|
|
29
|
+
val payload = jsObject {
|
|
30
|
+
put("name", snap.metadata.uniqueName)
|
|
31
|
+
put("description", snap.metadata.description ?: "")
|
|
32
|
+
put("modifiedAt", snap.metadata.lastModifiedTimestamp)
|
|
33
|
+
put("data", String(bytes, StandardCharsets.UTF_8))
|
|
34
|
+
}
|
|
35
|
+
client.discardAndClose(snap)
|
|
36
|
+
call.resolve(jsObject { put("snapshot", payload) })
|
|
37
|
+
}
|
|
38
|
+
.addOnFailureListener(IO_EXECUTOR) { e ->
|
|
39
|
+
call.rejectFromException(e, "snapshot load failed")
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fun save(call: PluginCall) {
|
|
44
|
+
val name = call.getString("name") ?: return call.reject("missing name")
|
|
45
|
+
val data = call.getString("data") ?: return call.reject("missing data")
|
|
46
|
+
val description = call.getString("description") ?: ""
|
|
47
|
+
|
|
48
|
+
// Blocking snapshot I/O off the main thread.
|
|
49
|
+
client.open(name, /* createIfNotFound = */ true, AUTO_RESOLVE)
|
|
50
|
+
.continueWithTask(IO_EXECUTOR) { task ->
|
|
51
|
+
val snap = task.result?.data
|
|
52
|
+
?: throw IllegalStateException("snapshot unavailable")
|
|
53
|
+
snap.snapshotContents.writeBytes(data.toByteArray(StandardCharsets.UTF_8))
|
|
54
|
+
val change = SnapshotMetadataChange.Builder()
|
|
55
|
+
.setDescription(description)
|
|
56
|
+
.build()
|
|
57
|
+
client.commitAndClose(snap, change)
|
|
58
|
+
}
|
|
59
|
+
.bind(call, "snapshot save failed")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fun list(call: PluginCall) {
|
|
63
|
+
client.load(/* forceReload = */ false).bind(call, "snapshot list failed") { result ->
|
|
64
|
+
val arr = JSArray()
|
|
65
|
+
result.get()?.use { buf ->
|
|
66
|
+
for (i in 0 until buf.count) arr.put(buf.get(i).toJsObject())
|
|
67
|
+
}
|
|
68
|
+
jsObject { put("snapshots", arr) }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fun delete(call: PluginCall) {
|
|
73
|
+
val name = call.getString("name") ?: return call.reject("missing name")
|
|
74
|
+
client.load(false)
|
|
75
|
+
.continueWithTask { task ->
|
|
76
|
+
val meta = task.result?.get()?.use { buf ->
|
|
77
|
+
buf.firstOrNull { it.uniqueName == name }?.freeze()
|
|
78
|
+
} ?: throw NoSuchElementException("snapshot '$name' not found")
|
|
79
|
+
client.delete(meta)
|
|
80
|
+
}
|
|
81
|
+
.bind(call, "snapshot delete failed")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private companion object {
|
|
85
|
+
const val AUTO_RESOLVE = SnapshotsClient.RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED
|
|
86
|
+
|
|
87
|
+
// Serial daemon thread for snapshot file I/O.
|
|
88
|
+
val IO_EXECUTOR: Executor = Executors.newSingleThreadExecutor { r ->
|
|
89
|
+
Thread(r, "pgs-saved-games").apply { isDaemon = true }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private fun SnapshotMetadata.toJsObject(): JSObject = jsObject {
|
|
95
|
+
put("name", uniqueName)
|
|
96
|
+
put("description", description ?: "")
|
|
97
|
+
put("modifiedAt", lastModifiedTimestamp)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** SnapshotMetadataBuffer has release() but isn't Closeable; provide a use {} of our own. */
|
|
101
|
+
private inline fun <R> com.google.android.gms.games.snapshot.SnapshotMetadataBuffer.use(
|
|
102
|
+
block: (com.google.android.gms.games.snapshot.SnapshotMetadataBuffer) -> R,
|
|
103
|
+
): R {
|
|
104
|
+
try {
|
|
105
|
+
return block(this)
|
|
106
|
+
} finally {
|
|
107
|
+
release()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private fun com.google.android.gms.games.snapshot.SnapshotMetadataBuffer.firstOrNull(
|
|
112
|
+
predicate: (SnapshotMetadata) -> Boolean,
|
|
113
|
+
): SnapshotMetadata? {
|
|
114
|
+
for (i in 0 until count) {
|
|
115
|
+
val m = get(i)
|
|
116
|
+
if (predicate(m)) return m
|
|
117
|
+
}
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package com.idleflowgames.playgames
|
|
2
|
+
|
|
3
|
+
import com.getcapacitor.PluginCall
|
|
4
|
+
import com.google.android.gms.common.ConnectionResult
|
|
5
|
+
import com.google.android.gms.common.GoogleApiAvailability
|
|
6
|
+
import com.google.android.gms.games.PlayGames
|
|
7
|
+
|
|
8
|
+
internal class SignInModule(plugin: PlayGamesPlugin) : PgsModule(plugin) {
|
|
9
|
+
private val signInClient get() = PlayGames.getGamesSignInClient(activity)
|
|
10
|
+
private val playersClient get() = PlayGames.getPlayersClient(activity)
|
|
11
|
+
|
|
12
|
+
fun signIn(call: PluginCall) {
|
|
13
|
+
val silent = call.getBoolean("silent", true) ?: true
|
|
14
|
+
// Interactive sign-in can crash uncatchably inside GMS's
|
|
15
|
+
// GamesResolutionActivity on devices with a broken Play Services install
|
|
16
|
+
// (some custom ROMs). Pre-flight the availability check and bail to
|
|
17
|
+
// signed-out rather than launch the activity. Silent auth shows no UI.
|
|
18
|
+
if (!silent) {
|
|
19
|
+
val gmsStatus = GoogleApiAvailability.getInstance()
|
|
20
|
+
.isGooglePlayServicesAvailable(activity)
|
|
21
|
+
if (gmsStatus != ConnectionResult.SUCCESS) {
|
|
22
|
+
resolveSignedOut(call)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
val task = if (silent) signInClient.isAuthenticated else signInClient.signIn()
|
|
27
|
+
task
|
|
28
|
+
.addOnSuccessListener { result ->
|
|
29
|
+
if (result.isAuthenticated) resolveWithPlayer(call) else resolveSignedOut(call)
|
|
30
|
+
}
|
|
31
|
+
.addOnFailureListener {
|
|
32
|
+
resolveSignedOut(call)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fun isSignedIn(call: PluginCall) {
|
|
37
|
+
signInClient.isAuthenticated.bind(call, "auth check failed") { result ->
|
|
38
|
+
jsObject { put("signedIn", result.isAuthenticated) }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fun requestServerSideAccess(call: PluginCall) {
|
|
43
|
+
val serverClientId = call.getString("serverClientId")
|
|
44
|
+
if (serverClientId.isNullOrEmpty()) {
|
|
45
|
+
call.reject("serverClientId is required")
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
val forceRefresh = call.getBoolean("forceRefresh", false) ?: false
|
|
49
|
+
signInClient.requestServerSideAccess(serverClientId, forceRefresh)
|
|
50
|
+
.bind(call, "server-side access failed") { authCode ->
|
|
51
|
+
jsObject { put("authCode", authCode) }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fun getPlayer(call: PluginCall) {
|
|
56
|
+
playersClient.currentPlayer.bind(call, "player lookup failed") { player ->
|
|
57
|
+
player.toJsObject()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private fun resolveWithPlayer(call: PluginCall) {
|
|
62
|
+
playersClient.currentPlayer
|
|
63
|
+
.addOnSuccessListener { player ->
|
|
64
|
+
emitAndResolve(call, jsObject {
|
|
65
|
+
put("signedIn", true)
|
|
66
|
+
put("player", player.toJsObject())
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
.addOnFailureListener {
|
|
70
|
+
// Signed in but profile lookup failed: report signed-in, no profile.
|
|
71
|
+
emitAndResolve(call, jsObject { put("signedIn", true) })
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun resolveSignedOut(call: PluginCall) {
|
|
76
|
+
emitAndResolve(call, jsObject { put("signedIn", false) })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun emitAndResolve(call: PluginCall, body: com.getcapacitor.JSObject) {
|
|
80
|
+
plugin.emit("signInStateChanged", body)
|
|
81
|
+
call.resolve(body)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { PluginListenerHandle } from "@capacitor/core";
|
|
2
|
+
/** A signed-in player's public profile. */
|
|
3
|
+
export interface PlayerInfo {
|
|
4
|
+
/** Stable, platform-assigned player id (PGS player id / GameKit `gamePlayerID`). */
|
|
5
|
+
playerId: string;
|
|
6
|
+
/** Display name as shown in Google Play Games / Game Center. */
|
|
7
|
+
displayName: string;
|
|
8
|
+
/** URL of the player's avatar image, when the platform exposes one. */
|
|
9
|
+
avatarUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Result of a sign-in attempt, or the payload of a sign-in state change. */
|
|
12
|
+
export interface SignInResult {
|
|
13
|
+
/** Whether the player is currently authenticated. */
|
|
14
|
+
signedIn: boolean;
|
|
15
|
+
/** The player profile, present only when `signedIn` is true. */
|
|
16
|
+
player?: PlayerInfo;
|
|
17
|
+
}
|
|
18
|
+
/** A saved-game snapshot together with its serialized payload. */
|
|
19
|
+
export interface Snapshot {
|
|
20
|
+
/** Stable unique name the snapshot was saved under. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Human-readable description stored with the snapshot. */
|
|
23
|
+
description: string;
|
|
24
|
+
/** Last-modified time, in epoch milliseconds. */
|
|
25
|
+
modifiedAt: number;
|
|
26
|
+
/** The serialized save payload as a UTF-8 string (encode binary yourself). */
|
|
27
|
+
data: string;
|
|
28
|
+
}
|
|
29
|
+
/** Snapshot metadata without the payload, as returned by `listSnapshots`. */
|
|
30
|
+
export interface SnapshotMeta {
|
|
31
|
+
/** Stable unique name of the snapshot. */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Human-readable description stored with the snapshot. */
|
|
34
|
+
description: string;
|
|
35
|
+
/** Last-modified time, in epoch milliseconds. */
|
|
36
|
+
modifiedAt: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A GameKit identity-verification bundle
|
|
40
|
+
* (`GKLocalPlayer.fetchItems(forIdentityVerificationSignature:)`). A third-party
|
|
41
|
+
* server verifies `signature` against the certificate at `publicKeyUrl` to trust
|
|
42
|
+
* the Game Center `playerId` without relaying it through the untrusted client.
|
|
43
|
+
*/
|
|
44
|
+
export interface IdentityVerificationSignature {
|
|
45
|
+
/** URL of Apple's public-key certificate used to verify `signature`. */
|
|
46
|
+
publicKeyUrl: string;
|
|
47
|
+
/** Base64-encoded signature over the verification payload. */
|
|
48
|
+
signature: string;
|
|
49
|
+
/** Base64-encoded random salt Apple mixed into the signed payload. */
|
|
50
|
+
salt: string;
|
|
51
|
+
/** Signature creation time, in epoch milliseconds (check freshness server-side). */
|
|
52
|
+
timestamp: number;
|
|
53
|
+
/** The player id the signature attests (GameKit `gamePlayerID`). */
|
|
54
|
+
playerId: string;
|
|
55
|
+
/** The app's bundle id, part of the signed payload. */
|
|
56
|
+
bundleId: string;
|
|
57
|
+
/** GameKit `teamPlayerID` (stable across the team's games), when available. */
|
|
58
|
+
teamPlayerId?: string;
|
|
59
|
+
/** GameKit `gamePlayerID` (stable per game), when available. */
|
|
60
|
+
gamePlayerId?: string;
|
|
61
|
+
}
|
|
62
|
+
/** Payload of the `signInStateChanged` event. */
|
|
63
|
+
export type SignInStateChangedEvent = SignInResult;
|
|
64
|
+
export interface PlayGamesPlugin {
|
|
65
|
+
/**
|
|
66
|
+
* Initialize the native games SDK. Idempotent.
|
|
67
|
+
*
|
|
68
|
+
* On Android this triggers `PlayGamesSdk.initialize`; on iOS it installs the
|
|
69
|
+
* GameKit authentication handler. Call once, after any App Tracking
|
|
70
|
+
* Transparency prompt has resolved, before the other methods.
|
|
71
|
+
*
|
|
72
|
+
* @since 0.1.0
|
|
73
|
+
*/
|
|
74
|
+
initialize(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Sign in to the platform games service.
|
|
77
|
+
*
|
|
78
|
+
* `silent` (default `true`) attempts auto sign-in with no UI; on most devices
|
|
79
|
+
* this succeeds if the player has previously authenticated this game. Pass
|
|
80
|
+
* `silent: false` to force the full interactive flow, and only in response to
|
|
81
|
+
* an explicit user gesture.
|
|
82
|
+
*
|
|
83
|
+
* @since 0.1.0
|
|
84
|
+
*/
|
|
85
|
+
signIn(opts?: {
|
|
86
|
+
silent?: boolean;
|
|
87
|
+
}): Promise<SignInResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Whether a player is currently signed in.
|
|
90
|
+
* @since 0.1.0
|
|
91
|
+
*/
|
|
92
|
+
isSignedIn(): Promise<{
|
|
93
|
+
signedIn: boolean;
|
|
94
|
+
}>;
|
|
95
|
+
/**
|
|
96
|
+
* Get the signed-in player's profile.
|
|
97
|
+
*
|
|
98
|
+
* On Android and iOS this rejects when no player is signed in. On web (the
|
|
99
|
+
* no-op fallback) it resolves an empty profile (`playerId: ""`).
|
|
100
|
+
* @since 0.1.0
|
|
101
|
+
*/
|
|
102
|
+
getPlayer(): Promise<PlayerInfo>;
|
|
103
|
+
/**
|
|
104
|
+
* Request a one-time OAuth 2.0 server auth code for the signed-in Play Games
|
|
105
|
+
* player, for a backend to exchange for the AUTHORITATIVE player id (Google Play
|
|
106
|
+
* Games Services v2 `GamesSignInClient.requestServerSideAccess`).
|
|
107
|
+
*
|
|
108
|
+
* `serverClientId` is the OAuth 2.0 **web** client id backing the game; the code
|
|
109
|
+
* is redeemed against it server-side. `forceRefresh` (default `false`) requests a
|
|
110
|
+
* fresh code even if one was recently granted.
|
|
111
|
+
*
|
|
112
|
+
* Android only. iOS rejects (unimplemented); the web fallback resolves an empty
|
|
113
|
+
* `authCode`.
|
|
114
|
+
* @since 0.2.0
|
|
115
|
+
*/
|
|
116
|
+
requestServerSideAccess(opts: {
|
|
117
|
+
serverClientId: string;
|
|
118
|
+
forceRefresh?: boolean;
|
|
119
|
+
}): Promise<{
|
|
120
|
+
authCode: string;
|
|
121
|
+
}>;
|
|
122
|
+
/**
|
|
123
|
+
* Fetch a GameKit identity-verification signature for the signed-in Game Center
|
|
124
|
+
* player (`GKLocalPlayer.fetchItems(forIdentityVerificationSignature:)`). A
|
|
125
|
+
* backend verifies the returned bundle against Apple's certificate to trust the
|
|
126
|
+
* player id rather than the untrusted client's claim. Rejects when no player is
|
|
127
|
+
* signed in.
|
|
128
|
+
*
|
|
129
|
+
* iOS only. Android rejects (unimplemented); the web fallback resolves an empty
|
|
130
|
+
* bundle.
|
|
131
|
+
* @since 0.2.0
|
|
132
|
+
*/
|
|
133
|
+
fetchIdentityVerificationSignature(): Promise<IdentityVerificationSignature>;
|
|
134
|
+
/**
|
|
135
|
+
* Unlock an achievement by its platform id (Play Console achievement id on
|
|
136
|
+
* Android, App Store Connect / Game Center id on iOS).
|
|
137
|
+
* @since 0.1.0
|
|
138
|
+
*/
|
|
139
|
+
unlockAchievement(opts: {
|
|
140
|
+
id: string;
|
|
141
|
+
}): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Increment a partial (incremental) achievement.
|
|
144
|
+
*
|
|
145
|
+
* `steps` is interpreted differently per platform: on Android (PGS) it is a
|
|
146
|
+
* discrete step count toward the achievement's Play Console step total; on
|
|
147
|
+
* iOS (GameKit) it is added to `percentComplete` as percentage points.
|
|
148
|
+
* Compute a platform-appropriate value (e.g. via `Capacitor.getPlatform()`)
|
|
149
|
+
* so progress matches on both stores.
|
|
150
|
+
* @since 0.1.0
|
|
151
|
+
*/
|
|
152
|
+
incrementAchievement(opts: {
|
|
153
|
+
id: string;
|
|
154
|
+
steps: number;
|
|
155
|
+
}): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Show the platform's native achievements UI.
|
|
158
|
+
* @since 0.1.0
|
|
159
|
+
*/
|
|
160
|
+
showAchievements(): Promise<void>;
|
|
161
|
+
/**
|
|
162
|
+
* Submit a score to a leaderboard by its platform id.
|
|
163
|
+
* @since 0.1.0
|
|
164
|
+
*/
|
|
165
|
+
submitScore(opts: {
|
|
166
|
+
leaderboardId: string;
|
|
167
|
+
score: number;
|
|
168
|
+
}): Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* Show the native UI for a single leaderboard.
|
|
171
|
+
* @since 0.1.0
|
|
172
|
+
*/
|
|
173
|
+
showLeaderboard(opts: {
|
|
174
|
+
leaderboardId: string;
|
|
175
|
+
}): Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* Show the native all-leaderboards UI.
|
|
178
|
+
* @since 0.1.0
|
|
179
|
+
*/
|
|
180
|
+
showAllLeaderboards(): Promise<void>;
|
|
181
|
+
/**
|
|
182
|
+
* Load a saved-game snapshot by its stable name. Resolves `{ snapshot: null }`
|
|
183
|
+
* when no snapshot exists for that name.
|
|
184
|
+
* @since 0.1.0
|
|
185
|
+
*/
|
|
186
|
+
loadSnapshot(opts: {
|
|
187
|
+
name: string;
|
|
188
|
+
}): Promise<{
|
|
189
|
+
snapshot: Snapshot | null;
|
|
190
|
+
}>;
|
|
191
|
+
/**
|
|
192
|
+
* Create or overwrite a saved-game snapshot. Conflicts are auto-resolved by
|
|
193
|
+
* most-recently-modified (last write wins), with no merge.
|
|
194
|
+
* @since 0.1.0
|
|
195
|
+
*/
|
|
196
|
+
saveSnapshot(opts: {
|
|
197
|
+
name: string;
|
|
198
|
+
data: string;
|
|
199
|
+
description?: string;
|
|
200
|
+
}): Promise<void>;
|
|
201
|
+
/**
|
|
202
|
+
* List metadata for all of the player's snapshots.
|
|
203
|
+
* @since 0.1.0
|
|
204
|
+
*/
|
|
205
|
+
listSnapshots(): Promise<{
|
|
206
|
+
snapshots: SnapshotMeta[];
|
|
207
|
+
}>;
|
|
208
|
+
/**
|
|
209
|
+
* Delete a saved-game snapshot by its stable name.
|
|
210
|
+
* @since 0.1.0
|
|
211
|
+
*/
|
|
212
|
+
deleteSnapshot(opts: {
|
|
213
|
+
name: string;
|
|
214
|
+
}): Promise<void>;
|
|
215
|
+
/**
|
|
216
|
+
* Listen for sign-in state changes: an interactive sign-in completing, or the
|
|
217
|
+
* player signing out of the platform service system-wide.
|
|
218
|
+
* @since 0.1.0
|
|
219
|
+
*/
|
|
220
|
+
addListener(event: "signInStateChanged", listener: (e: SignInStateChangedEvent) => void): Promise<PluginListenerHandle>;
|
|
221
|
+
/**
|
|
222
|
+
* Remove all listeners registered through this plugin.
|
|
223
|
+
* @since 0.1.0
|
|
224
|
+
*/
|
|
225
|
+
removeAllListeners(): Promise<void>;
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=definitions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,QAAA,MAAM,SAAS,iBAEb,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,SAAS,GAAG,cAAc,CAAkB,WAAW,EAAE;IAC7D,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,CAAC;CAC7D,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,CAAC"}
|