All files / src/contexts ConfigContext.tsx

33.33% Statements 6/18
33.33% Branches 2/6
40% Functions 2/5
31.25% Lines 5/16

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                                                                            165x   165x   63x                                                           165x   165x    
import React, { useEffect, useState } from 'react';
import type { AppConfig } from '../config/runtime-config';
import { loadRuntimeConfig } from '../config/runtime-config';
import { updateApiClientConfig } from '../services/api/apiClient';
import {
  setAmplifyRuntimeConfig,
  expectAmplifyRuntimeConfig,
  cancelExpectedAmplifyRuntimeConfig,
} from '../config/amplify';
import { ConfigContext } from './createConfigContext';
 
export interface ConfigProviderProps {
  /**
   * Explicit configuration. Primarily for tests / Storybook. When supplied, the
   * provider uses it directly and performs NO runtime fetch (preserves the original
   * prop-driven behaviour).
   */
  config?: AppConfig;
  children: React.ReactNode;
}
 
/**
 * Configuration Provider Component
 *
 * Renders its children IMMEDIATELY (the app shell paints without waiting for the
 * `GET /api/v1/config` round-trip), then loads runtime configuration in the
 * background. On resolve it wires the API client + Amplify and publishes the config
 * via context.
 *
 * Decoupling rationale (perf/public-homepage-followup):
 * - The public shell consumes no config; the API base URL is set synchronously at
 *   bootstrap (see main.tsx → getDefaultApiBaseUrl), so public data loads in parallel.
 * - `useConfig()` still throws if it reads a null config, but its only public-path
 *   consumer (`useTurnstile`) uses `useOptionalConfig()` and treats null as "not ready".
 * - A config-load failure is NON-fatal: public pages keep working; we never blank the
 *   whole app on it (unlike the old blocking bootstrap).
 */
export function ConfigProvider({ config: providedConfig, children }: ConfigProviderProps) {
  const [loadedConfig, setLoadedConfig] = useState<AppConfig | null>(null);
 
  useEffect(() => {
    // Explicit config supplied (tests) — skip the runtime fetch entirely.
    Eif (providedConfig) return;
 
    let cancelled = false;
    // 12.8 F8: announce the in-flight fetch so ensureAmplifyConfigured() WAITS for it
    // instead of silently no-op'ing (auth-touching calls — e.g. the /auth/callback code
    // exchange — raced this round-trip and left Amplify unconfigured forever).
    expectAmplifyRuntimeConfig();
    loadRuntimeConfig()
      .then((cfg) => {
        // Module-level stashes run regardless of unmount — config arrival is a fact, and
        // skipping them would strand the waiters inside ensureAmplifyConfigured().
        updateApiClientConfig(cfg.apiBaseUrl);
        setAmplifyRuntimeConfig(cfg);
        if (cancelled) return;
        setLoadedConfig(cfg);
      })
      .catch((error) => {
        // Non-fatal. Public pages render and fetch via the hostname-derived base URL
        // set at bootstrap; auth/feature-flag UI that needs config surfaces its own
        // error. Do NOT blank the whole app (the old behaviour) — that would defeat
        // "show the page immediately".
        cancelExpectedAmplifyRuntimeConfig();
        console.error('[Config] Runtime config unavailable; continuing without it:', error);
      });
 
    return () => {
      cancelled = true;
    };
  }, [providedConfig]);
 
  const value = providedConfig ?? loadedConfig;
 
  return <ConfigContext.Provider value={value}>{children}</ConfigContext.Provider>;
}