Skip to content

Javasteam wishlist demo - #1789

Merged
utkarshdalal merged 17 commits into
masterfrom
javasteam-wishlist-demo
Aug 2, 2026
Merged

Javasteam wishlist demo#1789
utkarshdalal merged 17 commits into
masterfrom
javasteam-wishlist-demo

Conversation

@utkarshdalal

@utkarshdalal utkarshdalal commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Description

Added ability to wishlist games and add demos directly from the app

Recording

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Adds in-app wishlist and demo CTAs for sponsored campaigns. Actions run over the live javasteam session, show “Wishlisted”/“In Library,” and fall back to the store page on failure.

  • New Features

    • SteamWishlistService: add/remove/read wishlist via CM.
    • SteamService: isAppInLibrary and requestFreeLicense for demos/F2P.
    • FeaturedCtaButton: runs WISHLIST and GET_DEMO in-app; others deep-link; buttons stay focusable and dim when busy/done.
    • Data model updates: FeaturedItem/FeaturedAction gain appId; FeaturedCta gains type and appId.
    • Conversion counting via ConversionTracker.featuredConversion.
    • Controller navigation: focus lands on the primary CTA or Buy; visible focus rings.
    • Optional mock /api/games/hero JSON and MockHeroResponseTest to validate payload decoding.
    • New CTA strings and translations across supported locales.
  • Dependencies

    • Bump javasteam to 1.8.0.1-26-SNAPSHOT; local javasteam builds off by default.

Written for commit dff5bc7. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Featured games now support wishlist actions, demo downloads, library checks, and store-page fallback.
    • Added improved campaign app linking, action states, and conversion tracking.
    • Added controller-friendly focus handling and visual focus indicators.
  • Localization

    • Added translations for featured-game actions across supported languages.
  • Bug Fixes

    • Improved handling of failed in-app actions and unavailable Steam services.

Utkarsh Dalal added 16 commits July 31, 2026 23:49
Adds SteamWishlistService covering IWishlistService add/remove/read. The stored
session token comes from a client login and may not carry the audience the web
endpoints want, so writes try it first and fall back to a web-audience token
minted from the refresh token; both paths log which one worked so we can settle
which route Steam actually accepts.

A featured WISHLIST action with an appid now wishlists in place and renders as
Wishlisted when the game is already on the list. Everything else still opens the
store page, and a failed call falls back to that too, so a bad token degrades to
the old behaviour rather than a dead button. FeaturedItem gains an appId because
a campaign previously had no way to name its Steam app.

USE_LOCAL_FEATURED serves a hardcoded Whisk campaign so this can be exercised
without a server-side campaign. It needs to go back off before this ships, and
the hero endpoint needs appId in its schema for a real one to work.
The web-token approach could not work: the app only persists access/refresh tokens
when rememberSession is set, so on this device PrefManager had no token and a steamid
of 0, and every call fell straight through to opening the store page.

Writes now go out on the already-authenticated JavaSteam connection via a Wishlist
UnifiedService stub, modelled on CloudConfigStoreService, so no token is involved at
all. The read still uses the public web endpoint since it only needs a steamid, taken
from the live session rather than prefs; a private wishlist stays unreadable and
reports unknown rather than "not wishlisted".

Requires the wishlist protos, so localBuild is on and points at the local JavaSteam
-23 jar. That has to go back to a published build before this merges.
The CM does route Wishlist.AddToWishlist, but a plain client session comes back
AccessDenied, and the CM proto header has no access_token field to carry a web
identity. So the write now falls back to the web endpoint using a token minted from
the session refresh token via Authentication.GenerateAccessTokenForApp over the CM.

That token was previously unavailable: PrefManager only stores it when rememberSession
is set. SteamService now keeps it in memory for the session, without persisting it.
The proto now carries its service block, so JavaSteam generates the stub and the
hand-written one here is redundant. Points localBuild at the -25 jars, since the
proto branch is based on jt/gamenative-latest rather than the older -23 line.
Pulls the inline wishlist button logic out of RecommendedGameScreen into
FeaturedCtaButton, which renders any featured action: types with an in-app handler
(WISHLIST, GET_DEMO) run on-device with probe/busy/done states and fall back to the
action URL on failure; everything else deep-links as before. A campaign composes any
subset of actions, so wishlist-only, demo-only, or both need no client changes, and a
new in-app type is one InAppCta entry.

