import React, { createContext, useContext, useMemo, useCallback, useState, useEffect } from 'react'; import { ThemeOverrideProvider } from './ui/theme'; import type { DubsTheme } from './ui/theme'; import { Connection } from '@solana/web3.js'; import { DubsClient } from './client'; import { NETWORK_CONFIG } from './constants'; import type { DubsNetwork } from './constants'; import type { WalletAdapter } from './wallet/types'; import type { TokenStorage } from './storage'; import { createSecureStoreStorage, STORAGE_KEYS } from './storage'; import { ManagedWalletProvider, useDisconnect } from './managed-wallet'; import { AuthGate } from './ui/AuthGate'; import type { RegistrationScreenProps } from './ui/AuthGate'; import type { ConnectWalletScreenProps } from './ui/ConnectWalletScreen'; import type { AuthStatus, UiConfig } from './types'; // ── Context ── export interface DubsContextValue { client: DubsClient; wallet: WalletAdapter; connection: Connection; appName: string; network: DubsNetwork; disconnect: () => Promise; uiConfig: UiConfig; pushEnabled: boolean; } const DubsContext = createContext(null); // ── Props ── export interface DubsProviderProps { apiKey: string; children: React.ReactNode; appName?: string; network?: DubsNetwork; /** Escape hatch: bring your own wallet adapter. Disables managed MWA. */ wallet?: WalletAdapter; /** Escape hatch: custom token persistence. Defaults to expo-secure-store. */ tokenStorage?: TokenStorage; /** Escape hatch: override base URL (takes precedence over network). */ baseUrl?: string; /** Escape hatch: override RPC URL (takes precedence over network). */ rpcUrl?: string; /** Custom connect screen, or false to hide it entirely (headless mode). */ renderConnectScreen?: ((props: ConnectWalletScreenProps) => React.ReactNode) | false; /** Custom loading screen shown during auth. */ renderLoading?: (status: AuthStatus) => React.ReactNode; /** Custom error screen shown on auth failure. */ renderError?: (error: Error, retry: () => void) => React.ReactNode; /** Custom registration screen. */ renderRegistration?: (props: RegistrationScreenProps) => React.ReactNode; /** Set to false to skip AuthGate and connect screen (headless/BYOA mode). Default: true. */ managed?: boolean; /** Deeplink redirect URI for Phantom wallet (required for iOS support). */ redirectUri?: string; /** App URL shown in Phantom's connect screen. */ appUrl?: string; /** Enable Dubs push notifications. Default: true. Set false if your app manages its own Firebase/push. */ pushEnabled?: boolean; } // ── Provider ── export function DubsProvider({ apiKey, children, appName = 'Dubs', network = 'mainnet-beta', wallet: externalWallet, tokenStorage, baseUrl: baseUrlOverride, rpcUrl: rpcUrlOverride, renderConnectScreen, renderLoading, renderError, renderRegistration, managed = true, redirectUri, appUrl, pushEnabled = true, }: DubsProviderProps) { // Resolve network config — explicit props override network defaults const config = NETWORK_CONFIG[network]; const baseUrl = baseUrlOverride || config.baseUrl; const rpcUrl = rpcUrlOverride || config.rpcUrl; // Create stable instances const client = useMemo(() => new DubsClient({ apiKey, baseUrl }), [apiKey, baseUrl]); const storage = useMemo(() => tokenStorage || createSecureStoreStorage(), [tokenStorage]); // Fetch developer UI config on mount (silent fail = default theme) // Also auto-detects sandbox/production environment from the API key const [uiConfig, setUiConfig] = useState(null); const [resolvedNetwork, setResolvedNetwork] = useState(network); useEffect(() => { client.getAppConfig() .then((cfg) => { console.log('[DubsProvider] UI config loaded:', JSON.stringify(cfg)); setUiConfig(cfg); // Auto-detect network from API key environment (sandbox → devnet) // Only override if the user didn't explicitly set network or rpcUrl if (cfg.environment === 'sandbox' && network === 'mainnet-beta' && !rpcUrlOverride) { console.log('[DubsProvider] Sandbox API key detected — auto-switching to devnet'); setResolvedNetwork('devnet'); } }) .catch((err) => { console.log('[DubsProvider] UI config fetch failed, using defaults:', err?.message); setUiConfig({}); }); }, [client]); // Use resolved network (may differ from prop if sandbox was auto-detected) const resolvedConfig = NETWORK_CONFIG[resolvedNetwork]; const resolvedRpcUrl = rpcUrlOverride || resolvedConfig.rpcUrl; const cluster = resolvedConfig.cluster; // Connection uses the resolved RPC (auto-switched to devnet for sandbox keys) const connection = useMemo(() => new Connection(resolvedRpcUrl, { commitment: 'confirmed' }), [resolvedRpcUrl]); // Wait for config before rendering so accent color is applied on first paint if (uiConfig === null) return null; // Build theme overrides from server config so all SDK components respect the tint const themeOverrides: Partial = {}; if (uiConfig.accentColor) { themeOverrides.accent = uiConfig.accentColor; } // ── Path A: External wallet provided (BYOA) ── if (externalWallet) { return ( {children} ); } // ── Path B: Managed MWA wallet ── return ( {(adapter) => ( {children} )} ); } // ── ManagedInner: context + AuthGate for managed wallet path ── function ManagedInner({ client, connection, wallet, appName, network, storage, renderLoading, renderError, renderRegistration, accentColor, uiConfig, pushEnabled, children, }: { client: DubsClient; connection: Connection; wallet: WalletAdapter; appName: string; network: DubsNetwork; storage: TokenStorage; renderLoading?: (status: AuthStatus) => React.ReactNode; renderError?: (error: Error, retry: () => void) => React.ReactNode; renderRegistration?: (props: RegistrationScreenProps) => React.ReactNode; accentColor?: string; uiConfig: UiConfig; pushEnabled: boolean; children: React.ReactNode; }) { const managedDisconnect = useDisconnect(); const disconnect = useCallback(async () => { // Clear client JWT client.setToken(null); // Delegate to managed wallet (clears adapter + storage) await managedDisconnect?.(); }, [client, managedDisconnect]); const value = useMemo( () => ({ client, wallet, connection, appName, network, disconnect, uiConfig, pushEnabled }), [client, wallet, connection, appName, network, disconnect, uiConfig, pushEnabled], ); return ( { if (token) return storage.setItem(STORAGE_KEYS.JWT_TOKEN, token); return storage.deleteItem(STORAGE_KEYS.JWT_TOKEN); }} onLoadToken={() => storage.getItem(STORAGE_KEYS.JWT_TOKEN)} renderLoading={renderLoading} renderError={renderError} renderRegistration={renderRegistration} appName={appName} accentColor={accentColor} > {children} ); } // ── ExternalWalletProvider: for BYOA path ── function ExternalWalletProvider({ client, connection, wallet, appName, network, storage, managed, renderLoading, renderError, renderRegistration, accentColor, uiConfig, pushEnabled, children, }: { client: DubsClient; connection: Connection; wallet: WalletAdapter; appName: string; network: DubsNetwork; storage: TokenStorage; managed: boolean; renderLoading?: (status: AuthStatus) => React.ReactNode; renderError?: (error: Error, retry: () => void) => React.ReactNode; renderRegistration?: (props: RegistrationScreenProps) => React.ReactNode; accentColor?: string; uiConfig: UiConfig; pushEnabled: boolean; children: React.ReactNode; }) { const disconnect = useCallback(async () => { client.setToken(null); await storage.deleteItem(STORAGE_KEYS.JWT_TOKEN).catch(() => {}); await wallet.disconnect?.(); }, [client, storage, wallet]); const value = useMemo( () => ({ client, wallet, connection, appName, network, disconnect, uiConfig, pushEnabled }), [client, wallet, connection, appName, network, disconnect, uiConfig, pushEnabled], ); if (!managed) { // Headless mode — just context, no AuthGate return {children}; } return ( { if (token) return storage.setItem(STORAGE_KEYS.JWT_TOKEN, token); return storage.deleteItem(STORAGE_KEYS.JWT_TOKEN); }} onLoadToken={() => storage.getItem(STORAGE_KEYS.JWT_TOKEN)} renderLoading={renderLoading} renderError={renderError} renderRegistration={renderRegistration} appName={appName} accentColor={accentColor} > {children} ); } // ── Hooks ── export function useDubs(): DubsContextValue { const ctx = useContext(DubsContext); if (!ctx) { throw new Error('useDubs must be used within a '); } return ctx; } export function useAppConfig(): UiConfig { const ctx = useContext(DubsContext); return ctx?.uiConfig || {}; }