mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
Merge pull request #1088 from hydralauncher/feat/update-hydra-api-subscription
feat: HydraApi updates
This commit is contained in:
commit
202533c85c
@ -355,7 +355,7 @@
|
|||||||
"user_achievements": "{{displayName}}'s Achievements",
|
"user_achievements": "{{displayName}}'s Achievements",
|
||||||
"your_achievements": "Your Achievements",
|
"your_achievements": "Your Achievements",
|
||||||
"unlocked_at": "Unlocked at:",
|
"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"
|
"new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games"
|
||||||
},
|
},
|
||||||
"tour": {
|
"tour": {
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
gamesPlaytime,
|
gamesPlaytime,
|
||||||
} from "@main/services";
|
} from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
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 signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const databaseOperations = dataSource
|
const databaseOperations = dataSource
|
||||||
@ -19,6 +19,10 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
await transactionalEntityManager
|
await transactionalEntityManager
|
||||||
.getRepository(UserAuth)
|
.getRepository(UserAuth)
|
||||||
.delete({ id: 1 });
|
.delete({ id: 1 });
|
||||||
|
|
||||||
|
await transactionalEntityManager
|
||||||
|
.getRepository(UserSubscription)
|
||||||
|
.delete({ id: 1 });
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
/* Removes all games being played */
|
/* Removes all games being played */
|
||||||
|
@ -1,43 +1,11 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { logger } from "@main/services";
|
import type { UserDetails } from "@types";
|
||||||
import type { ProfileVisibility, UserDetails } from "@types";
|
|
||||||
import { userAuthRepository } from "@main/repository";
|
|
||||||
import { UserNotLoggedInError } from "@shared";
|
|
||||||
import { getUserData } from "@main/services/user/get-user-data";
|
import { getUserData } from "@main/services/user/get-user-data";
|
||||||
|
|
||||||
const getMe = async (
|
const getMe = async (
|
||||||
_event: Electron.IpcMainInvokeEvent
|
_event: Electron.IpcMainInvokeEvent
|
||||||
): Promise<UserDetails | null> => {
|
): Promise<UserDetails | null> => {
|
||||||
return getUserData().catch(async (err) => {
|
return getUserData();
|
||||||
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;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getMe", getMe);
|
registerEvent("getMe", getMe);
|
||||||
|
@ -12,7 +12,6 @@ import { UserPreferences } from "./entity";
|
|||||||
import { RealDebridClient } from "./services/real-debrid";
|
import { RealDebridClient } from "./services/real-debrid";
|
||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
import { getUserData } from "./services/user/get-user-data";
|
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
import("./events");
|
import("./events");
|
||||||
@ -23,8 +22,7 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||||||
|
|
||||||
Ludusavi.addManifestToLudusaviConfig();
|
Ludusavi.addManifestToLudusaviConfig();
|
||||||
|
|
||||||
await HydraApi.setupApi().then(async () => {
|
HydraApi.setupApi().then(() => {
|
||||||
await getUserData().catch(() => {});
|
|
||||||
uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ export const mergeAchievements = async (
|
|||||||
id: game.remoteId,
|
id: game.remoteId,
|
||||||
achievements: mergedLocalAchievements,
|
achievements: mergedLocalAchievements,
|
||||||
},
|
},
|
||||||
{ needsCloud: true }
|
{ needsSubscription: true }
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return saveAchievementsOnLocal(
|
return saveAchievementsOnLocal(
|
||||||
|
@ -11,10 +11,18 @@ import { logger } from "./logger";
|
|||||||
import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
|
import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
|
||||||
import { omit } from "lodash-es";
|
import { omit } from "lodash-es";
|
||||||
import { appVersion } from "@main/constants";
|
import { appVersion } from "@main/constants";
|
||||||
|
import { getUserData } from "./user/get-user-data";
|
||||||
|
|
||||||
interface HydraApiOptions {
|
interface HydraApiOptions {
|
||||||
needsAuth?: boolean;
|
needsAuth?: boolean;
|
||||||
needsCloud?: boolean;
|
needsSubscription?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HydraApiUserAuth {
|
||||||
|
authToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
expirationTimestamp: number;
|
||||||
|
subscription: { expiresAt: Date | null } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HydraApi {
|
export class HydraApi {
|
||||||
@ -25,27 +33,22 @@ export class HydraApi {
|
|||||||
|
|
||||||
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
||||||
|
|
||||||
private static userAuth = {
|
private static userAuth: HydraApiUserAuth = {
|
||||||
authToken: "",
|
authToken: "",
|
||||||
refreshToken: "",
|
refreshToken: "",
|
||||||
expirationTimestamp: 0,
|
expirationTimestamp: 0,
|
||||||
|
subscription: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static isLoggedIn() {
|
private static isLoggedIn() {
|
||||||
return this.userAuth.authToken !== "";
|
return this.userAuth.authToken !== "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async hasCloudSubscription() {
|
private static hasCloudSubscription() {
|
||||||
return userSubscriptionRepository
|
|
||||||
.findOne({ where: { id: 1 } })
|
|
||||||
.then((userSubscription) => {
|
|
||||||
if (!userSubscription) return false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!userSubscription.expiresAt ||
|
this.userAuth.subscription?.expiresAt &&
|
||||||
userSubscription!.expiresAt > new Date()
|
this.userAuth.subscription.expiresAt > new Date()
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleExternalAuth(uri: string) {
|
static async handleExternalAuth(uri: string) {
|
||||||
@ -67,6 +70,7 @@ export class HydraApi {
|
|||||||
authToken: accessToken,
|
authToken: accessToken,
|
||||||
refreshToken: refreshToken,
|
refreshToken: refreshToken,
|
||||||
expirationTimestamp: tokenExpirationTimestamp,
|
expirationTimestamp: tokenExpirationTimestamp,
|
||||||
|
subscription: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
@ -84,6 +88,16 @@ export class HydraApi {
|
|||||||
["id"]
|
["id"]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await getUserData().then((userDetails) => {
|
||||||
|
if (userDetails?.subscription) {
|
||||||
|
this.userAuth.subscription = {
|
||||||
|
expiresAt: userDetails.subscription.expiresAt
|
||||||
|
? new Date(userDetails.subscription.expiresAt)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (WindowManager.mainWindow) {
|
if (WindowManager.mainWindow) {
|
||||||
WindowManager.mainWindow.webContents.send("on-signin");
|
WindowManager.mainWindow.webContents.send("on-signin");
|
||||||
await clearGamesRemoteIds();
|
await clearGamesRemoteIds();
|
||||||
@ -96,6 +110,7 @@ export class HydraApi {
|
|||||||
authToken: "",
|
authToken: "",
|
||||||
refreshToken: "",
|
refreshToken: "",
|
||||||
expirationTimestamp: 0,
|
expirationTimestamp: 0,
|
||||||
|
subscription: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,14 +176,20 @@ export class HydraApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await getUserData();
|
||||||
|
|
||||||
const userAuth = await userAuthRepository.findOne({
|
const userAuth = await userAuthRepository.findOne({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
|
relations: { subscription: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userAuth = {
|
this.userAuth = {
|
||||||
authToken: userAuth?.accessToken ?? "",
|
authToken: userAuth?.accessToken ?? "",
|
||||||
refreshToken: userAuth?.refreshToken ?? "",
|
refreshToken: userAuth?.refreshToken ?? "",
|
||||||
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
||||||
|
subscription: userAuth?.subscription
|
||||||
|
? { expiresAt: userAuth.subscription?.expiresAt }
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,9 +257,11 @@ export class HydraApi {
|
|||||||
authToken: "",
|
authToken: "",
|
||||||
expirationTimestamp: 0,
|
expirationTimestamp: 0,
|
||||||
refreshToken: "",
|
refreshToken: "",
|
||||||
|
subscription: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
userAuthRepository.delete({ id: 1 });
|
userAuthRepository.delete({ id: 1 });
|
||||||
|
userSubscriptionRepository.delete({ id: 1 });
|
||||||
|
|
||||||
this.sendSignOutEvent();
|
this.sendSignOutEvent();
|
||||||
}
|
}
|
||||||
@ -248,14 +271,14 @@ export class HydraApi {
|
|||||||
|
|
||||||
private static async validateOptions(options?: HydraApiOptions) {
|
private static async validateOptions(options?: HydraApiOptions) {
|
||||||
const needsAuth = options?.needsAuth == undefined || options.needsAuth;
|
const needsAuth = options?.needsAuth == undefined || options.needsAuth;
|
||||||
const needsCloud = options?.needsCloud === true;
|
const needsSubscription = options?.needsSubscription === true;
|
||||||
|
|
||||||
if (needsAuth) {
|
if (needsAuth) {
|
||||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsCloud) {
|
if (needsSubscription) {
|
||||||
if (!(await this.hasCloudSubscription())) {
|
if (!(await this.hasCloudSubscription())) {
|
||||||
throw new SubscriptionRequiredError();
|
throw new SubscriptionRequiredError();
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import type { UserDetails } from "@types";
|
import type { ProfileVisibility, UserDetails } from "@types";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import {
|
import {
|
||||||
userAuthRepository,
|
userAuthRepository,
|
||||||
userSubscriptionRepository,
|
userSubscriptionRepository,
|
||||||
} from "@main/repository";
|
} from "@main/repository";
|
||||||
import * as Sentry from "@sentry/electron/main";
|
import * as Sentry from "@sentry/electron/main";
|
||||||
|
import { UserNotLoggedInError } from "@shared";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
export const getUserData = () => {
|
export const getUserData = () => {
|
||||||
return HydraApi.get<UserDetails>(`/profile/me`).then(async (me) => {
|
return HydraApi.get<UserDetails>(`/profile/me`)
|
||||||
|
.then(async (me) => {
|
||||||
userAuthRepository.upsert(
|
userAuthRepository.upsert(
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -39,5 +42,38 @@ export const getUserData = () => {
|
|||||||
Sentry.setUser({ id: me.id, username: me.username });
|
Sentry.setUser({ id: me.id, username: me.username });
|
||||||
|
|
||||||
return me;
|
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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -42,6 +42,7 @@ interface AchievementSummaryProps {
|
|||||||
function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||||
const { t } = useTranslation("achievement");
|
const { t } = useTranslation("achievement");
|
||||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||||
|
const { handleClickOpenCheckout } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const getProfileImage = (
|
const getProfileImage = (
|
||||||
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
||||||
@ -90,7 +91,12 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
|||||||
>
|
>
|
||||||
<LockIcon size={24} />
|
<LockIcon size={24} />
|
||||||
<h3>
|
<h3>
|
||||||
<Link to={""}>{t("subscription_needed")}</Link>
|
<button
|
||||||
|
className={styles.subscriptionRequiredButton}
|
||||||
|
onClick={handleClickOpenCheckout}
|
||||||
|
>
|
||||||
|
{t("subscription_needed")}
|
||||||
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -207,3 +207,16 @@ export const profileAvatarSmall = style({
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const subscriptionRequiredButton = style({
|
||||||
|
textDecoration: "none",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
gap: `${SPACING_UNIT / 2}px`,
|
||||||
|
color: vars.color.body,
|
||||||
|
cursor: "pointer",
|
||||||
|
":hover": {
|
||||||
|
textDecoration: "underline",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -238,7 +238,7 @@ export interface Subscription {
|
|||||||
id: string;
|
id: string;
|
||||||
status: SubscriptionStatus;
|
status: SubscriptionStatus;
|
||||||
plan: { id: string; name: string };
|
plan: { id: string; name: string };
|
||||||
expiresAt: Date | null;
|
expiresAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserDetails {
|
export interface UserDetails {
|
||||||
|
Loading…
Reference in New Issue
Block a user