GET_DEMO requests a free license over the CM (SteamService.requestFreeLicense) and
shows In Library once licenses reflect it. Actions gain an optional per-action appId
since a demo is its own app; it falls back to the campaign appId. The local test
campaign borrows BZZZT Demo to exercise the path, as Whisk has no demo.
Whisk does ship a demo (appid 4320000, per the store's demos field); the BZZZT
borrow was based on not having checked that field.
featured_conversion fires on every confirmed in-app CTA success (wishlist added, demo
granted) — always, since campaign billing needs a complete count, but consent decides
its shape: opted-in users send a normal identified event, opted-out users a personless
one under a single-use random id, carrying only campaign_id/action_type/app_id/source.
Country breakdowns come from PostHog's server-side GeoIP either way. The gated
featured_action_clicked behavioral event is unchanged.

JavaSteam wishlist protos are merged upstream and published (1.8.0.1-26-SNAPSHOT), so
localBuild goes back off and the version bumps from -24.
The hardcoded Whisk campaign stays available for testing behind USE_LOCAL_FEATURED;
the hero endpoint is live again.
The hand-built FeaturedItem test campaign skipped deserialization — the layer that
actually breaks on a client/server schema mismatch. MOCK_HERO_RESPONSE now feeds a
verbatim /api/games/hero JSON payload through parseHero, so the mock exercises
everything a real server response would except the socket. Doubles as the payload
contract for the backend campaign schema.
The two per-action failure strings collapse into one generic featured_action_failed
(the fallback behavior is identical), and the demo-added snackbar goes away — the
button flipping to In Library is the confirmation. The surviving four new strings
(wishlisted, get-demo, in-library, failed) are translated into all 14 locales that
carry the featured block.
capsuleImageUrl pointed at the 231x87 store thumbnail rather than the vertical
library capsule the grid card expects, so the card rendered a tiny stretched strip.
heroImageUrl had the same problem one size up (460x215 header behind a 280dp hero).
Both now use the library assets from the store's asset manifest; Whisk is a recent
app, so its art lives under hashed paths and the usual unhashed URLs 404.
D-pad input had no anchor: nothing on the screen ever took focus, so controller
navigation was dead on arrival. Focus now lands on the primary action when the screen
opens (first featured CTA, or the buy button), the same pattern LibraryAppScreen uses
for its play button. CTA buttons, the buy button, and the back arrow get the shared
focusRing so the focused element is actually visible.
The mock is also the backend payload contract, so a schema drift between it and the
data classes should fail in CI rather than on a device.
A disabled button is not focusable, so finishing a wishlist or demo action dropped
controller focus with nothing to fall back to and left the screen unnavigable — the
same happened for the moment the action was in flight. The buttons now stay enabled
and swallow taps while busy or done, dimming instead of greying out.
It was flipped on for device testing and got swept into the strings commit.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds app-specific featured CTA data, Steam wishlist and free-license actions, controller focus handling, conversion tracking, localized action states, mock hero data, and updated JavaSteam dependencies.

Changes

Featured campaign CTA

Layer / File(s) Summary
Campaign contracts and mock payload
app/src/main/java/app/gamenative/data/Featured.kt, app/src/main/java/app/gamenative/data/RecommendedGame.kt, app/src/main/java/app/gamenative/data/RecommendationRepository.kt, app/src/test/java/app/gamenative/data/MockHeroResponseTest.kt
Featured campaigns and CTAs now carry optional app IDs and action types. Demo labels are localized. A mock hero response and deserialization test cover the featured payload.
Steam wishlist and license actions
app/src/main/java/app/gamenative/service/SteamService.kt, app/src/main/java/app/gamenative/service/SteamWishlistService.kt, app/build.gradle.kts, gradle/libs.versions.toml
Steam services now check library membership, request free licenses, and manage wishlist operations. JavaSteam and depot downloader snapshots are updated.
CTA rendering, tracking, focus, and localization
app/src/main/java/app/gamenative/ui/screen/library/FeaturedCtaButton.kt, app/src/main/java/app/gamenative/ui/screen/library/RecommendedGameScreen.kt, app/src/main/java/app/gamenative/utils/ConversionTracker.kt, app/src/main/res/values*/strings.xml
Featured CTAs execute supported Steam actions, track conversions, show loading and failure states, fall back to store URLs, and support controller focus. Action strings were added for the supported locales.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RecommendedGameScreen
  participant FeaturedCtaButton
  participant SteamWishlistService
  participant SteamService
  participant ConversionTracker

  RecommendedGameScreen->>FeaturedCtaButton: render featured CTA
  FeaturedCtaButton->>SteamWishlistService: execute wishlist action
  FeaturedCtaButton->>SteamService: request demo license
  SteamWishlistService-->>FeaturedCtaButton: return wishlist outcome
  SteamService-->>FeaturedCtaButton: return license result
  FeaturedCtaButton->>ConversionTracker: record successful conversion
  FeaturedCtaButton-->>RecommendedGameScreen: update CTA state or open store URL
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main wishlist and demo functionality added by the pull request.
Description check ✅ Passed The description explains the main changes and includes all template sections, but it does not provide a recording or confirm project-scope alignment.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch javasteam-wishlist-demo

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/SteamWishlistService.kt (1)

99-106: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Rethrow CancellationException in runJob.

catch (e: Exception) also catches CancellationException. That converts coroutine cancellation into Outcome.Failed(null) and logs it as an error. Rethrow cancellation so the caller's scope cancels normally.

♻️ Proposed fix
+import kotlinx.coroutines.CancellationException
+
 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: CancellationException) {
+    throw e
 } catch (e: Exception) {
     Timber.tag(TAG).e(e, "$method failed")
     Outcome.Failed(null)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/service/SteamWishlistService.kt` around
lines 99 - 106, Update SteamWishlistService.runJob to catch
CancellationException separately and rethrow it before the general Exception
handler; preserve the existing failure logging and Outcome.Failed(null) behavior
for other exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 736-749: Wrap the awaited Steam callback in requestFreeLicense
with the same 5,000 ms withTimeout guard used by getEncryptedAppTicket,
preserving the existing success logging, result validation, and exception
fallback to false.
- Around line 730-734: The GetDemo completion flow must preserve the successful
button state after requestFreeLicense succeeds, even while licenseDao has not
yet received the updated license list. Update the GetDemo-specific done/isDone
handling around SteamService.isAppInLibrary so later reads cannot reset a
locally successful result; if rejection of a previously accepted license must be
detected, implement that consistency check in a separate GetDemo path rather
than changing isAppInLibrary globally.

In `@app/src/main/java/app/gamenative/ui/screen/library/FeaturedCtaButton.kt`:
- Line 65: Update the openUrl lambda in FeaturedCtaButton to catch
ActivityNotFoundException around context.startActivity, including malformed or
unresolvable URLs, and log the failure with Timber instead of allowing the app
to crash. Preserve its use as the fallback for failed in-app actions.

In `@app/src/main/java/app/gamenative/utils/ConversionTracker.kt`:
- Around line 26-35: Update the disabled-analytics branch in the conversion
tracking flow to call PostHog.capture without the custom distinctId argument.
Keep setting properties["$process_person_profile"] to false and rely on the
SDK’s anonymous distinct ID while preserving the existing "featured_conversion"
event and properties.

In `@app/src/main/res/values-zh-rTW/strings.xml`:
- Line 2084: Update the featured_action_in_library translation to use the file’s
established “遊戲庫” term instead of “收藏庫,” matching app_library and
destination_library while preserving the existing meaning.

In `@gradle/libs.versions.toml`:
- Line 15: Update the javasteam dependency version in the version catalog to a
resolvable published artifact, or ensure the referenced gamenative-latest build
is actually published and available through the configured repositories; do not
leave the dependency pointing to the unavailable 1.8.0.1-26-SNAPSHOT artifact.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/service/SteamWishlistService.kt`:
- Around line 99-106: Update SteamWishlistService.runJob to catch
CancellationException separately and rethrow it before the general Exception
handler; preserve the existing failure logging and Outcome.Failed(null) behavior
for other exceptions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e5cd98b-f6aa-46ad-816f-744942136ead

📥 Commits

Reviewing files that changed from the base of the PR and between 155ad56 and e634b2d.

📒 Files selected for processing (26)
  • app/build.gradle.kts
  • app/src/main/java/app/gamenative/data/Featured.kt
  • app/src/main/java/app/gamenative/data/RecommendationRepository.kt
  • app/src/main/java/app/gamenative/data/RecommendedGame.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/service/SteamWishlistService.kt
  • app/src/main/java/app/gamenative/ui/screen/library/FeaturedCtaButton.kt
  • app/src/main/java/app/gamenative/ui/screen/library/RecommendedGameScreen.kt
  • app/src/main/java/app/gamenative/utils/ConversionTracker.kt
  • app/src/main/res/values-da/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-ro/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/test/java/app/gamenative/data/MockHeroResponseTest.kt
  • gradle/libs.versions.toml

Comment on lines +730 to +734
/** Whether any owned license grants [appId]. Follows the license list, so a freshly
* granted free license flips this once Steam pushes the updated list. */
suspend fun isAppInLibrary(appId: Int): Boolean =
instance?.licenseDao?.getAllLicenses()?.any { appId in it.appIds } == true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect how FeaturedCtaButton reconciles run()/isDone() state after a successful requestFreeLicense call.
ast-grep run --pattern 'class $_ {
  $$$
  override suspend fun run(): Boolean = $$$
  $$$
}' --lang kotlin app/src/main/java/app/gamenative/ui/screen/library/FeaturedCtaButton.kt

