diff --git a/docs/pr-assets/premium-membership-required.png b/docs/pr-assets/premium-membership-required.png new file mode 100644 index 000000000..85d30ff4d Binary files /dev/null and b/docs/pr-assets/premium-membership-required.png differ diff --git a/docs/pr-assets/premium-membership-unknown.png b/docs/pr-assets/premium-membership-unknown.png new file mode 100644 index 000000000..fd2f82864 Binary files /dev/null and b/docs/pr-assets/premium-membership-unknown.png differ diff --git a/locales/en.json b/locales/en.json index 8fd2acffe..145156d19 100644 --- a/locales/en.json +++ b/locales/en.json @@ -336,6 +336,8 @@ "gameDetails": { "eyebrow": "Game details", "details": "About this game", + "premiumRequired": "Premium membership required", + "freeTierUnavailable": "Not playable on the Free tier. Requires {{tier}} or higher on GeForce NOW.", "notOwned": "Not owned", "playOn": "Play on {{store}}", "storeOption": "{{store}}, {{ownership}}" diff --git a/opennow-stable/src/renderer/src/components/GameDetailModal.tsx b/opennow-stable/src/renderer/src/components/GameDetailModal.tsx index 0ee37e37f..629de5b8a 100644 --- a/opennow-stable/src/renderer/src/components/GameDetailModal.tsx +++ b/opennow-stable/src/renderer/src/components/GameDetailModal.tsx @@ -1,4 +1,4 @@ -import { Check, X } from "lucide-react"; +import { Check, Crown, X } from "lucide-react"; import { useId, useLayoutEffect, @@ -9,6 +9,7 @@ import { import type { GameInfo } from "@shared/gfn"; import { getControllerHeroBackgroundCandidates, getPlayerSummary } from "../lib/controllerCatalogUi"; import { getActiveStoreOption, getStoreOptions } from "../lib/gameCardStores"; +import { getRequiredPaidMembershipTier } from "../lib/premiumMembership"; import { useTranslation } from "../i18n"; import { getStoreDisplayName, getStoreIconComponent } from "./GameCard"; import { ModalSurface } from "./ui/ModalSurface"; @@ -126,6 +127,7 @@ export function GameDetailModal({ const storeOptions = getStoreOptions(game, selectedVariantId); const activeStoreOption = getActiveStoreOption(storeOptions); + const requiredPaidMembershipTier = getRequiredPaidMembershipTier(game); const playerSummary = getPlayerSummary(game); const description = game.description || game.longDescription @@ -181,6 +183,18 @@ export function GameDetailModal({
+ {t("gameDetails.freeTierUnavailable", { tier: requiredPaidMembershipTier })} +
+ )} +{game.longDescription || game.description || game.featureLabels?.join(" / ") || t("library.loadingGameDetails")}
diff --git a/opennow-stable/src/renderer/src/lib/premiumMembership.test.ts b/opennow-stable/src/renderer/src/lib/premiumMembership.test.ts new file mode 100644 index 000000000..62d384bec --- /dev/null +++ b/opennow-stable/src/renderer/src/lib/premiumMembership.test.ts @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { getRequiredPaidMembershipTier } from "./premiumMembership"; + +test("detects known paid GFN membership requirements", () => { + assert.equal(getRequiredPaidMembershipTier({ membershipTierLabel: "Performance" }), "Performance"); + assert.equal(getRequiredPaidMembershipTier({ membershipTierLabel: "Ultimate membership" }), "Ultimate membership"); + assert.equal(getRequiredPaidMembershipTier({ membershipTierLabel: " premium " }), "premium"); +}); + +test("does not claim a restriction for free, missing, or unknown tier metadata", () => { + assert.equal(getRequiredPaidMembershipTier({ membershipTierLabel: "Free" }), null); + assert.equal(getRequiredPaidMembershipTier({ membershipTierLabel: "" }), null); + assert.equal(getRequiredPaidMembershipTier({}), null); + assert.equal(getRequiredPaidMembershipTier({ membershipTierLabel: "Special Access" }), null); +}); diff --git a/opennow-stable/src/renderer/src/lib/premiumMembership.ts b/opennow-stable/src/renderer/src/lib/premiumMembership.ts new file mode 100644 index 000000000..bf6778934 --- /dev/null +++ b/opennow-stable/src/renderer/src/lib/premiumMembership.ts @@ -0,0 +1,24 @@ +import type { GameInfo } from "@shared/gfn"; + +const PAID_MEMBERSHIP_TIERS = new Set([ + "FOUNDERS", + "PERFORMANCE", + "PREMIUM", + "PRIORITY", + "ULTIMATE", +]); + +export function getRequiredPaidMembershipTier( + game: Pick