feat: add PRO badge and credits modal for HuggingChat (#2048)
Browse files* feat: add PRO badge and credits modal for HuggingChat
- Add isPro field to User type and persist from OAuth login
- Fetch fresh isPro status from HuggingFace public API on page load
- Display "Get PRO" button for non-PRO users in sidebar
- Display "PRO" badge for PRO users in sidebar
- Update SubscribeModal to show "Purchase Credits" for PRO users
- Add IconPro component for gradient sparkle icon
* feat: improve SubscribeModal UI for PRO users
- Use IconDazzled with yellow-orange-red gradient for "Out of Credits"
- Use IconPro with pink-green-yellow gradient for "Upgrade Required"
- Update credits message to mention HF services and Inference Providers
- src/lib/components/NavMenu.svelte +25 -2
- src/lib/components/SubscribeModal.svelte +50 -43
- src/lib/components/icons/IconPro.svelte +37 -0
- src/lib/server/api/routes/groups/user.ts +1 -0
- src/lib/stores/isPro.ts +4 -0
- src/lib/types/User.ts +1 -0
- src/routes/+layout.svelte +13 -0
- src/routes/login/callback/updateUser.ts +5 -1
src/lib/components/NavMenu.svelte
CHANGED
|
@@ -29,6 +29,8 @@
|
|
| 29 |
import { useAPIClient, handleResponse } from "$lib/APIClient";
|
| 30 |
import { requireAuthUser } from "$lib/utils/auth";
|
| 31 |
import { enabledServersCount } from "$lib/stores/mcpServers";
|
|
|
|
|
|
|
| 32 |
import MCPServerManager from "./mcp/MCPServerManager.svelte";
|
| 33 |
|
| 34 |
const publicConfig = usePublicConfig();
|
|
@@ -171,16 +173,37 @@
|
|
| 171 |
>
|
| 172 |
{#if user?.username || user?.email}
|
| 173 |
<div
|
| 174 |
-
class="group flex items-center gap-1.5 rounded-lg pl-2.5 pr-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
| 175 |
>
|
| 176 |
<span
|
| 177 |
class="flex h-9 flex-none shrink items-center gap-1.5 truncate pr-2 text-gray-500 dark:text-gray-400"
|
| 178 |
>{user?.username || user?.email}</span
|
| 179 |
>
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
<img
|
| 182 |
src="https://huggingface.co/api/users/{user.username}/avatar?redirect=true"
|
| 183 |
-
class="
|
|
|
|
|
|
|
| 184 |
alt=""
|
| 185 |
/>
|
| 186 |
</div>
|
|
|
|
| 29 |
import { useAPIClient, handleResponse } from "$lib/APIClient";
|
| 30 |
import { requireAuthUser } from "$lib/utils/auth";
|
| 31 |
import { enabledServersCount } from "$lib/stores/mcpServers";
|
| 32 |
+
import { isPro } from "$lib/stores/isPro";
|
| 33 |
+
import IconPro from "$lib/components/icons/IconPro.svelte";
|
| 34 |
import MCPServerManager from "./mcp/MCPServerManager.svelte";
|
| 35 |
|
| 36 |
const publicConfig = usePublicConfig();
|
|
|
|
| 173 |
>
|
| 174 |
{#if user?.username || user?.email}
|
| 175 |
<div
|
| 176 |
+
class="group flex items-center gap-1.5 rounded-lg pl-2.5 pr-2 hover:bg-gray-100 first:hover:bg-transparent dark:hover:bg-gray-700 first:dark:hover:bg-transparent"
|
| 177 |
>
|
| 178 |
<span
|
| 179 |
class="flex h-9 flex-none shrink items-center gap-1.5 truncate pr-2 text-gray-500 dark:text-gray-400"
|
| 180 |
>{user?.username || user?.email}</span
|
| 181 |
>
|
| 182 |
|
| 183 |
+
{#if publicConfig.isHuggingChat && $isPro === false}
|
| 184 |
+
<a
|
| 185 |
+
href="https://huggingface.co/subscribe/pro?from=HuggingChat"
|
| 186 |
+
target="_blank"
|
| 187 |
+
rel="noopener noreferrer"
|
| 188 |
+
class="ml-auto flex h-[20px] items-center gap-1 rounded-md bg-gradient-to-r from-pink-500/10 via-green-500/10 to-green-500/5 px-1.5 py-0.5 text-xs text-gray-500 hover:from-pink-500/20 hover:via-green-500/20 dark:from-pink-500/20 dark:via-green-500/20 dark:to-green-500/10 dark:text-gray-400 dark:hover:from-pink-500/30 dark:hover:via-green-500/30"
|
| 189 |
+
>
|
| 190 |
+
<IconPro />
|
| 191 |
+
Get PRO
|
| 192 |
+
</a>
|
| 193 |
+
{:else if publicConfig.isHuggingChat && $isPro === true}
|
| 194 |
+
<span
|
| 195 |
+
class="ml-auto flex h-[20px] items-center gap-1 rounded-md bg-gradient-to-r from-pink-500/10 via-green-500/10 to-green-500/5 px-1.5 py-0.5 text-xs text-gray-500 hover:from-pink-500/20 hover:via-green-500/20 dark:from-pink-500/20 dark:via-green-500/20 dark:to-green-500/10 dark:text-gray-400 dark:hover:from-pink-500/30 dark:hover:via-green-500/30"
|
| 196 |
+
>
|
| 197 |
+
<IconPro />
|
| 198 |
+
PRO
|
| 199 |
+
</span>
|
| 200 |
+
{/if}
|
| 201 |
+
|
| 202 |
<img
|
| 203 |
src="https://huggingface.co/api/users/{user.username}/avatar?redirect=true"
|
| 204 |
+
class="{!(publicConfig.isHuggingChat && $isPro !== null)
|
| 205 |
+
? 'ml-auto'
|
| 206 |
+
: ''} size-4 rounded-full border bg-gray-500 dark:border-white/40"
|
| 207 |
alt=""
|
| 208 |
/>
|
| 209 |
</div>
|
src/lib/components/SubscribeModal.svelte
CHANGED
|
@@ -1,5 +1,8 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import Modal from "$lib/components/Modal.svelte";
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
interface Props {
|
| 5 |
close: () => void;
|
|
@@ -17,58 +20,62 @@
|
|
| 17 |
>
|
| 18 |
<div class="flex flex-col items-center justify-center gap-2.5 px-8 text-center">
|
| 19 |
<div
|
| 20 |
-
class="flex size-14 items-center justify-center rounded-full
|
|
|
|
|
|
|
| 21 |
>
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
xmlns="http://www.w3.org/2000/svg"
|
| 28 |
-
>
|
| 29 |
-
<path
|
| 30 |
-
d="M6.48 1.26001C6.48 2.81001 7.15 3.84001 7.98 4.50001C8.84 5.18001 9.88 5.50001 10.56 5.57001V6.43001C9.6233 6.5513 8.73602 6.92071 7.99 7.50001C7.50131 7.88332 7.10989 8.37647 6.84753 8.93943C6.58516 9.50238 6.45925 10.1193 6.48 10.74H5.52C5.52 9.19001 4.85 8.16001 4.02 7.50001C3.27114 6.91907 2.3802 6.54958 1.44 6.43001V5.57001C2.37671 5.44872 3.26398 5.07931 4.01 4.50001C4.4987 4.1167 4.89011 3.62355 5.15248 3.06059C5.41484 2.49764 5.54076 1.88075 5.52 1.26001H6.48Z"
|
| 31 |
-
fill="url(#paint0_linear_141_2)"
|
| 32 |
-
/>
|
| 33 |
-
<defs>
|
| 34 |
-
<linearGradient
|
| 35 |
-
id="paint0_linear_141_2"
|
| 36 |
-
x1="3.37"
|
| 37 |
-
y1="3.43001"
|
| 38 |
-
x2="8.14"
|
| 39 |
-
y2="8.90001"
|
| 40 |
-
gradientUnits="userSpaceOnUse"
|
| 41 |
-
>
|
| 42 |
-
<stop stop-color="#FF0789" />
|
| 43 |
-
<stop offset="0.63" stop-color="#21DE75" />
|
| 44 |
-
<stop offset="1" stop-color="#FF8D00" />
|
| 45 |
-
</linearGradient>
|
| 46 |
-
</defs>
|
| 47 |
-
</svg>
|
| 48 |
</div>
|
| 49 |
-
<h2 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
|
| 50 |
</div>
|
| 51 |
</div>
|
| 52 |
|
| 53 |
<div class="text-gray-700 dark:text-gray-200">
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
</div>
|
| 62 |
|
| 63 |
<div class="flex flex-col gap-2.5">
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
<button
|
| 73 |
class="w-full rounded-xl bg-gray-200 px-5 py-2.5 text-base font-medium text-gray-700 hover:bg-gray-300/80 dark:bg-white/5 dark:text-gray-200 dark:hover:bg-white/10"
|
| 74 |
onclick={close}
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import Modal from "$lib/components/Modal.svelte";
|
| 3 |
+
import { isPro } from "$lib/stores/isPro";
|
| 4 |
+
import IconPro from "$lib/components/icons/IconPro.svelte";
|
| 5 |
+
import IconDazzled from "$lib/components/icons/IconDazzled.svelte";
|
| 6 |
|
| 7 |
interface Props {
|
| 8 |
close: () => void;
|
|
|
|
| 20 |
>
|
| 21 |
<div class="flex flex-col items-center justify-center gap-2.5 px-8 text-center">
|
| 22 |
<div
|
| 23 |
+
class="flex size-14 items-center justify-center rounded-full text-3xl {$isPro
|
| 24 |
+
? 'bg-gradient-to-br from-yellow-500/15 via-orange-500/15 to-red-500/15'
|
| 25 |
+
: 'bg-gradient-to-br from-pink-500/15 from-15% via-green-500/15 to-yellow-500/15'}"
|
| 26 |
>
|
| 27 |
+
{#if $isPro}
|
| 28 |
+
<IconDazzled />
|
| 29 |
+
{:else}
|
| 30 |
+
<IconPro classNames="!mr-0" />
|
| 31 |
+
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
</div>
|
| 33 |
+
<h2 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
| 34 |
+
{$isPro ? "Out of Credits" : "Upgrade Required"}
|
| 35 |
+
</h2>
|
| 36 |
</div>
|
| 37 |
</div>
|
| 38 |
|
| 39 |
<div class="text-gray-700 dark:text-gray-200">
|
| 40 |
+
{#if $isPro}
|
| 41 |
+
<p class="text-[15px] leading-relaxed">
|
| 42 |
+
You've used all your available credits. Purchase additional credits to continue using
|
| 43 |
+
HuggingChat.
|
| 44 |
+
</p>
|
| 45 |
+
<p class="mt-3 text-[15px] italic leading-relaxed opacity-75">
|
| 46 |
+
Your credits can be used in other HF services and external apps via Inference Providers.
|
| 47 |
+
</p>
|
| 48 |
+
{:else}
|
| 49 |
+
<p class="text-[15px] leading-relaxed">
|
| 50 |
+
You've reached your message limit. Upgrade to Hugging Face PRO to continue using
|
| 51 |
+
HuggingChat.
|
| 52 |
+
</p>
|
| 53 |
+
<p class="mt-3 text-[15px] italic leading-relaxed opacity-75">
|
| 54 |
+
It's also possible to use your PRO credits in your favorite AI tools.
|
| 55 |
+
</p>
|
| 56 |
+
{/if}
|
| 57 |
</div>
|
| 58 |
|
| 59 |
<div class="flex flex-col gap-2.5">
|
| 60 |
+
{#if $isPro}
|
| 61 |
+
<a
|
| 62 |
+
href="https://huggingface.co/settings/billing?add-credits=true"
|
| 63 |
+
target="_blank"
|
| 64 |
+
rel="noopener noreferrer"
|
| 65 |
+
class="w-full rounded-xl bg-black px-5 py-2.5 text-center text-base font-medium text-white hover:bg-gray-800 dark:bg-white dark:text-black dark:hover:bg-gray-200"
|
| 66 |
+
>
|
| 67 |
+
Purchase Credits
|
| 68 |
+
</a>
|
| 69 |
+
{:else}
|
| 70 |
+
<a
|
| 71 |
+
href="https://huggingface.co/subscribe/pro?from=HuggingChat"
|
| 72 |
+
target="_blank"
|
| 73 |
+
rel="noopener noreferrer"
|
| 74 |
+
class="w-full rounded-xl bg-black px-5 py-2.5 text-center text-base font-medium text-white hover:bg-gray-800 dark:bg-white dark:text-black dark:hover:bg-gray-200"
|
| 75 |
+
>
|
| 76 |
+
Upgrade to Pro
|
| 77 |
+
</a>
|
| 78 |
+
{/if}
|
| 79 |
<button
|
| 80 |
class="w-full rounded-xl bg-gray-200 px-5 py-2.5 text-base font-medium text-gray-700 hover:bg-gray-300/80 dark:bg-white/5 dark:text-gray-200 dark:hover:bg-white/10"
|
| 81 |
onclick={close}
|
src/lib/components/icons/IconPro.svelte
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
interface Props {
|
| 3 |
+
classNames?: string;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
let { classNames = "" }: Props = $props();
|
| 7 |
+
|
| 8 |
+
// I've no idea wht a fixed id doesnt work...
|
| 9 |
+
const gradientId = `gradient-${Math.random().toString(36).slice(2, 9)}`;
|
| 10 |
+
</script>
|
| 11 |
+
|
| 12 |
+
<svg
|
| 13 |
+
class="text-gray-500 {classNames}"
|
| 14 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 15 |
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
| 16 |
+
role="img"
|
| 17 |
+
width="1em"
|
| 18 |
+
height="1em"
|
| 19 |
+
viewBox="0 0 12 12"
|
| 20 |
+
><defs
|
| 21 |
+
><linearGradient
|
| 22 |
+
id={gradientId}
|
| 23 |
+
x1="3.371"
|
| 24 |
+
y1="3.43"
|
| 25 |
+
x2="8.141"
|
| 26 |
+
y2="8.9"
|
| 27 |
+
gradientUnits="userSpaceOnUse"
|
| 28 |
+
><stop stop-color="#FF0789" /><stop offset=".63" stop-color="#21DE75" /><stop
|
| 29 |
+
offset="1"
|
| 30 |
+
stop-color="#FF8D00"
|
| 31 |
+
/></linearGradient
|
| 32 |
+
></defs
|
| 33 |
+
><path
|
| 34 |
+
d="M6.481 1.26c0 1.55.67 2.58 1.5 3.24.86.68 1.9 1 2.58 1.07v.86a5.3 5.3 0 0 0-2.57 1.07 3.95 3.95 0 0 0-1.51 3.24h-.96c0-1.55-.67-2.58-1.5-3.24a5.3 5.3 0 0 0-2.58-1.07v-.86a5.3 5.3 0 0 0 2.57-1.07 3.95 3.95 0 0 0 1.51-3.24h.96Z"
|
| 35 |
+
fill="url(#{gradientId})"
|
| 36 |
+
/></svg
|
| 37 |
+
>
|
src/lib/server/api/routes/groups/user.ts
CHANGED
|
@@ -34,6 +34,7 @@ export const userGroup = new Elysia()
|
|
| 34 |
email: locals.user.email,
|
| 35 |
isAdmin: locals.user.isAdmin ?? false,
|
| 36 |
isEarlyAccess: locals.user.isEarlyAccess ?? false,
|
|
|
|
| 37 |
}
|
| 38 |
: null;
|
| 39 |
})
|
|
|
|
| 34 |
email: locals.user.email,
|
| 35 |
isAdmin: locals.user.isAdmin ?? false,
|
| 36 |
isEarlyAccess: locals.user.isEarlyAccess ?? false,
|
| 37 |
+
isPro: locals.user.isPro ?? false,
|
| 38 |
}
|
| 39 |
: null;
|
| 40 |
})
|
src/lib/stores/isPro.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { writable } from "svelte/store";
|
| 2 |
+
|
| 3 |
+
// null = unknown/loading, true = PRO, false = not PRO
|
| 4 |
+
export const isPro = writable<boolean | null>(null);
|
src/lib/types/User.ts
CHANGED
|
@@ -11,4 +11,5 @@ export interface User extends Timestamps {
|
|
| 11 |
hfUserId: string;
|
| 12 |
isAdmin?: boolean;
|
| 13 |
isEarlyAccess?: boolean;
|
|
|
|
| 14 |
}
|
|
|
|
| 11 |
hfUserId: string;
|
| 12 |
isAdmin?: boolean;
|
| 13 |
isEarlyAccess?: boolean;
|
| 14 |
+
isPro?: boolean;
|
| 15 |
}
|
src/routes/+layout.svelte
CHANGED
|
@@ -19,6 +19,7 @@
|
|
| 19 |
import { setContext } from "svelte";
|
| 20 |
import { handleResponse, useAPIClient } from "$lib/APIClient";
|
| 21 |
import { isAborted } from "$lib/stores/isAborted";
|
|
|
|
| 22 |
import IconShare from "$lib/components/icons/IconShare.svelte";
|
| 23 |
import { shareModal } from "$lib/stores/shareModal";
|
| 24 |
import BackgroundGenerationPoller from "$lib/components/BackgroundGenerationPoller.svelte";
|
|
@@ -123,6 +124,18 @@
|
|
| 123 |
const settings = createSettingsStore(data.settings);
|
| 124 |
|
| 125 |
onMount(async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
if (page.url.searchParams.has("model")) {
|
| 127 |
await settings
|
| 128 |
.instantSet({
|
|
|
|
| 19 |
import { setContext } from "svelte";
|
| 20 |
import { handleResponse, useAPIClient } from "$lib/APIClient";
|
| 21 |
import { isAborted } from "$lib/stores/isAborted";
|
| 22 |
+
import { isPro } from "$lib/stores/isPro";
|
| 23 |
import IconShare from "$lib/components/icons/IconShare.svelte";
|
| 24 |
import { shareModal } from "$lib/stores/shareModal";
|
| 25 |
import BackgroundGenerationPoller from "$lib/components/BackgroundGenerationPoller.svelte";
|
|
|
|
| 124 |
const settings = createSettingsStore(data.settings);
|
| 125 |
|
| 126 |
onMount(async () => {
|
| 127 |
+
if (publicConfig.isHuggingChat && data.user?.username) {
|
| 128 |
+
fetch(`https://huggingface.co/api/users/${data.user.username}/overview`)
|
| 129 |
+
.then((res) => res.json())
|
| 130 |
+
.then((userData) => {
|
| 131 |
+
isPro.set(userData.isPro ?? false);
|
| 132 |
+
})
|
| 133 |
+
.catch(() => {
|
| 134 |
+
// Fallback to database value on error
|
| 135 |
+
isPro.set(data.user?.isPro ?? false);
|
| 136 |
+
});
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
if (page.url.searchParams.has("model")) {
|
| 140 |
await settings
|
| 141 |
.instantSet({
|
src/routes/login/callback/updateUser.ts
CHANGED
|
@@ -39,6 +39,7 @@ export async function updateUser(params: {
|
|
| 39 |
picture: avatarUrl,
|
| 40 |
sub: hfUserId,
|
| 41 |
orgs,
|
|
|
|
| 42 |
} = z
|
| 43 |
.object({
|
| 44 |
preferred_username: z.string().optional(),
|
|
@@ -46,6 +47,7 @@ export async function updateUser(params: {
|
|
| 46 |
picture: z.string().optional(),
|
| 47 |
sub: z.string(),
|
| 48 |
email: z.string().email().optional(),
|
|
|
|
| 49 |
orgs: z
|
| 50 |
.array(
|
| 51 |
z.object({
|
|
@@ -72,6 +74,7 @@ export async function updateUser(params: {
|
|
| 72 |
picture?: string;
|
| 73 |
sub: string;
|
| 74 |
name: string;
|
|
|
|
| 75 |
orgs?: Array<{
|
| 76 |
sub: string;
|
| 77 |
name: string;
|
|
@@ -134,7 +137,7 @@ export async function updateUser(params: {
|
|
| 134 |
// update existing user if any
|
| 135 |
await collections.users.updateOne(
|
| 136 |
{ _id: existingUser._id },
|
| 137 |
-
{ $set: { username, name, avatarUrl, isAdmin, isEarlyAccess } }
|
| 138 |
);
|
| 139 |
|
| 140 |
// remove previous session if it exists and add new one
|
|
@@ -164,6 +167,7 @@ export async function updateUser(params: {
|
|
| 164 |
hfUserId,
|
| 165 |
isAdmin,
|
| 166 |
isEarlyAccess,
|
|
|
|
| 167 |
});
|
| 168 |
|
| 169 |
userId = insertedId;
|
|
|
|
| 39 |
picture: avatarUrl,
|
| 40 |
sub: hfUserId,
|
| 41 |
orgs,
|
| 42 |
+
isPro,
|
| 43 |
} = z
|
| 44 |
.object({
|
| 45 |
preferred_username: z.string().optional(),
|
|
|
|
| 47 |
picture: z.string().optional(),
|
| 48 |
sub: z.string(),
|
| 49 |
email: z.string().email().optional(),
|
| 50 |
+
isPro: z.boolean().optional(),
|
| 51 |
orgs: z
|
| 52 |
.array(
|
| 53 |
z.object({
|
|
|
|
| 74 |
picture?: string;
|
| 75 |
sub: string;
|
| 76 |
name: string;
|
| 77 |
+
isPro?: boolean;
|
| 78 |
orgs?: Array<{
|
| 79 |
sub: string;
|
| 80 |
name: string;
|
|
|
|
| 137 |
// update existing user if any
|
| 138 |
await collections.users.updateOne(
|
| 139 |
{ _id: existingUser._id },
|
| 140 |
+
{ $set: { username, name, avatarUrl, isAdmin, isEarlyAccess, isPro } }
|
| 141 |
);
|
| 142 |
|
| 143 |
// remove previous session if it exists and add new one
|
|
|
|
| 167 |
hfUserId,
|
| 168 |
isAdmin,
|
| 169 |
isEarlyAccess,
|
| 170 |
+
isPro,
|
| 171 |
});
|
| 172 |
|
| 173 |
userId = insertedId;
|