rg -n -B3 -A15 'isDone|fun run\(\)' app/src/main/java/app/gamenative/ui/screen/library/FeaturedCtaButton.kt

Repository: utkarshdalal/GameNative

Length of output: 2127


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== FeaturedCtaButton relevant sections =="
wc -l app/src/main/java/app/gamenative/ui/screen/library/FeaturedCtaButton.kt
sed -n '1,120p' app/src/main/java/app/gamenative/ui/screen/library/FeaturedCtaButton.kt

echo
echo "== SteamService relevant sections =="
wc -l app/src/main/java/app/gamenative/service/SteamService.kt
sed -n '660,820p' app/src/main/java/app/gamenative/service/SteamService.kt

echo
echo "== CTA data/types =="
rg -n -B4 -A20 'data class FeaturedCta|enum class FeaturedCta|FeaturedCtaButton|sealed class Inapp|data class InApp' app/src/main/java/app/gamenative -g '*.kt'

Repository: utkarshdalal/GameNative

Length of output: 19158


Handle license propagation for GetDemo completion.

FeaturedCtaButton sets done = true immediately when requestFreeLicense(appId) returns true, but SteamService.isAppInLibrary(appId) still reads the cached license list and can return false before Steam updates it. For the GetDemo action, keep the button done/success-only when local run() succeeds; do not let later isDone() reads that still read licenseDao flip the UI back to "not in library". If the server/client can reject a license that was previously accepted, keep a success-consistency check in a separate GetDemo path instead of reusing isAppInLibrary(appId) unchanged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/service/SteamService.kt` around lines 730 -
734, The GetDemo completion flow must preserve the successful button state after
requestFreeLicense succeeds, even while licenseDao has not yet received the
updated license list. Update the GetDemo-specific done/isDone handling around
SteamService.isAppInLibrary so later reads cannot reset a locally successful
result; if rejection of a previously accepted license must be detected,
implement that consistency check in a separate GetDemo path rather than changing
isAppInLibrary globally.

Comment on lines +736 to +749
suspend fun requestFreeLicense(appId: Int): Boolean = withContext(Dispatchers.IO) {
val steamApps = instance?._steamApps ?: return@withContext false
try {
val callback = 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
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add a timeout to requestFreeLicense.

requestFreeLicense awaits the Steam callback without a timeout. If Steam never responds, the coroutine hangs indefinitely. getEncryptedAppTicket in this same file wraps its one-shot RPC in withTimeout(5_000) for the same class of risk. Apply the same guard here, since this call is triggered directly by a user tapping a featured CTA button.

🕐 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
suspend fun requestFreeLicense(appId: Int): Boolean = withContext(Dispatchers.IO) {
val steamApps = instance?._steamApps ?: return@withContext false
try {
val callback = 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
}
}
suspend fun requestFreeLicense(appId: Int): Boolean = withContext(Dispatchers.IO) {
val steamApps = instance?._steamApps ?: return@withContext false
try {
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
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/service/SteamService.kt` around lines 736 -
749, Wrap the awaited Steam callback in requestFreeLicense with the same 5,000
ms withTimeout guard used by getEncryptedAppTicket, preserving the existing
success logging, result validation, and exception fallback to false.

}
}

