diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 3d58c86d..c3e3fcf0 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -355,7 +355,7 @@ "user_achievements": "{{displayName}}'s Achievements", "your_achievements": "Your Achievements", "unlocked_at": "Unlocked at:", - "subscription_needed": "A Hydra Cloud subscription is needed to see this content", + "subscription_needed": "A Hydra Cloud subscription is required to see this content", "new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games" }, "tour": { diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts index 724626b7..ed399f6d 100644 --- a/src/main/events/auth/sign-out.ts +++ b/src/main/events/auth/sign-out.ts @@ -7,7 +7,7 @@ import { gamesPlaytime, } from "@main/services"; import { dataSource } from "@main/data-source"; -import { DownloadQueue, Game, UserAuth } from "@main/entity"; +import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity"; const signOut = async (_event: Electron.IpcMainInvokeEvent) => { const databaseOperations = dataSource @@ -19,6 +19,10 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => { await transactionalEntityManager .getRepository(UserAuth) .delete({ id: 1 }); + + await transactionalEntityManager + .getRepository(UserSubscription) + .delete({ id: 1 }); }) .then(() => { /* Removes all games being played */ diff --git a/src/main/events/profile/get-me.ts b/src/main/events/profile/get-me.ts index 8ab78bf9..4f34ca32 100644 --- a/src/main/events/profile/get-me.ts +++ b/src/main/events/profile/get-me.ts @@ -1,43 +1,11 @@ import { registerEvent } from "../register-event"; -import { logger } from "@main/services"; -import type { ProfileVisibility, UserDetails } from "@types"; -import { userAuthRepository } from "@main/repository"; -import { UserNotLoggedInError } from "@shared"; +import type { UserDetails } from "@types"; import { getUserData } from "@main/services/user/get-user-data"; const getMe = async ( _event: Electron.IpcMainInvokeEvent ): Promise => { - return getUserData().catch(async (err) => { - if (err instanceof UserNotLoggedInError) { - return null; - } - logger.error("Failed to get logged user", err); - const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } }); - - if (loggedUser) { - return { - ...loggedUser, - id: loggedUser.userId, - username: "", - bio: "", - profileVisibility: "PUBLIC" as ProfileVisibility, - subscription: loggedUser.subscription - ? { - id: loggedUser.subscription.subscriptionId, - status: loggedUser.subscription.status, - plan: { - id: loggedUser.subscription.planId, - name: loggedUser.subscription.planName, - }, - expiresAt: loggedUser.subscription.expiresAt, - } - : null, - }; - } - - return null; - }); + return getUserData(); }; registerEvent("getMe", getMe); diff --git a/src/main/main.ts b/src/main/main.ts index a680c824..69bc62e0 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -12,7 +12,6 @@ import { UserPreferences } from "./entity"; import { RealDebridClient } from "./services/real-debrid"; import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; -import { getUserData } from "./services/user/get-user-data"; const loadState = async (userPreferences: UserPreferences | null) => { import("./events"); @@ -23,8 +22,7 @@ const loadState = async (userPreferences: UserPreferences | null) => { Ludusavi.addManifestToLudusaviConfig(); - await HydraApi.setupApi().then(async () => { - await getUserData().catch(() => {}); + HydraApi.setupApi().then(() => { uploadGamesBatch(); }); diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index b1ae0273..41bf575a 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -125,7 +125,7 @@ export const mergeAchievements = async ( id: game.remoteId, achievements: mergedLocalAchievements, }, - { needsCloud: true } + { needsSubscription: true } ) .then((response) => { return saveAchievementsOnLocal( diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index ffe2fcb7..41d76408 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -11,10 +11,18 @@ import { logger } from "./logger"; import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared"; import { omit } from "lodash-es"; import { appVersion } from "@main/constants"; +import { getUserData } from "./user/get-user-data"; interface HydraApiOptions { needsAuth?: boolean; - needsCloud?: boolean; + needsSubscription?: boolean; +} + +interface HydraApiUserAuth { + authToken: string; + refreshToken: string; + expirationTimestamp: number; + subscription: { expiresAt: Date | null } | null; } export class HydraApi { @@ -25,27 +33,22 @@ export class HydraApi { private static secondsToMilliseconds = (seconds: number) => seconds * 1000; - private static userAuth = { + private static userAuth: HydraApiUserAuth = { authToken: "", refreshToken: "", expirationTimestamp: 0, + subscription: null, }; private static isLoggedIn() { return this.userAuth.authToken !== ""; } - private static async hasCloudSubscription() { - return userSubscriptionRepository - .findOne({ where: { id: 1 } }) - .then((userSubscription) => { - if (!userSubscription) return false; - - return ( - !userSubscription.expiresAt || - userSubscription!.expiresAt > new Date() - ); - }); + private static hasCloudSubscription() { + return ( + this.userAuth.subscription?.expiresAt && + this.userAuth.subscription.expiresAt > new Date() + ); } static async handleExternalAuth(uri: string) { @@ -67,6 +70,7 @@ export class HydraApi { authToken: accessToken, refreshToken: refreshToken, expirationTimestamp: tokenExpirationTimestamp, + subscription: null, }; logger.log( @@ -84,6 +88,16 @@ export class HydraApi { ["id"] ); + await getUserData().then((userDetails) => { + if (userDetails?.subscription) { + this.userAuth.subscription = { + expiresAt: userDetails.subscription.expiresAt + ? new Date(userDetails.subscription.expiresAt) + : null, + }; + } + }); + if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-signin"); await clearGamesRemoteIds(); @@ -96,6 +110,7 @@ export class HydraApi { authToken: "", refreshToken: "", expirationTimestamp: 0, + subscription: null, }; } @@ -161,14 +176,20 @@ export class HydraApi { ); } + await getUserData(); + const userAuth = await userAuthRepository.findOne({ where: { id: 1 }, + relations: { subscription: true }, }); this.userAuth = { authToken: userAuth?.accessToken ?? "", refreshToken: userAuth?.refreshToken ?? "", expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0, + subscription: userAuth?.subscription + ? { expiresAt: userAuth.subscription?.expiresAt } + : null, }; } @@ -236,9 +257,11 @@ export class HydraApi { authToken: "", expirationTimestamp: 0, refreshToken: "", + subscription: null, }; userAuthRepository.delete({ id: 1 }); + userSubscriptionRepository.delete({ id: 1 }); this.sendSignOutEvent(); } @@ -248,14 +271,14 @@ export class HydraApi { private static async validateOptions(options?: HydraApiOptions) { const needsAuth = options?.needsAuth == undefined || options.needsAuth; - const needsCloud = options?.needsCloud === true; + const needsSubscription = options?.needsSubscription === true; if (needsAuth) { if (!this.isLoggedIn()) throw new UserNotLoggedInError(); await this.revalidateAccessTokenIfExpired(); } - if (needsCloud) { + if (needsSubscription) { if (!(await this.hasCloudSubscription())) { throw new SubscriptionRequiredError(); } diff --git a/src/main/services/user/get-user-data.ts b/src/main/services/user/get-user-data.ts index 5035b296..c3ca6eb8 100644 --- a/src/main/services/user/get-user-data.ts +++ b/src/main/services/user/get-user-data.ts @@ -1,43 +1,79 @@ -import type { UserDetails } from "@types"; +import type { ProfileVisibility, UserDetails } from "@types"; import { HydraApi } from "../hydra-api"; import { userAuthRepository, userSubscriptionRepository, } from "@main/repository"; import * as Sentry from "@sentry/electron/main"; +import { UserNotLoggedInError } from "@shared"; +import { logger } from "../logger"; export const getUserData = () => { - return HydraApi.get(`/profile/me`).then(async (me) => { - userAuthRepository.upsert( - { - id: 1, - displayName: me.displayName, - profileImageUrl: me.profileImageUrl, - backgroundImageUrl: me.backgroundImageUrl, - userId: me.id, - }, - ["id"] - ); - - if (me.subscription) { - await userSubscriptionRepository.upsert( + return HydraApi.get(`/profile/me`) + .then(async (me) => { + userAuthRepository.upsert( { id: 1, - subscriptionId: me.subscription?.id || "", - status: me.subscription?.status || "", - planId: me.subscription?.plan.id || "", - planName: me.subscription?.plan.name || "", - expiresAt: me.subscription?.expiresAt || null, - user: { id: 1 }, + displayName: me.displayName, + profileImageUrl: me.profileImageUrl, + backgroundImageUrl: me.backgroundImageUrl, + userId: me.id, }, ["id"] ); - } else { - await userSubscriptionRepository.delete({ id: 1 }); - } - Sentry.setUser({ id: me.id, username: me.username }); + if (me.subscription) { + await userSubscriptionRepository.upsert( + { + id: 1, + subscriptionId: me.subscription?.id || "", + status: me.subscription?.status || "", + planId: me.subscription?.plan.id || "", + planName: me.subscription?.plan.name || "", + expiresAt: me.subscription?.expiresAt || null, + user: { id: 1 }, + }, + ["id"] + ); + } else { + await userSubscriptionRepository.delete({ id: 1 }); + } - return me; - }); + Sentry.setUser({ id: me.id, username: me.username }); + + return me; + }) + .catch(async (err) => { + if (err instanceof UserNotLoggedInError) { + return null; + } + logger.error("Failed to get logged user", err); + const loggedUser = await userAuthRepository.findOne({ + where: { id: 1 }, + relations: { subscription: true }, + }); + + if (loggedUser) { + return { + ...loggedUser, + id: loggedUser.userId, + username: "", + bio: "", + profileVisibility: "PUBLIC" as ProfileVisibility, + subscription: loggedUser.subscription + ? { + id: loggedUser.subscription.subscriptionId, + status: loggedUser.subscription.status, + plan: { + id: loggedUser.subscription.planId, + name: loggedUser.subscription.planName, + }, + expiresAt: loggedUser.subscription.expiresAt, + } + : null, + } as UserDetails; + } + + return null; + }); }; diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index aa93bc30..72dfe83f 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -42,6 +42,7 @@ interface AchievementSummaryProps { function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { const { t } = useTranslation("achievement"); const { userDetails, hasActiveSubscription } = useUserDetails(); + const { handleClickOpenCheckout } = useContext(gameDetailsContext); const getProfileImage = ( user: Pick @@ -90,7 +91,12 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { >

- {t("subscription_needed")} +