Flesh out payment stuff

This commit is contained in:
Joey Yakimowich-Payne 2026-01-22 12:21:12 -07:00
commit acfed861ab
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
27 changed files with 938 additions and 173 deletions

View file

@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from 'react-oidc-context';
import { useAuthenticatedFetch } from './useAuthenticatedFetch';
import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question, GenerateQuizOptions, ProcessedDocument, GameConfig, DEFAULT_GAME_CONFIG, PointsBreakdown } from '../types';
import { generateQuiz } from '../services/geminiService';
import { QUESTION_TIME, QUESTION_TIME_MS, PLAYER_COLORS, calculatePointsWithBreakdown, getPlayerRank } from '../constants';
@ -96,10 +97,11 @@ const clearDraftQuiz = () => {
sessionStorage.removeItem(DRAFT_QUIZ_KEY);
};
export const useGame = () => {
export const useGame = (defaultGameConfig?: GameConfig) => {
const navigate = useNavigate();
const location = useLocation();
const auth = useAuth();
const { authFetch } = useAuthenticatedFetch();
const [role, setRole] = useState<GameRole>('HOST');
const [gameState, setGameState] = useState<GameState>('LANDING');
@ -120,7 +122,9 @@ export const useGame = () => {
const [currentPlayerName, setCurrentPlayerName] = useState<string | null>(null);
const [pendingQuizToSave, setPendingQuizToSave] = useState<{ quiz: Quiz; topic: string } | null>(null);
const [sourceQuizId, setSourceQuizId] = useState<string | null>(null);
const [gameConfig, setGameConfig] = useState<GameConfig>(DEFAULT_GAME_CONFIG);
const [gameConfig, setGameConfig] = useState<GameConfig>(defaultGameConfig || DEFAULT_GAME_CONFIG);
const defaultConfigRef = useRef<GameConfig>(defaultGameConfig || DEFAULT_GAME_CONFIG);
const [subscriptionAccessType, setSubscriptionAccessType] = useState<'group' | 'subscription' | 'none'>('none');
const [firstCorrectPlayerId, setFirstCorrectPlayerId] = useState<string | null>(null);
const [hostSecret, setHostSecret] = useState<string | null>(null);
const [isReconnecting, setIsReconnecting] = useState(false);
@ -149,6 +153,10 @@ export const useGame = () => {
useEffect(() => { currentQuestionIndexRef.current = currentQuestionIndex; }, [currentQuestionIndex]);
useEffect(() => { quizRef.current = quiz; }, [quiz]);
useEffect(() => { gameConfigRef.current = gameConfig; }, [gameConfig]);
useEffect(() => {
if (!defaultGameConfig) return;
defaultConfigRef.current = defaultGameConfig;
}, [defaultGameConfig]);
useEffect(() => { gamePinRef.current = gamePin; }, [gamePin]);
useEffect(() => { hostSecretRef.current = hostSecret; }, [hostSecret]);
useEffect(() => { gameStateRef.current = gameState; }, [gameState]);
@ -156,6 +164,38 @@ export const useGame = () => {
useEffect(() => { currentCorrectShapeRef.current = currentCorrectShape; }, [currentCorrectShape]);
useEffect(() => { presenterIdRef.current = presenterId; }, [presenterId]);
useEffect(() => {
let isMounted = true;
if (auth.isLoading || !auth.isAuthenticated) {
setSubscriptionAccessType('none');
return () => {
isMounted = false;
};
}
const fetchSubscriptionAccess = async () => {
try {
const response = await authFetch('/api/payments/status');
if (!response.ok) return;
const data = await response.json();
if (!isMounted) return;
setSubscriptionAccessType(
data.accessType === 'group' ? 'group' : (data.accessType === 'subscription' ? 'subscription' : 'none')
);
} catch {
if (!isMounted) return;
setSubscriptionAccessType('none');
}
};
fetchSubscriptionAccess();
return () => {
isMounted = false;
};
}, [auth.isLoading, auth.isAuthenticated, authFetch]);
const isInitializingFromUrl = useRef(false);
useEffect(() => {
@ -723,10 +763,12 @@ export const useGame = () => {
};
const generatedQuiz = await generateQuiz(generateOptions);
const withDefaultConfig = { ...generatedQuiz, config: defaultConfigRef.current };
const saveLabel = options.topic || options.files?.map(f => f.name).join(', ') || '';
setPendingQuizToSave({ quiz: generatedQuiz, topic: saveLabel });
setQuiz(generatedQuiz);
storeDraftQuiz({ quiz: generatedQuiz, topic: saveLabel });
setPendingQuizToSave({ quiz: withDefaultConfig, topic: saveLabel });
setQuiz(withDefaultConfig);
setGameConfig(defaultConfigRef.current);
storeDraftQuiz({ quiz: withDefaultConfig, topic: saveLabel });
setGameState('EDITING');
} catch (e) {
const message = e instanceof Error ? e.message : "Failed to generate quiz.";
@ -741,6 +783,7 @@ export const useGame = () => {
const startManualCreation = () => {
setRole('HOST');
setGameConfig(defaultConfigRef.current);
setGameState('CREATING');
};
@ -758,6 +801,7 @@ export const useGame = () => {
const loadSavedQuiz = (savedQuiz: Quiz, quizId?: string) => {
setRole('HOST');
setQuiz(savedQuiz);
setGameConfig(savedQuiz.config || defaultConfigRef.current);
setSourceQuizId(quizId || null);
storeDraftQuiz({ quiz: savedQuiz, sourceQuizId: quizId });
setGameState('EDITING');
@ -818,6 +862,20 @@ export const useGame = () => {
if (!reconnectedPlayer && gameConfigRef.current.randomNamesEnabled) {
assignedName = generateRandomName();
}
if (!reconnectedPlayer) {
const configuredMax = gameConfigRef.current.maxPlayers || 0;
const cap = subscriptionAccessType === 'subscription' || subscriptionAccessType === 'group' ? 150 : 10;
const effectiveMax = Math.min(configuredMax || cap, cap);
const realPlayersCount = playersRef.current.filter(p => p.id !== 'host').length;
if (realPlayersCount >= effectiveMax) {
conn.send({ type: 'JOIN_DENIED', payload: { reason: 'Lobby is full.' } });
connectionsRef.current.delete(conn.peer);
conn.close();
return;
}
}
let updatedPlayers = playersRef.current;
let newPlayer: Player | null = null;
@ -1294,6 +1352,12 @@ export const useGame = () => {
};
const handleClientData = (data: NetworkMessage) => {
if (data.type === 'JOIN_DENIED') {
setError(data.payload.reason || 'Unable to join the game.');
setGamePin(null);
setGameState('LANDING');
return;
}
if (data.type === 'WELCOME') {
const payload = data.payload;
console.log('[CLIENT] Received WELCOME:', {