val openUrl = { context.startActivity(Intent(Intent.ACTION_VIEW, action.url.toUri())) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard startActivity against ActivityNotFoundException.

openUrl starts an ACTION_VIEW intent without a guard. If no activity resolves the URL, or if action.url is empty or malformed, the app crashes. This path is also the failure fallback at Line 101, so a failed in-app action can turn into a crash.

🛡️ 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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))
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/screen/library/FeaturedCtaButton.kt` at
line 65, Update the openUrl lambda in FeaturedCtaButton to catch
ActivityNotFoundException around context.startActivity, including malformed or
unresolvable URLs, and log the failure with Timber instead of allowing the app
to crash. Preserve its use as the fallback for failed in-app actions.

Comment on lines +26 to +35
if (PrefManager.usageAnalyticsEnabled) {
PostHog.capture(event = "featured_conversion", properties = properties)
} else {
properties["\$process_person_profile"] = false
PostHog.capture(
event = "featured_conversion",
distinctId = UUID.randomUUID().toString(),
properties = properties,
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

PostHog Android SDK 3.8.0 capture distinctId $process_person_profile personless events

💡 Result:

In the PostHog Android SDK (including version 3.8.0), you can capture events in "personless" mode—meaning no person profile is created or updated for the event—by setting the $process_person_profile property to false within the properties dictionary of your capture call [1][2]. When this property is set to false, PostHog processes the event without creating a person record in the database, instead using a deterministic "fake" person UUID derived from the distinctId [1]. This is useful for reducing ingestion resources [1]. Example implementation: PostHog.capture( event = "your_event_name", properties = mapOf( "$process_person_profile" to false)) Important considerations: 1. Compatibility: This property is explicitly supported for standard event capture. However, you cannot use this mode with identity-related events such as $identify, $create_alias, $merge_dangerously, or $groupidentify, as these events are specifically intended to manage person profiles and will be dropped if processed without person support [3]. 2. Later Identification: If a user who previously generated "personless" events later triggers an $identify event, PostHog can create an override to link their previous anonymous/personless history to their newly identified person profile [1]. 3. SDK Defaults: Note that the Android SDK's overall event capture behavior (whether events are considered "anonymous" or "identified") is also governed by your initialization configuration (personProfiles = PersonProfiles.IDENTIFIED_ONLY by default) [4][5]. Setting $process_person_profile: false is a specific instruction to bypass person processing entirely for that individual event [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'PostHog|build.gradle|ConversionTracker' . | sed 's#^\./##' | head -80

echo "== ConversionTracker context =="
file=$(fd 'ConversionTracker.kt' . | head -1 | xargs -r readlink -f || true)
if [ -n "$file" ]; then
  wc -l "$file"
  cat -n "$file" | sed -n '1,80p'
fi

echo "== gradle posthog refs =="
rg -n --glob '*.gradle*' --glob '*.kts' 'posthog|PostHog|use ' . | head -80

echo "== local PostHog capture usage =="
rg -n 'PostHog\.capture|capture\s*\(' . --glob '*.kt' --glob '*.kts' --glob '*.java' | head -100

Repository: utkarshdalal/GameNative

Length of output: 8828


🌐 Web query:

PostHog Android SDK 3.8.0 API capture distinctId signature Kotlin capture(event distinctId properties)

💡 Result:

In the PostHog Android SDK (including version 3.8.0), the capture method does not accept distinctId as a parameter in its primary signature [1][2]. The PostHog Android SDK manages the distinctId automatically based on the user's identification state (anonymous or identified) to ensure data consistency [3]. The standard Kotlin signature for the capture method is [1][4]: PostHog.capture( event: String, properties: Map<String, Any>? = null, userProperties: Map<String, Any>? = null, userPropertiesSetOnce: Map<String, Any>? = null, groupProperties: Map<String, Any>? = null) If you need to associate an event with a specific user, you should use the identify method to set the user's distinctId before calling capture [5][3]. Example usage: // Identify the user first PostHog.identify("your_distinct_id") // Capture the event PostHog.capture( event = "user_signed_up", properties = mapOf("is_free_trial" to true)) The properties parameter is a map used to include additional context with the event [4][6]. If you are attempting to override the distinctId on a per-event basis, this is generally not supported by the Android SDK's capture API, as identifying a user is intended to be a session-level or persistent state change [5][3].

Citations:


🌐 Web query:

PostHog Android 3.8.0 PostHog.capture distinctId Kotlin source

💡 Result:

In the PostHog Android SDK (including version 3.8.0), the PostHog.capture method does not accept a distinctId as an argument [1][2][3]. PostHog's mobile SDKs are designed to manage the distinctId automatically [4]. When using the Android SDK, the library generates and persists an anonymous distinctId for the user, which is then automatically attached to all events captured via PostHog.capture [2][3][4]. To associate events with a specific user (a known distinctId), you should use the PostHog.identify method instead of attempting to pass an ID during capture [5][4]. Kotlin usage example for capturing an event: import com.posthog.PostHog // Capture an event with the currently identified or anonymous distinctId PostHog.capture( event = "user_signed_up", properties = mapOf("is_free_trial" to true)) Kotlin usage example for identifying a user: import com.posthog.PostHog // Associate future events with this specific distinctId PostHog.identify( distinctId = "your_user_id_or_email", userProperties = mapOf("name" to "Max Hedgehog")) If you need to retrieve the current distinctId being used by the SDK, you can call PostHog.distinctId [1][6]. Version 3.8.0 of the PostHog Android SDK was released on October 3, 2024 [7][8].

Citations:


🌐 Web query:

site:github.com/PostHog/posthog-android 3.8.0 PostHog.capture distinctId override posthog-android

💡 Result:

In the PostHog Android SDK, there is no direct "override" method for the distinctId during event capture. Instead, PostHog follows a standard identity lifecycle [1]. To manage the distinctId, use the following patterns: 1. Identify Users: Use the identify method to associate a specific distinctId with a user [1]. Note that once a user is identified, subsequent calls to identify may be restricted or ignored if the user is already identified [1]. 2. Bootstrap Identity: If you need to set a known identity or distinctId on the very first launch (before the SDK initializes or fetches its own state), use the PostHogConfig.bootstrap property [2]. This allows you to seed the identity (and feature flag state) before any network requests occur [2]. 3. Reset: If you need to change the user or distinctId (e.g., after a logout), you must call the reset method first [1]. This clears the current identity and allows for a new session or user identification [1]. 4. Alias: If you want to merge two different distinctIds (e.g., an anonymous ID and a registered user ID), use the alias method [1]. For checking the current ID, you can use the PostHog.distinctId method to read the identifier currently in use by the SDK [3][4]. Always refer to the official PostHog Android documentation for the specific version you are using, as SDK behaviors regarding identity persistence and identification can evolve [5][6].

Citations:


🌐 Web query:

PostHog Android SDK capture(event distinctId Kotlin signature

💡 Result:

In the PostHog Android SDK, the capture method does not take a distinctId parameter directly [1][2][3]. The SDK automatically manages the distinctId based on the current user session [1][4]. To associate events with a specific user, you must use the identify method to set the distinctId before calling capture [1][4]. 1. Use PostHog.identify(distinctId = "your-user-id") to set the identity [4]. 2. Subsequent calls to PostHog.capture(event = "event-name") will automatically associate the event with that distinctId [1][4]. The standard Kotlin signature for capturing an event is: PostHog.capture( event: String, properties: Map<String, Any>? = null, options: CaptureOptions? = null) If you need to change the user or associate an event with a different identifier, you should call identify again with the new ID, provided the session allows it [4]. If you are working in a context where you must explicitly manage user context for a single event (such as in server-side scenarios), the Android/Kotlin client SDK design relies on the persistent identity set via identify [4].

Citations:


🌐 Web query:

PostHog Android CaptureOptions distinctId captureOptions posthog-android

💡 Result:

In the PostHog Android SDK, distinct IDs are managed via the PostHog API rather than through a direct CaptureOptions object [1][2]. Key points regarding distinct ID and capturing in the Android SDK: 1. Identifying Users: You do not provide a distinct ID within a CaptureOptions parameter when capturing an event. Instead, you use the PostHog.identify(distinctId) method [3]. Once identify is called, the SDK persists this ID and associates it with all subsequent events until you call PostHog.reset [3]. 2. Capturing Events: When using the capture method, you provide the event name and optional properties [4][2]. There is no requirement to manually pass a distinct ID for each capture call, as the SDK handles this automatically using the currently identified (or anonymous) ID [5][6]. 3. Retrieving the ID: You can check the currently active distinct ID at any time by calling PostHog.distinctId [1][7]. 4. CaptureOptions: While some PostHog SDKs (such as those for Kotlin Multiplatform) utilize a CaptureOptions object for attaching group context or custom timestamps to individual events [8], the core PostHog Android SDK primarily manages configuration and identification at the global level during initialization or via dedicated methods like identify and group [1][2][3]. For standard Android implementations, prioritize using PostHog.identify to associate events with a specific user [3]. If you are working in a complex environment like Kotlin Multiplatform, verify if your specific SDK version supports the CaptureOptions object for events [8].

Citations:


🌐 Web query:

github PostHog posthog-android 3.8.0 PostHog.kt capture distinctId

💡 Result:

In the PostHog Android SDK (including version 3.8.0), you associate a user with a specific identifier by using the PostHog.identify method [1][2][3]. To identify a user, call the method with their unique identifier (such as an email or database ID) [1][2]: import com.posthog.PostHog PostHog.identify( distinctId = "your_user_unique_id", userProperties = mapOf( "name" to "Max Hedgehog", "email" to "max@hedgehogmail.com")) Key points regarding distinctId in the Android SDK: 1. Automatic vs. Manual: If you do not call identify, the SDK automatically generates and uses an anonymous distinct ID [1][4]. Once you call identify with a specific ID, the SDK associates subsequent events with that identity [2][5]. 2. Retrieving the current ID: You can retrieve the current distinct ID (whether it is the generated anonymous ID or an ID set via identify) by calling PostHog.distinctId [1][6]. 3. Aliasing: If you need to link multiple distinct IDs to a single user (e.g., merging an anonymous web ID with an authenticated mobile ID), use the PostHog.alias method [1][7]. 4. Release Context: Version 3.8.0 of posthog-android was released on October 3, 2024 [8]. It primarily included minor features and dependency updates [8]. The usage of identify and distinctId remains consistent with the standard patterns for the PostHog Android SDK [1][6]. For full implementation details, refer to the official PostHog Android documentation [1][9].

Citations:


🌐 Web query:

PostHog Android SDK distinctId capture override private distinctId CaptureOptions distinctId Kotlin

💡 Result:

In the PostHog Android SDK, you cannot manually override the distinctId on a per-event basis through CaptureOptions [1]. PostHog is designed to maintain a consistent identity for a user session once they have been identified [2]. To manage or change the distinctId, use the following official methods: 1. Identify the user: Use PostHog.identify(distinctId) to associate events with a specific user [1][3]. Once identified, this distinctId will be used for all subsequent events captured by the SDK [4][3]. 2. Aliasing: If you need to associate multiple distinct IDs with a single user (e.g., merging an anonymous ID with a known user ID), use PostHog.alias("new_distinct_id") [3]. 3. Reset: To switch users or clear the current identity, call PostHog.reset(). This unlinks the person profile and forces the SDK to generate a new anonymous distinctId for future events [5]. 4. Bootstrapping: If you know the user's identity before the SDK initializes (e.g., from a backend session token), you can use PostHogBootstrapConfig before calling PostHog.setup() [6]. This ensures events captured early in the application lifecycle are associated with the correct ID [4][6]. If you need to retrieve the current ID, you can call PostHog.distinctId() [7][3]. Avoid attempting to inject a custom distinctId directly into capture calls, as the SDK enforces identity consistency internally [2].

Citations:


Use capture() without passing a custom distinctId.

PostHog Android 3.8.0 does not expose PostHog.capture(..., distinctId = ..., ...), so this call will not compile as written. Set the personless intent via properties["$process_person_profile"] = false, and rely on the SDK’s anonymous distinct ID instead of trying to supply one here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/utils/ConversionTracker.kt` around lines 26
- 35, Update the disabled-analytics branch in the conversion tracking flow to
call PostHog.capture without the custom distinctId argument. Keep setting
properties["$process_person_profile"] to false and rely on the SDK’s anonymous
distinct ID while preserving the existing "featured_conversion" event and
properties.

