04 - State Management¶
Dokument: State-Management-Architektur Stand: 28.02.2026 Scope: Provider, Context, Custom Hooks Status: Audit-ready
1. Architektur-Übersicht¶
Die App verwendet React Context API + Custom Hooks (kein Redux, Zustand o.ä.). State wird über 3 Context-Provider bereitgestellt und durch 10 spezialisierte Hooks konsumiert.
Provider-Stack¶
NextIntlClientProvider (i18n)
└─ TelegramProvider
└─ Web3AuthProvider (Haupt-State, 1.152 Zeilen)
└─ DeviceProvider
2. Web3AuthProvider¶
Datei: app/providers/Web3AuthProvider.tsx (1.152 Zeilen)
Pattern: Singleton (globale web3authInstance, wiederverwendbar nach Logout)
2.1 State-Variablen¶
| Variable | Typ | Beschreibung |
|---|---|---|
web3auth |
Web3Auth \| null |
Web3Auth-Instanz |
provider |
IProvider \| null |
JSON-RPC Provider |
isInitialized |
boolean |
Web3Auth init() abgeschlossen |
isConnected |
boolean |
User eingeloggt |
isLoading |
boolean |
Operation läuft |
address |
string \| null |
Wallet-Adresse (checksummed) |
balance |
string \| null |
ETH-Guthaben (formatiert) |
usdtBalance |
string \| null |
USDT-Guthaben (2 Dezimalstellen) |
userInfo |
UserInfo \| null |
Web3Auth Profil (email, name, profileImage) |
walletError |
string \| null |
Fehlermeldung |
playerStats |
PlayerStats \| null |
Tagesstatistiken |
poolStats |
PoolStats \| null |
Pool-Daten (Jackpots, Reserves, Streaks) |
claimStatus |
ClaimStatus \| null |
Verfügbare Gewinne |
affiliate |
string \| null |
Gebundener Affiliate |
userProfile |
UserProfile \| null |
Vollständiges DB-Profil |
showProfileModal |
boolean |
Profil-Modal anzeigen |
2.2 Throttling¶
| Ref | Cooldown | Zweck |
|---|---|---|
lastBalanceRefresh |
10.000 ms | Verhindert RPC-Überflutung |
lastStatsRefresh |
10.000 ms | Verhindert API-Überflutung |
2.3 Exportierte Funktionen¶
| Funktion | Beschreibung |
|---|---|
login() |
Web3Auth Modal öffnen |
loginWithEmail(email) |
Direkter E-Mail-Login (Telegram) |
loginWithSocial(provider) |
Social Login (Google, Apple) |
logout() |
Session beenden, State zurücksetzen |
refreshBalance(force?) |
ETH + USDT neu laden (mit Throttling) |
refreshStats(force?) |
Player/Pool-Stats neu laden (mit Throttling) |
refreshUserProfile() |
Profil aus DB neu laden |
getSigner() |
ethers.Signer für Signaturen |
getEthersProvider() |
ethers.BrowserProvider |
openWalletUI() |
Web3Auth Wallet-UI öffnen |
openBuyCrypto() |
Fiat On-Ramp (Krypto kaufen) |
openSwap() |
Token-Swap-UI |
openBridge() |
Bridge-UI |
2.4 Typ-Definitionen¶
PlayerStats:
PoolStats:
{
jackpots: Record<number, number>,
jackpotReserves: Record<number, number>,
noHitStreaks: Record<number, number>,
charityTotal: number
}
ClaimStatus:
{
winnerCount: number, winnerTotal: string,
affiliateCount: number, affiliateTotal: string,
totalCount: number, totalAmount: string, hasClaims: boolean,
affiliateLocked?: boolean,
affiliateLockReason?: "not_sunday" | "insufficient_activity",
affiliateActualTips?: number, affiliateRequiredTips?: number
}
UserProfile:
{
walletAddress: string,
firstName/lastName/email: string | null,
emailVerified/phoneVerified: boolean,
dateOfBirth/nationality: string | null,
addressStreet/City/PostalCode/State/Country: string | null,
kycStatus: "none" | "pending" | "verified" | "rejected",
kycLevel: number,
dailyLimitUsdt/weeklyLimitUsdt/monthlyLimitUsdt: number | null,
selfExclusionUntil: string | null
}
3. TelegramProvider¶
Datei: app/providers/TelegramProvider.tsx
Context-Werte¶
| Wert | Typ | Beschreibung |
|---|---|---|
isTelegram |
boolean |
In Telegram WebView |
isTelegramMobile |
boolean |
Android/iOS WebView |
telegramUser |
TelegramUser \| null |
TG User-Info |
startParam |
string \| null |
Affiliate-Ref aus /start |
Erkennung¶
- Prüft
window.Telegram?.WebApp?.initData - Platform: android, android_x, ios = Mobile
- Ruft
tgWebApp.expand()undtgWebApp.ready()auf
4. DeviceProvider¶
Datei: app/hooks/useDeviceContext.tsx
Context-Werte¶
| Wert | Logik |
|---|---|
isMobile |
UA-Check ODER (Touch + Screen < 768px) ODER Telegram |
isDesktop |
!isMobile UND !Telegram |
isTablet |
Touch + minDimension >= 480px + width >= 1024px + !Telegram |
isTelegram |
Aus TelegramProvider |
isIOS |
iPad/iPhone/iPod im UA |
isStandalone |
PWA-Modus oder navigator.standalone |
Side Effects¶
- Synchrone Initial-Erkennung (kein Layout-Flash)
- Window
resizeEvent-Listener - Neuberechnung bei
isTelegram-Änderung
5. Custom Hooks (10)¶
5.1 useBuyFlow¶
Datei: app/hooks/useBuyFlow.ts (~592 Zeilen)
Zweck: 6-stufiger Ticket-Kauf-Flow
Return:
{
step: BuyStep,
intent: IntentResponse | null,
txHash: string | null,
result: ConfirmTxResponse["result"] | null,
error: string | null,
showModal: boolean,
startBuy(poolId, tipps, paidCount?, bonusCount?): Promise<void>,
resumeIntent(intentId): Promise<void>,
reset(): void,
closeModal(): void
}
BuyStep States:
idle → creating_intent → signing → signed → approving → approve_confirmed
→ submitting → tx_pending → confirming → confirmed
Error States: error_balance, error_unsettled, error_limit, error_self_excluded, error_tx_in_progress, error_no_gas, error_allowance, error_rejected, error_timeout, error_network, error_expired, signature_cancelled
API-Calls: 6 Endpoints (intent, confirm-signature, gas-faucet, submit-tx, confirm-tx, resume-intent)
Siehe 05 - Buy Flow für Details.
5.2 useClaimStatus¶
Datei: app/hooks/useClaimStatus.ts
Zweck: Claim-Status mit Signatur-Authentifizierung
Return:
{
isLoading: boolean,
hasClaims: boolean, // totalCount > 0 && !hasPending
winnerCount/affiliateCount/totalCount: number,
winnerTotal/affiliateTotal/totalAmount: string,
hasPending: boolean,
error: string | null,
refresh(): Promise<void>
}
| Eigenschaft | Beschreibung |
|---|---|
| Authentifizierung | EIP-191 signierte Nachricht erforderlich |
| Polling | KEIN automatisches Polling (Claims ändern sich nur 1x/Tag) |
| Fetch | Einmalig pro Adresse, manuell via refresh() |
| Light-Version | useClaimStatusLight ohne Signatur (Debug-Endpoint) |
5.3 useDashboard¶
Datei: app/hooks/useDashboard.ts
Zweck: Dashboard-Daten laden
Return:
{
data: DashboardData | null,
isLoading: boolean,
error: string | null,
fetchDashboard(address): Promise<void>,
refresh(address): void
}
DashboardData enthält: - Overview: totalTickets, totalTips, totalSpent, spentToday, totalWon, netResult - Win Stats: totalWins, winsByClass (class2-5), bestMatch - Listen: recentTickets[], pendingClaims[], poolStats[] - Win History: winHistory[] (nach Tag mit Klassen-Breakdown) - Affiliate: boundTo, referralCode, referralCount, totalEarned, pendingEarnings, referrals[]
Helper-Funktionen: formatUsdt(), formatAddress(), formatDate(), getNetResultColor(), getNetResultEmoji()
5.4 useGlobalStats¶
Datei: app/hooks/useGlobalStats.ts
Zweck: Plattformweite Statistiken mit Pagination
Return:
{
data: GlobalStatsData | null,
isLoading: boolean,
error: string | null,
selectedPool: number,
changePool(poolId): void,
daysToShow: number, // Default: 3, Max: 14
loadMore(): void, // +3 Tage
canLoadMore: boolean,
refresh(poolId?, days?): Promise<void>
}
| Eigenschaft | Beschreibung |
|---|---|
| Auto-Fetch | Trigger bei Pool- oder Tages-Änderung |
| Reset | daysToShow zurück auf Default bei Pool-Wechsel |
| Daten | today, 7d-metrics, alltime, jackpots, prizeHistory, winDistribution, hotNumbers, coldNumbers, recentWins, charityBalance |
5.5 useBonusBalance¶
Datei: app/hooks/useBonusBalance.ts
Zweck: Bonus-Tips-Guthaben und -Historie
Return:
{
totalBonus: number,
byType: Record<string, number>,
history: BonusHistoryItem[],
isLoading: boolean,
error: string | null,
refetch(): void
}
| Eigenschaft | Beschreibung |
|---|---|
| Event-Listener | bonus-tips-changed Event für sofortige Updates |
| Auto-Fetch | Bei Adress-Änderung |
| History | id, amount, reason, createdAt, used, usedAt, sourceTicketId |
5.6 usePrizeHistory¶
Datei: app/hooks/usePrizeHistory.ts
Zweck: Gewinnhistorie pro Pool
Return:
{
data: PrizeHistoryDay[],
isLoading: boolean,
error: string | null,
fetchPrizeHistory(poolId?, days?): Promise<void>,
refresh(poolId?, days?): void
}
Helper: getClassColor(classId), getClassEmoji(classId)
5.7 useMaintenanceStatus¶
Datei: app/hooks/useMaintenanceStatus.ts
Zweck: Maintenance-Modus-Erkennung
Return: boolean (true = Maintenance)
| Eigenschaft | Wert |
|---|---|
| Polling-Intervall | 30 Sekunden |
| Endpoint | GET /api/maintenance |
| Fehlerverhalten | Annahme: KEIN Maintenance bei Fehler |
5.8 useOnlineStatus¶
Datei: app/hooks/useOnlineStatus.ts
Zweck: Online/Offline-Erkennung
Return: boolean (true = online)
- Verwendet
navigator.onLine - Event-Listener:
online,offline - SSR-Default:
true
5.9 usePullToRefresh¶
Datei: app/hooks/usePullToRefresh.ts
Zweck: Pull-to-Refresh-Geste (Mobile)
Parameter:
{
containerRef: RefObject<HTMLElement>,
options: {
onRefresh: () => Promise<void> | void,
threshold?: number, // Default: 60px
maxPull?: number // Default: 120px
}
}
Return:
{
pullDistance: number,
isRefreshing: boolean,
indicatorStyle: CSSProperties,
onTouchStart/onTouchMove/onTouchEnd: EventHandler
}
| Feature | Beschreibung |
|---|---|
| Damping | 50% Dämpfung auf Pull-Distanz |
| Threshold | 60px Mindest-Pull für Refresh |
| Transition | 200ms ease-out beim Loslassen |
| Bedingung | Nur aktiv wenn scrollTop === 0 |
5.10 (Implicit) useWeb3Auth¶
Hook: Zugriff auf Web3AuthProvider Context Validierung: Wirft Fehler wenn außerhalb des Providers verwendet.
6. Zusammenfassung¶
| Hook | Typ | Async | Polling | Auth | API-Calls |
|---|---|---|---|---|---|
| useBuyFlow | Transaktion | Ja | Nein | EIP-712 | 6 |
| useClaimStatus | Query | Ja | Nein | EIP-191 | 1 |
| useDashboard | Query | Ja | Nein | Nein | 1 |
| useGlobalStats | Query | Ja | Auto | Nein | 1 |
| useBonusBalance | Query | Ja | Event | Nein | 1 |
| usePrizeHistory | Query | Ja | Nein | Nein | 1 |
| useDeviceContext | Detection | Nein | Resize | Nein | 0 |
| useMaintenanceStatus | Query | Ja | 30s | Nein | 1 |
| useOnlineStatus | Detection | Nein | Events | Nein | 0 |
| usePullToRefresh | Gesture | Async | Nein | Nein | 0 |
Weiterführende Dokumente: - 05 - Buy Flow - 06 - Claim Flow - 02 - Authentifizierung