Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | 'use client'
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { billingKeys } from '@/lib/queryKeys'
import type { TierName, DurationOption } from '@/lib/tier-limits'
export interface TierLimitsResponse {
maxPracticeStudents: number | null // null = unlimited
maxSessionMinutes: DurationOption
maxSessionsPerWeek: number | null // null = unlimited
maxOfflineParsingPerMonth: number
}
export interface TierResponse {
tier: TierName
limits: TierLimitsResponse
}
export interface EffectiveTierResponse extends TierResponse {
/** Non-null when a different parent provides the best tier for this student. */
providedBy: { name: string } | null
}
const DEFAULT_TIER: TierResponse = {
tier: 'guest',
limits: {
maxPracticeStudents: 1,
maxSessionMinutes: 10,
maxSessionsPerWeek: null,
maxOfflineParsingPerMonth: 3,
},
}
async function fetchTier(): Promise<TierResponse> {
const res = await fetch('/api/billing/tier')
if (!res.ok) throw new Error('Failed to fetch tier')
return res.json()
}
/**
* Hook to get the current user's subscription tier and limits.
*
* Prefetched server-side in root layout — no extra request on initial load.
* Revalidates in background after 60s stale time.
*/
export function useTier() {
const { data, isLoading } = useQuery({
queryKey: billingKeys.tier(),
queryFn: fetchTier,
staleTime: 60_000,
})
return useMemo(
() => ({
tier: data?.tier ?? DEFAULT_TIER.tier,
limits: data?.limits ?? DEFAULT_TIER.limits,
isLoading,
}),
[data?.tier, data?.limits, isLoading]
)
}
// ---------------------------------------------------------------------------
// Family coverage hook
// ---------------------------------------------------------------------------
export interface FamilyCoverageResponse {
isCovered: boolean
coveredBy: { userId: string; name: string } | null
coveredChildCount: number
totalChildCount: number
}
async function fetchFamilyCoverage(): Promise<FamilyCoverageResponse> {
const res = await fetch('/api/billing/coverage')
if (!res.ok) throw new Error('Failed to fetch family coverage')
return res.json()
}
const DEFAULT_COVERAGE: FamilyCoverageResponse = {
isCovered: false,
coveredBy: null,
coveredChildCount: 0,
totalChildCount: 0,
}
/**
* Hook to check whether any of the current user's children are covered
* by another parent's family subscription.
*
* Used on pricing/settings pages to surface inherited coverage.
*/
export function useFamilyCoverage() {
const { data, isLoading } = useQuery({
queryKey: billingKeys.coverage(),
queryFn: fetchFamilyCoverage,
staleTime: 60_000,
})
return useMemo(
() => ({
isCovered: data?.isCovered ?? DEFAULT_COVERAGE.isCovered,
coveredBy: data?.coveredBy ?? DEFAULT_COVERAGE.coveredBy,
coveredChildCount: data?.coveredChildCount ?? DEFAULT_COVERAGE.coveredChildCount,
totalChildCount: data?.totalChildCount ?? DEFAULT_COVERAGE.totalChildCount,
isLoading,
}),
[data?.isCovered, data?.coveredBy, data?.coveredChildCount, data?.totalChildCount, isLoading]
)
}
// ---------------------------------------------------------------------------
// Effective tier hook (per-student)
// ---------------------------------------------------------------------------
const DEFAULT_EFFECTIVE: EffectiveTierResponse = {
...DEFAULT_TIER,
providedBy: null,
}
async function fetchEffectiveTier(playerId: string): Promise<EffectiveTierResponse> {
const res = await fetch(`/api/players/${playerId}/effective-tier`)
if (!res.ok) throw new Error('Failed to fetch effective tier')
return res.json()
}
/**
* Hook to get the effective subscription tier for a *student*,
* considering all linked parents' plans (not just the logged-in user).
*
* Use this in student-scoped UI (StartPracticeModal, DurationSelector)
* instead of `useTier()` which only returns the acting user's own tier.
*/
export function useEffectiveTier(playerId: string) {
const { data, isLoading } = useQuery({
queryKey: billingKeys.effectiveTier(playerId),
queryFn: () => fetchEffectiveTier(playerId),
staleTime: 60_000,
})
return useMemo(
() => ({
tier: data?.tier ?? DEFAULT_EFFECTIVE.tier,
limits: data?.limits ?? DEFAULT_EFFECTIVE.limits,
providedBy: data?.providedBy ?? null,
isLoading,
}),
[data?.tier, data?.limits, data?.providedBy, isLoading]
)
}
|