<string name="featured_action_wishlist_on">在 %1$s 加入願望清單</string>
<string name="featured_action_wishlisted">已加入願望清單</string>
<string name="featured_action_get_demo">取得試玩版</string>
<string name="featured_action_in_library">已在收藏庫中</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align "in library" wording with the file's established term.

featured_action_in_library uses "收藏庫" (collection). This file uses "遊戲庫" for the library concept everywhere else, for example in app_library and destination_library. Use the same term here to avoid implying a different concept (a saved/wishlist collection instead of an owned game).

💬 Proposed wording fix
-    <string name="featured_action_in_library">已在收藏庫中</string>
+    <string name="featured_action_in_library">已在遊戲庫中</string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<string name="featured_action_in_library">已在收藏庫中</string>
<string name="featured_action_in_library">已在遊戲庫中</string>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/values-zh-rTW/strings.xml` at line 2084, Update the
featured_action_in_library translation to use the file’s established “遊戲庫” term
instead of “收藏庫,” matching app_library and destination_library while preserving
the existing meaning.

Comment thread gradle/libs.versions.toml
play-integrity = "1.6.0" # https://mvnrepository.com/artifact/com.google.android.play/integrity
hiltNavigationCompose = "1.2.0" # https://mvnrepository.com/artifact/androidx.hilt/hilt-navigation-compose
javasteam = "1.8.0.1-24-SNAPSHOT" # https://github.com/joshuatam/JavaSteam/tree/gamenative-latest
javasteam = "1.8.0.1-26-SNAPSHOT" # https://github.com/joshuatam/JavaSteam/tree/gamenative-latest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether the JavaSteam snapshot version is referenced consistently and confirm the repository URL configuration.
rg -n "javasteam" gradle/libs.versions.toml app/build.gradle.kts

Repository: utkarshdalal/GameNative

Length of output: 1116


🌐 Web query:

io.github.joshuatam javasteam 1.8.0.1-26-SNAPSHOT maven central repository

💡 Result:

The requested artifact, io.github.joshuatam:javasteam:1.8.0.1-26-SNAPSHOT, is not a standard or documented release for the JavaSteam library [1][2][3]. The primary JavaSteam library is maintained under the Maven group ID in.dragonbra [1][4]. Joshua Tam is a contributor to the JavaSteam project [1][5][2], but there is no evidence of a distinct io.github.joshuatam repository or group for this library in Maven Central. Snapshot versions for the JavaSteam library are typically hosted in the Sonatype snapshot repository [1][6], not via a joshuatam group ID. If you are attempting to use a specific snapshot version, it is recommended to verify the correct dependency coordinates in your project build file: Group: in.dragonbra Artifact: javasteam You can reference the official repository and build instructions via the project's GitHub page at https://github.com/Longi94/JavaSteam [1].

Citations:


🌐 Web query:

repo1.maven.org/maven2/io/github/joshuatam/javasteam/1.8.0.1-26-SNAPSHOT

💡 Result:

The artifact identified as io.github.joshuatam:javasteam:1.8.0.1-26-SNAPSHOT is a development snapshot version of the JavaSteam library, a project that provides an interface to interact with Steam's servers [1]. This specific artifact is associated with a fork or personal repository of JavaSteam maintained by user joshuatam [1]. It is not part of the primary, official JavaSteam distribution, which is hosted under the Maven coordinates in.dragonbra:javasteam [2][3][4]. Development in the community frequently involves creating forks or branches to implement specific features, optimizations, or bug fixes (such as query parameter handling or Android optimizations) that are then tested in downstream projects like GameNative [5][6]. The version suffix -26-SNAPSHOT indicates that this is the 26th build of the 1.8.0.1-SNAPSHOT development cycle within that specific fork's CI/CD pipeline [6]. Users looking for the official, stable, or main development versions of JavaSteam should use the in.dragonbra group ID [2][3][4]. The official project releases are available via Maven Central, and users should verify their dependency coordinates against the official project repository to ensure they are using the intended version [2][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print the relevant catalog/version repository configuration without running repository code.
echo "gradle/libs.versions.toml version and dependency entries:"
sed -n '10,20p;74,82p' gradle/libs.versions.toml

echo
echo "app/build.gradle.kts dependency block context:"
sed -n '320,345p' app/build.gradle.kts

echo
echo "Repository declarations for JavaSteam/Snapshot repositories:"
rg -n "maven|snapshot|joshua|dragoon|depotdownloader" build.gradle.kts settings.gradle.kts gradle -g '*.kts' -g '*.toml' -g '*.gradle' || true

Repository: utkarshdalal/GameNative

Length of output: 3914


🌐 Web query:

"central.sonatype.com/repository/maven-snapshots" "io.github.joshuatam:javasteam"

💡 Result:

The Java library for interacting with Steam servers is named JavaSteam, maintained primarily under the repository Longi94/JavaSteam [1][2][3]. The correct Maven coordinate for this library is in.dragonbra:javasteam, not io.github.joshuatam:javasteam [1][4][3]. While joshuatam is listed as a contributor to the JavaSteam project [4][2][3], they are not the maintainer of the project's primary Maven artifact. If you are looking to use JavaSteam, you should use the following dependency configuration [1][3][5]: Maven: in.dragonbra javasteam x.y.z Gradle: implementation 'in.dragonbra:javasteam:x.y.z' Regarding the central.sonatype.com/repository/maven-snapshots/ URL, this is the official repository used by the Maven Central Publishing Portal for hosting snapshot versions of projects [6]. Although JavaSteam documentation historically mentions that snapshots may be available via the Sonatype repository [4][3][5], the project itself is hosted under the in.dragonbra group ID [1][4]. If you are attempting to consume a snapshot, you would need to configure your build tool to include this repository URL with specific content filters for the correct group ID [6].

Citations:


🌐 Web query:

github.com/joshuatam/JavaSteam maven deploy github pages io.github.joshuatam

💡 Result:

The GitHub repository joshuatam/JavaSteam is a project that provides a Java library for interacting with Valve's Steam servers [1][2]. It is not currently associated with a maven deployment to GitHub Pages under the io.github.joshuatam namespace. The JavaSteam project is primarily maintained and distributed through other community forks (such as Longi94/JavaSteam), which manage their releases via Maven Central under the in.dragonbra groupId [3][4][5]. Regarding the concepts mentioned in your query: 1. Maven Deployment to GitHub Packages: You can automate the deployment of Java artifacts to GitHub Packages using GitHub Actions. This involves configuring your pom.xml with the appropriate distribution management section and using a workflow with actions/setup-java to authenticate via GITHUB_TOKEN [6]. 2. Maven Site to GitHub Pages: Publishing a Maven site (generated documentation) to GitHub Pages is a distinct process from deploying Maven artifacts. This is typically achieved using plugins like maven-scm-publish-plugin to push site contents to the gh-pages branch of a repository [7]. 3. Namespace io.github.joshuatam: If you intend to publish to Maven Central under this namespace, you must verify ownership of the io.github.joshuatam coordinate through the Central Portal (Sonatype), which confirms your GitHub account identity [8]. There is no evidence that joshuatam/JavaSteam currently implements a Maven-to-GitHub-Pages workflow or uses the io.github.joshuatam group ID for distribution [1].

Citations:


🌐 Web query:

"utkarshdalal/JavaSteam" "maven" "io.github.joshuatam"

💡 Result:

The JavaSteam library, which provides an interface to interact with Valve's Steam network, is primarily maintained under the group ID in.dragonbra in the Maven Central repository [1][2]. There is no standard or widely used Maven artifact associated with io.github.joshuatam for JavaSteam [3]. While Joshua Tam is a contributor to the JavaSteam library (specifically credited for work on the depot downloader implementation) [4][3], the official releases are distributed via in.dragonbra:javasteam [2][5]. Several GitHub repositories, such as utkarshdalal/JavaSteam and Longi94/JavaSteam, appear to be references or mirrors for the same underlying project managed by in.dragonbra [1][6][7]. You should use the following dependency configuration for your project [8]: in.dragonbra javasteam 1.8.0 Always verify the latest version available on the Maven Central repository [2].

Citations:


🌐 Web query:

utkarshdalal/JavaSteam javasteam-1.8.0.1-26-SNAPSHOT

💡 Result:

The identifier javasteam-1.8.0.1-26-SNAPSHOT refers to a development snapshot version of the JavaSteam library, which is a Java port of SteamKit2 used to interface with Valve's Steam servers [1][2]. This specific version is used as a dependency in GameNative, an open-source project by Utkarsh Dalal that enables native PC gaming on Android [3][4]. GameNative uses a modified or extended version of the JavaSteam library to handle core Steam functionality, such as authentication, game library management, and content downloads [5][3]. Key points regarding this artifact include: - Relationship: While the primary JavaSteam library is maintained at github.com/Longi94/JavaSteam, the GameNative project frequently integrates custom snapshots and contributions (including those by Utkarsh Dalal) to optimize the library for Android performance and compatibility [2][6][3][7]. - Usage: The version string format (e.g., 1.8.0.1-26-SNAPSHOT) represents a specific build in the development cycle of the library as it is used within the GameNative ecosystem [8]. Developers working on or building GameNative locally may reference these snapshot JARs directly in their build configurations (e.g., in build.gradle.kts) [9][8]. - Context: These snapshots are often created to fix specific issues or implement features required for GameNative, such as handling query parameters for API requests or optimizing download processes [5][8].

Citations:


🌐 Web query:

site:joshuatam.github.io/JavaSteam "1.8.0.1-26-SNAPSHOT"

💡 Result:

No indexed results were found for:

site:joshuatam.github.io/JavaSteam "1.8.0.1-26-SNAPSHOT"


🌐 Web query:

site:raw.githubusercontent.com/joshuatam/JavaSteam "1.8.0.1-26-SNAPSHOT"

💡 Result:

No indexed results were found for:

site:raw.githubusercontent.com/joshuatam/JavaSteam "1.8.0.1-26-SNAPSHOT"


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Git history for JavaSteam version references (concise)"
git -C .. log --oneline --all -- gradle/libs.versions.toml app/build.gradle.kts gradle/libs.versions.toml | head -20 2>/dev/null || true

echo
echo "Dependency comments and localBuild references"
rg -n "gamenative-latest|localBuild|javasteam-1\.8\.0\.1-2(4|5|6)-SNAPSHOT|javasteam-depotdownloader-1\.8\.0\.1-2(4|5|6)-SNAPSHOT" gradle app

Repository: utkarshdalal/GameNative

Length of output: 898


🌐 Web query:

joshuatam.jfrog.io javasteam 1.8.0.1-26-SNAPSHOT

💡 Result:

The term javasteam refers to a Java library that provides an interface to interact with Valve's Steam servers [1][2]. It is a port of the.NET library SteamKit2 and targets Java 17 [1][2]. The specific artifact version 1.8.0.1-26-SNAPSHOT is a development snapshot build of this library. These snapshot versions are frequently referenced in projects like GameNative [3], a project that includes contributions from joshuatam (Joshua Tam) [4]. Development snapshots, such as the ...-21-SNAPSHOT or ...-26-SNAPSHOT series, are often managed in custom or local Gradle dependency configurations to incorporate specific fixes—such as improvements to query parameter handling—before they are formally released in the main in.dragonbra:javasteam Maven artifact [3][1][2]. Users encountering this specific snapshot typically find it within the dependency declarations of projects that use JavaSteam as a library, rather than as a publicly hosted artifact on standard repositories like Maven Central [3]. You can find the main source code and releases for the library at the official repository [5][2].

Citations:


🌐 Web query:

io.github.joshuatam javasteam 1.8.0.1-26-SNAPSHOT "maven-snapshots"

💡 Result:

The artifact 1.8.0.1-26-SNAPSHOT refers to a specific development snapshot build of JavaSteam, a Java port of SteamKit2 [1]. While user "joshuatam" is a contributor to the official JavaSteam project (maintained under the Longi94/JavaSteam repository) [2][3][4], the library itself is published under the Maven Group ID in.dragonbra [2][5][4]. There is no official artifact group ID io.github.joshuatam for this library; references to such versions often appear in downstream projects (such as GameNative) that track specific SNAPSHOT builds of the upstream library for testing or integration purposes [6]. Key points regarding this artifact: * Origin: The core project is located at github.com/Longi94/JavaSteam [2][1]. * Group ID: Official releases and snapshots are published under in.dragonbra:javasteam [2][5][1]. * Snapshot Usage: Snapshot versions like 1.8.0.1-26-SNAPSHOT are typically hosted in the Sonatype maven-snapshots repository [5][4][1]. They are used by developers to access the latest commits (such as fixes for query parameter handling or dependency updates) before they are promoted to a formal release [6]. * Context: Projects like GameNative often track these specific build numbers (e.g., -21-SNAPSHOT, -26-SNAPSHOT) in their build configuration files (libs.versions.toml or build.gradle.kts) to maintain compatibility with the latest developments in the JavaSteam codebase [6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf 'Attempt Maven repository path probe:\n'
for base in \
  "https://repo1.maven.org/maven2" \
  "https://central.sonatype.com/repository/maven-snapshots"
do
  url="$base/io/github/joshuatam/javasteam/1.8.0.1-26-SNAPSHOT/javasteam-1.8.0.1-26-SNAPSHOT.pom"
  printf '%s\n' "$url"
  if command -v curl >/dev/null 2>&1; then
    curl -fsSIL "$url" | sed -n '1,5p' || true
  else
    printf 'curl not available\n'
  fi
done

Repository: utkarshdalal/GameNative

Length of output: 873


Resolve the JavaSteam snapshot before merging.

io.github.joshuatam:javasteam:1.8.0.1-26-SNAPSHOT is not available from the configured Maven Central / Maven-snapshots repositories. This external SNAPSHOT is resolved with isChanging = true; use a published artifact, update the gamenative-latest checkout to actually contain and publish this build, or add a configured repository that hosts it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gradle/libs.versions.toml` at line 15, Update the javasteam dependency
version in the version catalog to a resolvable published artifact, or ensure the
referenced gamenative-latest build is actually published and available through
the configured repositories; do not leave the dependency pointing to the
unavailable 1.8.0.1-26-SNAPSHOT artifact.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/build.gradle.kts">

