Files
gui/apps/gui.hanzo.ai/features/user/userSubscriptionStatus.ts
T
zeekay 61a4bc23c9 fix(site): remove external admin allowlist
Empties ADMIN_EMAILS and DEVELOPER_GITHUB_USERNAMES, so the impersonate,
recent-purchases and whitelist routes answer 403 to every caller. Points
security reports and funding at Hanzo.
2026-08-21 12:46:33 -07:00

161 lines
4.8 KiB
TypeScript

/**
* This file contains the whitelist of users who are allowed to access the subscription features.
*/
import type { UserContextType } from '~/features/auth/types'
import {
ProductName,
type ProductNameType,
SubscriptionStatus,
type UserSubscriptionStatus,
} from '~/shared/types/subscription'
/**
* GitHub usernames allowed to test the purchase flow while holding Pro. These
* users bypass the active-subscription check, unlock the DEV_TEST_99 coupon
* (99% off), and may repurchase the same domain. Empty, so no account
* qualifies.
*/
export const DEVELOPER_GITHUB_USERNAMES: readonly string[] = []
/**
* Check if a subscription has specific product access
*/
const hasProductAccess = (
subscriptions: UserContextType['subscriptions'],
productName: ProductNameType
): boolean => {
return (
subscriptions?.some(
(sub) =>
(sub.status === SubscriptionStatus.Active ||
sub.status === SubscriptionStatus.Trialing) &&
sub.subscription_items?.some((item) => item.price?.product?.name === productName)
) ?? false
)
}
/**
* Calculate support tier from subscription data
* V2 support tiers: direct = 1, sponsor = 2
* V1 support tier: based on quantity
*/
const calculateSupportTier = (
subscriptions: UserContextType['subscriptions']
): number => {
if (!subscriptions) return 0
// Check V2 sponsor support first (highest tier)
const hasSponsor = subscriptions.some(
(sub) =>
sub.subscription_items?.some(
(item) => item.price?.product?.name === ProductName.GuiSupportSponsor
) &&
(sub.status === SubscriptionStatus.Active ||
sub.status === SubscriptionStatus.Trialing)
)
if (hasSponsor) return 2
// Check V2 direct support
const hasDirect = subscriptions.some(
(sub) =>
sub.subscription_items?.some(
(item) => item.price?.product?.name === ProductName.GuiSupportDirect
) &&
(sub.status === SubscriptionStatus.Active ||
sub.status === SubscriptionStatus.Trialing)
)
if (hasDirect) return 1
// V1 support (legacy)
const supportItem = subscriptions.find((sub) =>
sub.subscription_items?.some(
(item) =>
item.price?.product?.name === ProductName.GuiSupport &&
(sub.status === SubscriptionStatus.Active ||
sub.status === SubscriptionStatus.Trialing)
)
)
return supportItem?.quantity ?? 0
}
const calculateTeamSeats = (subscriptions: UserContextType['subscriptions']): number => {
if (!subscriptions) return 0
return (
subscriptions.find((sub) =>
sub.subscription_items?.some(
(item) =>
item.price?.product?.name === ProductName.GuiProTeamSeats &&
(sub.status === SubscriptionStatus.Active ||
sub.status === SubscriptionStatus.Trialing)
)
)?.quantity ?? 0
)
}
/**
* Check if a user is a developer who can test the purchase flow
*/
const isDeveloperUser = (userData?: UserContextType): boolean => {
if (!userData?.githubUsername) return false
return DEVELOPER_GITHUB_USERNAMES.includes(userData.githubUsername.toLowerCase())
}
/**
* Get the subscription status of a user.
*/
export const userSubscriptionStatus = (
userData?: UserContextType
): UserSubscriptionStatus => {
if (!userData)
return {
pro: false,
proV1: false,
proV2: false,
chat: false,
supportTier: 0,
teamSeats: 0,
couponCodes: { previouslySubscribed: 'RENEWAL30' },
isDeveloper: false,
}
const hasV1Pro =
hasProductAccess(userData.subscriptions, ProductName.GuiPro) ||
hasProductAccess(userData.subscriptions, ProductName.GuiProTeamSeats)
const hasV2Pro =
hasProductAccess(userData.subscriptions, ProductName.GuiProV2) ||
hasProductAccess(userData.subscriptions, ProductName.GuiProV2Upgrade) ||
// V2 support tiers also imply Pro access (support is add-on to Pro license)
hasProductAccess(userData.subscriptions, ProductName.GuiSupportDirect) ||
hasProductAccess(userData.subscriptions, ProductName.GuiSupportSponsor)
const isPro = hasV1Pro || hasV2Pro
const isChat =
// V1 chat
hasProductAccess(userData.subscriptions, ProductName.GuiChat) ||
// V2 Pro includes basic chat support
hasProductAccess(userData.subscriptions, ProductName.GuiProV2) ||
hasProductAccess(userData.subscriptions, ProductName.GuiProV2Upgrade) ||
// V2 support tiers include chat
hasProductAccess(userData.subscriptions, ProductName.GuiSupportDirect) ||
hasProductAccess(userData.subscriptions, ProductName.GuiSupportSponsor)
const supportTier = calculateSupportTier(userData.subscriptions) || 0
return {
pro: isPro,
proV1: hasV1Pro,
proV2: hasV2Pro,
chat: isChat,
supportTier,
teamSeats: calculateTeamSeats(userData.subscriptions),
couponCodes: {
previouslySubscribed: 'RENEWAL30',
},
isDeveloper: isDeveloperUser(userData),
}
}