-
-
Notifications
You must be signed in to change notification settings - Fork 400
Javasteam wishlist demo #1789
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Javasteam wishlist demo #1789
Changes from all commits
ca5cd4a
a1781e5
bfe2ff4
928506f
c6a30ae
d99dfc7
8155cb8
8012c70
8a0e696
5562894
1c850bb
854a204
6747223
cea6b00
04b35d1
e634b2d
dff5bc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,37 @@ object RecommendationRepository { | |
| private const val API_URL = "https://api.gamenative.app/api/games/hero" | ||
| private const val CACHE_TTL_MS = 24L * 60L * 60L * 1000L | ||
|
|
||
| private const val MOCK_HERO_RESPONSE = false | ||
|
|
||
| internal val MOCK_HERO_JSON = """ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: This PR ships the demo scaffold inside production code: Prompt for AI agents |
||
| { | ||
| "recommendation": null, | ||
| "featured": { | ||
| "campaignId": "mock-whisk", | ||
| "title": "Whisk", | ||
| "appId": 3602270, | ||
| "developer": "Double Dusk Inc.", | ||
| "heroImageUrl": "https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/3602270/92fb97a2832c9c075165c43d14d974c730ca716b/library_hero.jpg", | ||
| "capsuleImageUrl": "https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/3602270/435512f90bdf39498f17fcbd103b19fa1223a430/library_capsule.jpg", | ||
| "screenshots": [ | ||
| "https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/3602270/e0a52a09cd85472aecfb430ad086aae040cb100c/ss_e0a52a09cd85472aecfb430ad086aae040cb100c.1920x1080.jpg", | ||
| "https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/3602270/77719570b08d1b46facf8477df20aa5b97b54573/ss_77719570b08d1b46facf8477df20aa5b97b54573.1920x1080.jpg", | ||
| "https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/3602270/08fd286888a7efb1e782a5106fdb1b1f237bcdb2/ss_08fd286888a7efb1e782a5106fdb1b1f237bcdb2.1920x1080.jpg" | ||
| ], | ||
| "tags": ["Action", "Indie"], | ||
| "status": "COMING_SOON", | ||
| "description": { | ||
| "en": "Whisk is a two-player platformer about shared movement and communication. Coordinate jumps, climbs and throws with a partner to get every Dreamcat home." | ||
| }, | ||
| "actions": [ | ||
| { "type": "WISHLIST", "url": "https://store.steampowered.com/app/3602270/", "store": "Steam", "style": "primary" }, | ||
| { "type": "GET_DEMO", "url": "https://store.steampowered.com/app/3602270/", "appId": 4320000 }, | ||
| { "type": "VISIT", "url": "https://store.steampowered.com/app/3602270/" } | ||
| ] | ||
| } | ||
| } | ||
| """.trimIndent() | ||
|
|
||
| private val json = Json { ignoreUnknownKeys = true } | ||
|
|
||
| // Latest featured from the most recent fetch. Kept in memory (not the disk cache) so the | ||
|
|
@@ -30,7 +61,7 @@ object RecommendationRepository { | |
| */ | ||
| suspend fun getHero(context: Context): HeroResponse = | ||
| withContext(Dispatchers.IO) { | ||
| val fetched = fetchRemote() | ||
| val fetched = if (MOCK_HERO_RESPONSE) parseHero(MOCK_HERO_JSON) else fetchRemote() | ||
| if (fetched != null) { | ||
| lastFeatured = fetched.featured | ||
| return@withContext HeroResponse( | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -727,6 +727,24 @@ class SteamService : Service(), IChallengeUrlChanged { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }.orEmpty() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| suspend fun isAppInLibrary(appId: Int): Boolean = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| instance?.licenseDao?.getAllLicenses()?.any { appId in it.appIds } == true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Rendering multiple demo CTAs repeatedly loads the entire license table per app, which scales with the user's full library rather than the featured items. Use a cached/indexed entitlement lookup or batch these app IDs instead. (Based on your team's feedback about scaling library lookups.) Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| suspend fun requestFreeLicense(appId: Int): Boolean = withContext(Dispatchers.IO) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val steamApps = instance?._steamApps ?: return@withContext false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val callback = steamApps.requestFreeLicense(appId).toFuture().await() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: requestFreeLicense awaits the Steam callback without a timeout, so if Steam never responds the coroutine hangs indefinitely. This call is triggered directly from a user tapping the featured CTA button, so consider wrapping the await in withTimeout(...) similar to getEncryptedAppTicket elsewhere in this file. Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Timber.i( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "requestFreeLicense($appId) -> ${callback.result}, " + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "apps=${callback.grantedApps}, packages=${callback.grantedPackages}", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| callback.result == EResult.OK && appId in callback.grantedApps | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e: Exception) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Leaving the screen while the license request is pending is treated as a failed request and can open the fallback URL from a cancelled UI coroutine. Rethrow Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Timber.e(e, "requestFreeLicense($appId) failed") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+733
to
+746
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Add a timeout to
🕐 Proposed fix to add a timeout suspend fun requestFreeLicense(appId: Int): Boolean = withContext(Dispatchers.IO) {
val steamApps = instance?._steamApps ?: return@withContext false
try {
- val callback = steamApps.requestFreeLicense(appId).toFuture().await()
+ val callback = withTimeout(15_000) {
+ steamApps.requestFreeLicense(appId).toFuture().await()
+ }
Timber.i(
"requestFreeLicense($appId) -> ${callback.result}, " +
"apps=${callback.grantedApps}, packages=${callback.grantedPackages}",
)
callback.result == EResult.OK && appId in callback.grantedApps
} catch (e: Exception) {
Timber.e(e, "requestFreeLicense($appId) failed")
false
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| suspend fun getOwnedAppDlc(appId: Int): Map<Int, DepotInfo> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val client = instance?.steamClient ?: return emptyMap() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val accountId = client.steamID?.accountID?.toInt() ?: return emptyMap() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package app.gamenative.service | ||
|
|
||
| import app.gamenative.utils.Net | ||
| import `in`.dragonbra.javasteam.enums.EResult | ||
| import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesWishlistSteamclient.CWishlist_AddToWishlist_Request | ||
| import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesWishlistSteamclient.CWishlist_RemoveFromWishlist_Request | ||
| import `in`.dragonbra.javasteam.rpc.service.Wishlist | ||
| import `in`.dragonbra.javasteam.steam.handlers.steamunifiedmessages.SteamUnifiedMessages | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.future.await | ||
| import kotlinx.coroutines.withContext | ||
| import okhttp3.HttpUrl.Companion.toHttpUrl | ||
| import okhttp3.Request | ||
| import org.json.JSONObject | ||
| import timber.log.Timber | ||
|
|
||
| object SteamWishlistService { | ||
|
|
||
| private const val TAG = "SteamWishlist" | ||
| private const val JOB_TIMEOUT_MS = 15_000L | ||
| private const val GET_URL = "https://api.steampowered.com/IWishlistService/GetWishlist/v1/" | ||
|
|
||
| sealed interface Outcome { | ||
| data object Success : Outcome | ||
| data object NoSession : Outcome | ||
| data class Failed(val result: EResult?) : Outcome | ||
| } | ||
|
|
||
| suspend fun addToWishlist(appId: Int): Outcome = withContext(Dispatchers.IO) { | ||
| val service = service() ?: return@withContext Outcome.NoSession | ||
| val request = CWishlist_AddToWishlist_Request.newBuilder().setAppid(appId).build() | ||
| runJob("AddToWishlist") { | ||
| service.addToWishlist(request).also { it.timeout = JOB_TIMEOUT_MS }.toFuture().await().result | ||
| } | ||
| } | ||
|
|
||
| suspend fun removeFromWishlist(appId: Int): Outcome = withContext(Dispatchers.IO) { | ||
| val service = service() ?: return@withContext Outcome.NoSession | ||
| val request = CWishlist_RemoveFromWishlist_Request.newBuilder().setAppid(appId).build() | ||
| runJob("RemoveFromWishlist") { | ||
| service.removeFromWishlist(request).also { it.timeout = JOB_TIMEOUT_MS }.toFuture().await().result | ||
| } | ||
| } | ||
|
|
||
| suspend fun isWishlisted(appId: Int): Boolean? = withContext(Dispatchers.IO) { | ||
| val steamId = SteamService.userSteamId?.convertToUInt64() | ||
| if (steamId == null || steamId == 0L) { | ||
| Timber.tag(TAG).w("no live steam session, cannot read wishlist") | ||
| return@withContext null | ||
| } | ||
| val url = GET_URL.toHttpUrl().newBuilder() | ||
| .addQueryParameter("steamid", steamId.toString()) | ||
| .build() | ||
| try { | ||
| Net.http.newCall(Request.Builder().url(url).build()).execute().use { res -> | ||
| if (!res.isSuccessful) { | ||
| Timber.tag(TAG).w("wishlist read failed ${res.code}") | ||
| return@use null | ||
| } | ||
| val response = JSONObject(res.body?.string().orEmpty()).optJSONObject("response") | ||
| val items = response?.optJSONArray("items") ?: return@use null | ||
| (0 until items.length()).any { items.optJSONObject(it)?.optInt("appid") == appId } | ||
| } | ||
| } catch (e: Exception) { | ||
| Timber.tag(TAG).e(e, "wishlist read failed") | ||
| null | ||
| } | ||
| } | ||
|
|
||
| private fun service(): Wishlist? { | ||
| val client = SteamService.instance?.steamClient | ||
| if (client == null) { | ||
| Timber.tag(TAG).w("no steam client") | ||
| return null | ||
| } | ||
| val unifiedMessages = client.getHandler<SteamUnifiedMessages>() | ||
| if (unifiedMessages == null) { | ||
| Timber.tag(TAG).e("SteamUnifiedMessages handler not available") | ||
| return null | ||
| } | ||
| return try { | ||
| unifiedMessages.createService(Wishlist::class.java) | ||
| } catch (t: Throwable) { | ||
| Timber.tag(TAG).e(t, "cannot create Wishlist service") | ||
| null | ||
| } | ||
| } | ||
|
|
||
| private suspend fun runJob(method: String, block: suspend () -> EResult?): Outcome = try { | ||
| val result = block() | ||
| Timber.tag(TAG).i("$method -> $result") | ||
| if (result == EResult.OK) Outcome.Success else Outcome.Failed(result) | ||
| } catch (e: Exception) { | ||
| Timber.tag(TAG).e(e, "$method failed") | ||
| Outcome.Failed(null) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,167 @@ | ||||||||||||||||||||
| package app.gamenative.ui.screen.library | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import android.content.Intent | ||||||||||||||||||||
| import androidx.annotation.StringRes | ||||||||||||||||||||
| import androidx.compose.foundation.interaction.MutableInteractionSource | ||||||||||||||||||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||||||||||||||||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||||||||||||||||||
| import androidx.compose.material3.Button | ||||||||||||||||||||
| import androidx.compose.material3.ButtonDefaults | ||||||||||||||||||||
| import androidx.compose.material3.MaterialTheme | ||||||||||||||||||||
| import androidx.compose.material3.OutlinedButton | ||||||||||||||||||||
| import androidx.compose.material3.Text | ||||||||||||||||||||
| import androidx.compose.runtime.Composable | ||||||||||||||||||||
| import androidx.compose.runtime.LaunchedEffect | ||||||||||||||||||||
| import androidx.compose.runtime.getValue | ||||||||||||||||||||
| import androidx.compose.runtime.mutableStateOf | ||||||||||||||||||||
| import androidx.compose.runtime.remember | ||||||||||||||||||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||||||||||||||||||
| import androidx.compose.runtime.setValue | ||||||||||||||||||||
| import androidx.compose.ui.Modifier | ||||||||||||||||||||
| import androidx.compose.ui.focus.FocusRequester | ||||||||||||||||||||
| import androidx.compose.ui.focus.focusRequester | ||||||||||||||||||||
| import androidx.compose.ui.platform.LocalContext | ||||||||||||||||||||
| import androidx.compose.ui.res.stringResource | ||||||||||||||||||||
| import androidx.compose.ui.text.font.FontWeight | ||||||||||||||||||||
| import androidx.compose.ui.unit.dp | ||||||||||||||||||||
| import androidx.core.net.toUri | ||||||||||||||||||||
| import app.gamenative.PrefManager | ||||||||||||||||||||
| import app.gamenative.R | ||||||||||||||||||||
| import app.gamenative.data.FeaturedCta | ||||||||||||||||||||
| import app.gamenative.service.SteamService | ||||||||||||||||||||
| import app.gamenative.service.SteamWishlistService | ||||||||||||||||||||
| import app.gamenative.ui.component.focusRing | ||||||||||||||||||||
| import app.gamenative.ui.util.SnackbarManager | ||||||||||||||||||||
| import app.gamenative.utils.ConversionTracker | ||||||||||||||||||||
| import com.posthog.PostHog | ||||||||||||||||||||
| import kotlinx.coroutines.launch | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Composable | ||||||||||||||||||||
| internal fun FeaturedCtaButton( | ||||||||||||||||||||
| action: FeaturedCta, | ||||||||||||||||||||
| campaignId: String, | ||||||||||||||||||||
| recSource: String, | ||||||||||||||||||||
| focusRequester: FocusRequester? = null, | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| val context = LocalContext.current | ||||||||||||||||||||
| val scope = rememberCoroutineScope() | ||||||||||||||||||||
| val interactionSource = remember { MutableInteractionSource() } | ||||||||||||||||||||
| val cta = remember(action) { InAppCta.forAction(action) } | ||||||||||||||||||||
| var done by remember(action) { mutableStateOf<Boolean?>(null) } | ||||||||||||||||||||
| var busy by remember(action) { mutableStateOf(false) } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| LaunchedEffect(action) { | ||||||||||||||||||||
| if (cta != null) { | ||||||||||||||||||||
| done = cta.isDone() | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| val openUrl = { context.startActivity(Intent(Intent.ACTION_VIEW, action.url.toUri())) } | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Guard
🛡️ Proposed fix- val openUrl = { context.startActivity(Intent(Intent.ACTION_VIEW, action.url.toUri())) }
+ val openUrl = {
+ try {
+ context.startActivity(Intent(Intent.ACTION_VIEW, action.url.toUri()))
+ } catch (e: ActivityNotFoundException) {
+ Timber.e(e, "no activity to open featured action url")
+ SnackbarManager.show(context.getString(R.string.featured_action_failed))
+ }
+ }Add the imports: import android.content.ActivityNotFoundException
import timber.log.Timber📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| val inert = cta != null && (busy || done == true) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| val onClick: () -> Unit = onClick@{ | ||||||||||||||||||||
| if (inert) return@onClick | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (PrefManager.usageAnalyticsEnabled) { | ||||||||||||||||||||
| PostHog.capture( | ||||||||||||||||||||
| event = "featured_action_clicked", | ||||||||||||||||||||
| properties = mapOf( | ||||||||||||||||||||
| "campaign_id" to campaignId, | ||||||||||||||||||||
| "action_label" to action.label, | ||||||||||||||||||||
| "url" to action.url, | ||||||||||||||||||||
| "source" to recSource, | ||||||||||||||||||||
| ), | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if (cta == null) { | ||||||||||||||||||||
| openUrl() | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| busy = true | ||||||||||||||||||||
| scope.launch { | ||||||||||||||||||||
| val ok = cta.run() | ||||||||||||||||||||
| busy = false | ||||||||||||||||||||
| if (ok) { | ||||||||||||||||||||
| done = true | ||||||||||||||||||||
| ConversionTracker.featuredConversion( | ||||||||||||||||||||
| campaignId = campaignId, | ||||||||||||||||||||
| actionType = action.type, | ||||||||||||||||||||
| appId = action.appId, | ||||||||||||||||||||
| source = recSource, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| SnackbarManager.show(context.getString(R.string.featured_action_failed)) | ||||||||||||||||||||
| openUrl() | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| val label = if (cta != null && done == true) stringResource(cta.doneLabelRes) else action.label | ||||||||||||||||||||
| val shape = RoundedCornerShape(12.dp) | ||||||||||||||||||||
| val buttonModifier = Modifier | ||||||||||||||||||||
| .fillMaxWidth() | ||||||||||||||||||||
| .focusRing(interactionSource, shape, width = 2.dp) | ||||||||||||||||||||
| .let { if (focusRequester != null) it.focusRequester(focusRequester) else it } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| val contentAlpha = if (inert) 0.6f else 1f | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (action.primary) { | ||||||||||||||||||||
| Button( | ||||||||||||||||||||
| onClick = onClick, | ||||||||||||||||||||
| modifier = buttonModifier, | ||||||||||||||||||||
| shape = shape, | ||||||||||||||||||||
| interactionSource = interactionSource, | ||||||||||||||||||||
| colors = ButtonDefaults.buttonColors( | ||||||||||||||||||||
| containerColor = MaterialTheme.colorScheme.primary.copy(alpha = contentAlpha), | ||||||||||||||||||||
| ), | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| Text(text = label, fontWeight = FontWeight.SemiBold) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| OutlinedButton( | ||||||||||||||||||||
| onClick = onClick, | ||||||||||||||||||||
| modifier = buttonModifier, | ||||||||||||||||||||
| shape = shape, | ||||||||||||||||||||
| interactionSource = interactionSource, | ||||||||||||||||||||
| colors = ButtonDefaults.outlinedButtonColors( | ||||||||||||||||||||
| contentColor = MaterialTheme.colorScheme.primary.copy(alpha = contentAlpha), | ||||||||||||||||||||
| ), | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| Text(text = label, fontWeight = FontWeight.SemiBold) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private sealed class InAppCta( | ||||||||||||||||||||
| val appId: Int, | ||||||||||||||||||||
| @StringRes val doneLabelRes: Int, | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| abstract suspend fun isDone(): Boolean? | ||||||||||||||||||||
|
|
||||||||||||||||||||
| abstract suspend fun run(): Boolean | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private class Wishlist(appId: Int) : InAppCta(appId, R.string.featured_action_wishlisted) { | ||||||||||||||||||||
| override suspend fun isDone(): Boolean? = SteamWishlistService.isWishlisted(appId) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| override suspend fun run(): Boolean = | ||||||||||||||||||||
| SteamWishlistService.addToWishlist(appId) is SteamWishlistService.Outcome.Success | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private class GetDemo(appId: Int) : InAppCta(appId, R.string.featured_action_in_library) { | ||||||||||||||||||||
| override suspend fun isDone(): Boolean = SteamService.isAppInLibrary(appId) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| override suspend fun run(): Boolean = SteamService.requestFreeLicense(appId) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| companion object { | ||||||||||||||||||||
| fun forAction(action: FeaturedCta): InAppCta? { | ||||||||||||||||||||
| val appId = action.appId ?: return null | ||||||||||||||||||||
| return when (action.type) { | ||||||||||||||||||||
| "WISHLIST" -> Wishlist(appId) | ||||||||||||||||||||
| "GET_DEMO" -> GetDemo(appId) | ||||||||||||||||||||
| else -> null | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: This edit bumps only the inactive
localBuildbranch (localBuild=false), so it has no effect on the actual dependency resolution — the activeelsebranch useslibs.javasteamfrom the version catalog, which is already at 1.8.0.1-26-SNAPSHOT. The local-build paths are now one version behind the catalog, creating a confusing mismatch for anyone who flips localBuild on. Consider instead bumping the version in gradle/libs.versions.toml (the path that actually affects the build), or dropping this dead-branch edit to keep the diff scoped to functional changes.Prompt for AI agents