<violation number="1" location="app/build.gradle.kts:334">
P3: This edit bumps only the inactive `localBuild` branch (localBuild=false), so it has no effect on the actual dependency resolution — the active `else` branch uses `libs.javasteam` from 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.</violation>
</file>

<file name="app/src/main/java/app/gamenative/service/SteamService.kt">

<violation number="1" location="app/src/main/java/app/gamenative/service/SteamService.kt:731">
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.) [FEEDBACK_USED]</violation>

<violation number="2" location="app/src/main/java/app/gamenative/service/SteamService.kt:736">
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.</violation>

<violation number="3" location="app/src/main/java/app/gamenative/service/SteamService.kt:742">
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 `CancellationException` before handling ordinary failures.</violation>
</file>

<file name="app/src/main/java/app/gamenative/data/RecommendationRepository.kt">

<violation number="1" location="app/src/main/java/app/gamenative/data/RecommendationRepository.kt:22">
P3: This PR ships the demo scaffold inside production code: `MOCK_HERO_RESPONSE` is a hardcoded `false`, making the `parseHero(MOCK_HERO_JSON)` branch unreachable at runtime, so the large embedded payload (a real 'Whisk' campaign with its actual Steam appid, asset URLs, and a separate demo appid) is effectively a test fixture compiled into the app. It's only used by `MockHeroResponseTest`. This adds noise and embeds a real third-party/Steam campaign in the shipped binary just to power the demo. Consider moving the mock payload and a `demo`/debug toggle out of the production `RecommendationRepository` (e.g. into the test source set or behind a debug-only/flag-gated layer), so the demo doesn't ship with the app.</violation>
</file>

<file name="app/src/main/res/values-zh-rTW/strings.xml">

<violation number="1" location="app/src/main/res/values-zh-rTW/strings.xml:2084">
P3: The new featured_action_in_library string uses "收藏庫" while this file consistently uses "遊戲庫" elsewhere (e.g. app_library, destination_library) for the library concept. Using a different term here could confuse users into thinking this is a separate saved/wishlist collection rather than the owned-games library.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}

suspend fun isAppInLibrary(appId: Int): Boolean =
instance?.licenseDao?.getAllLicenses()?.any { appId in it.appIds } == true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/service/SteamService.kt, line 733:

<comment>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.) </comment>

<file context>
@@ -727,6 +727,27 @@ class SteamService : Service(), IChallengeUrlChanged {
+        /** Whether any owned license grants [appId]. Follows the license list, so a freshly
+         *  granted free license flips this once Steam pushes the updated list. */
+        suspend fun isAppInLibrary(appId: Int): Boolean =
+            instance?.licenseDao?.getAllLicenses()?.any { appId in it.appIds } == true
+
+        /** Requests a free license (demos, F2P) for [appId] over the CM connection. */
</file context>

"apps=${callback.grantedApps}, packages=${callback.grantedPackages}",
)
callback.result == EResult.OK && appId in callback.grantedApps
} catch (e: Exception) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 CancellationException before handling ordinary failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/service/SteamService.kt, line 745:

<comment>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 `CancellationException` before handling ordinary failures.</comment>

<file context>
@@ -727,6 +727,27 @@ class SteamService : Service(), IChallengeUrlChanged {
+                        "apps=${callback.grantedApps}, packages=${callback.grantedPackages}",
+                )
+                callback.result == EResult.OK && appId in callback.grantedApps
+            } catch (e: Exception) {
+                Timber.e(e, "requestFreeLicense($appId) failed")
+                false
</file context>
Suggested change
} catch (e: Exception) {
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {

suspend fun requestFreeLicense(appId: Int): Boolean = withContext(Dispatchers.IO) {
val steamApps = instance?._steamApps ?: return@withContext false
try {
val callback = steamApps.requestFreeLicense(appId).toFuture().await()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/service/SteamService.kt, line 739:

<comment>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.</comment>

<file context>
@@ -727,6 +727,27 @@ class SteamService : Service(), IChallengeUrlChanged {
+        suspend fun requestFreeLicense(appId: Int): Boolean = withContext(Dispatchers.IO) {
+            val steamApps = instance?._steamApps ?: return@withContext false
+            try {
+                val callback = steamApps.requestFreeLicense(appId).toFuture().await()
+                Timber.i(
+                    "requestFreeLicense($appId) -> ${callback.result}, " +
</file context>
Suggested change
val callback = steamApps.requestFreeLicense(appId).toFuture().await()
val callback = withTimeout(15_000) {
steamApps.requestFreeLicense(appId).toFuture().await()
}

Comment thread app/build.gradle.kts
if (localBuild) {
implementation(files("../../JavaSteam/build/libs/javasteam-1.8.0.1-22-SNAPSHOT.jar"))
implementation(files("../../JavaSteam/javasteam-depotdownloader/build/libs/javasteam-depotdownloader-1.8.0.1-22-SNAPSHOT.jar"))
implementation(files("../../JavaSteam/build/libs/javasteam-1.8.0.1-25-SNAPSHOT.jar"))

Copy link
Copy Markdown
Contributor

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 localBuild branch (localBuild=false), so it has no effect on the actual dependency resolution — the active else branch uses libs.javasteam from 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
Check if this issue is valid — if so, understand the root cause and fix it. At app/build.gradle.kts, line 334:

<comment>This edit bumps only the inactive `localBuild` branch (localBuild=false), so it has no effect on the actual dependency resolution — the active `else` branch uses `libs.javasteam` from 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.</comment>

<file context>
@@ -331,8 +331,8 @@ dependencies {
     if (localBuild) {
-        implementation(files("../../JavaSteam/build/libs/javasteam-1.8.0.1-22-SNAPSHOT.jar"))
-        implementation(files("../../JavaSteam/javasteam-depotdownloader/build/libs/javasteam-depotdownloader-1.8.0.1-22-SNAPSHOT.jar"))
+        implementation(files("../../JavaSteam/build/libs/javasteam-1.8.0.1-25-SNAPSHOT.jar"))
+        implementation(files("../../JavaSteam/javasteam-depotdownloader/build/libs/javasteam-depotdownloader-1.8.0.1-25-SNAPSHOT.jar"))
         implementation(libs.bundles.javasteam.dev)
</file context>


private const val MOCK_HERO_RESPONSE = false

internal val MOCK_HERO_JSON = """

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This PR ships the demo scaffold inside production code: MOCK_HERO_RESPONSE is a hardcoded false, making the parseHero(MOCK_HERO_JSON) branch unreachable at runtime, so the large embedded payload (a real 'Whisk' campaign with its actual Steam appid, asset URLs, and a separate demo appid) is effectively a test fixture compiled into the app. It's only used by MockHeroResponseTest. This adds noise and embeds a real third-party/Steam campaign in the shipped binary just to power the demo. Consider moving the mock payload and a demo/debug toggle out of the production RecommendationRepository (e.g. into the test source set or behind a debug-only/flag-gated layer), so the demo doesn't ship with the app.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/data/RecommendationRepository.kt, line 25:

<comment>This PR ships the demo scaffold inside production code: `MOCK_HERO_RESPONSE` is a hardcoded `false`, making the `parseHero(MOCK_HERO_JSON)` branch unreachable at runtime, so the large embedded payload (a real 'Whisk' campaign with its actual Steam appid, asset URLs, and a separate demo appid) is effectively a test fixture compiled into the app. It's only used by `MockHeroResponseTest`. This adds noise and embeds a real third-party/Steam campaign in the shipped binary just to power the demo. Consider moving the mock payload and a `demo`/debug toggle out of the production `RecommendationRepository` (e.g. into the test source set or behind a debug-only/flag-gated layer), so the demo doesn't ship with the app.</comment>

<file context>
@@ -17,6 +17,40 @@ object RecommendationRepository {
+
+    // Verbatim /api/games/hero payload for a sponsored campaign with in-app CTAs; goes through
+    // parseHero like a real response, so it exercises deserialization, not just the UI.
+    internal val MOCK_HERO_JSON = """
+        {
+          "recommendation": null,
</file context>

<string name="featured_action_wishlist_on">在 %1$s 加入願望清單</string>
<string name="featured_action_wishlisted">已加入願望清單</string>
<string name="featured_action_get_demo">取得試玩版</string>
<string name="featured_action_in_library">已在收藏庫中</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new featured_action_in_library string uses "收藏庫" while this file consistently uses "遊戲庫" elsewhere (e.g. app_library, destination_library) for the library concept. Using a different term here could confuse users into thinking this is a separate saved/wishlist collection rather than the owned-games library.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/res/values-zh-rTW/strings.xml, line 2084:

<comment>The new featured_action_in_library string uses "收藏庫" while this file consistently uses "遊戲庫" elsewhere (e.g. app_library, destination_library) for the library concept. Using a different term here could confuse users into thinking this is a separate saved/wishlist collection rather than the owned-games library.</comment>

<file context>
@@ -2079,6 +2079,10 @@
     <string name="featured_action_wishlist_on">在 %1$s 加入願望清單</string>
+    <string name="featured_action_wishlisted">已加入願望清單</string>
+    <string name="featured_action_get_demo">取得試玩版</string>
+    <string name="featured_action_in_library">已在收藏庫中</string>
+    <string name="featured_action_failed">無法在應用程式內完成,正在開啟商店頁面</string>
     <string name="featured_action_preorder">預購</string>
</file context>
Suggested change
<string name="featured_action_in_library">已在收藏庫中</string>
<string name="featured_action_in_library">已在遊戲庫中</string>

@utkarshdalal
utkarshdalal merged commit 78e9343 into master Aug 2, 2026
3 checks passed
@utkarshdalal
utkarshdalal deleted the javasteam-wishlist-demo branch August 2, 2026 03:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant