Rewrite FestipodDataContext over the Refine seam, meeting-point centric

This commit is contained in:
Sylvain Duchesne
2026-10-09 21:04:35 +02:00
parent b09884d586
commit c195211629
25 changed files with 2949 additions and 1580 deletions
+29 -10
View File
@@ -1,4 +1,5 @@
import { RouterProvider, useRouter } from './router';
import { useEffect } from 'react';
import { RouterProvider, useNavigate, useRouter } from './router';
import { ThemeProvider } from '../shared/context/ThemeContext';
import { NextGraphProvider } from '../shared/context/NextGraphContext';
import { FestipodDataProvider } from '../shared/context/FestipodDataContext';
@@ -38,7 +39,7 @@ function AppContent() {
case 'home': return <HomeScreen />;
case 'events': return <EventsScreen />;
case 'create-event': return <CreateEventScreen />;
case 'login': return null;
case 'login': return <AuthenticatedLoginLanding />;
case 'event-detail': return <EventDetailScreen />;
case 'update-event': return <UpdateEventScreen />;
case 'invite': return <InviteScreen />;
@@ -54,26 +55,44 @@ function AppContent() {
}
}
/**
* What `/login` shows once the gate has let the user through: home. The route
* exists for the Solid-OIDC callback (`AuthGate` intercepts `?code` and
* `?register_app` before children render); a plain `/login` with nothing left
* to do is not a destination — send the user to the app.
*/
function AuthenticatedLoginLanding() {
const navigate = useNavigate();
useEffect(() => {
navigate('/home');
}, [navigate]);
return null;
}
export function App() {
const backendName = initialBackendName();
const isNg = backendName === 'nextgraph';
// THE AUTH GATE ABOVE THE DATA PROVIDER — signing in must have settled before
// any data path mounts. The gate renders children only once there is a
// session to read through (or an explicit offline-demo choice), so the data
// provider's dispatch decision never races a sign-in that has not finished.
const appTree = (
<FestipodDataProvider>
<RouterProvider>
<AuthGate>
<FestipodDataProvider>
<div className="app-container">
<AuthGate>
<AppContent />
</AuthGate>
<AppContent />
<ToastContainer />
</div>
</RouterProvider>
</FestipodDataProvider>
</FestipodDataProvider>
</AuthGate>
);
return (
<ThemeProvider>
{isNg ? <NextGraphProvider>{appTree}</NextGraphProvider> : appTree}
<RouterProvider>
{isNg ? <NextGraphProvider>{appTree}</NextGraphProvider> : appTree}
</RouterProvider>
</ThemeProvider>
);
}
+12 -2
View File
@@ -7,7 +7,9 @@
* a Pod-provider picker when no session exists, and the `/login` route handling
* the OAuth callback plus the ActivityPods app-registration stage. The auth
* provider is built with the app description document served by our backend as
* `clientId` and `${origin}/login` as `redirectUri`.
* `clientId` and `${origin}/login` as `redirectUri`. Every session the flow
* establishes is published to `utils/currentSession` — the module store the
* data provider's dispatch reads, independent of where each mounts in the tree.
*
* 2. NEXTGRAPH (suspended at the root, kept for the URL variant). The legacy
* `ensureIdentity()` flow: the SDK mounts its own full-screen barrier and owns
@@ -26,6 +28,7 @@ import { useEffect, useMemo, useState, type ReactNode } from 'react';
import { ensureIdentity } from '@ng-eventually/polyfill';
import { startNgSession } from '../shared/utils/ngSession';
import { setCurrentPrincipal } from '../shared/utils/currentPrincipal';
import { setCurrentSession } from '../shared/utils/currentSession';
import { useRouter, useNavigate } from './router';
import { initialBackendName } from '../shared/refine/providers';
import { getBackendUrl } from '../shared/utils/runtimeConfig';
@@ -89,7 +92,9 @@ export function AuthGate({ children }: { children: ReactNode }) {
if (stage.stage === 'callback') {
const result = await provider.handleCallback();
if (cancelled) return;
const session = provider.getSession();
setCurrentPrincipal(result.webId);
setCurrentSession(session ? { webId: session.webId, token: session.token } : undefined);
navigate(`/login?register_app=1&redirect=${encodeURIComponent(stage.redirectPath)}`);
return;
}
@@ -99,7 +104,10 @@ export function AuthGate({ children }: { children: ReactNode }) {
const registration = session ? await provider.registerApp(session.webId) : undefined;
if (cancelled) return;
if (registration) {
if (session) setCurrentPrincipal(session.webId);
if (session) {
setCurrentPrincipal(session.webId);
setCurrentSession({ webId: session.webId, token: session.token });
}
navigate(stage.redirectPath);
}
// If `registerApp` returned `undefined`, the browser left for the Pod's
@@ -111,8 +119,10 @@ export function AuthGate({ children }: { children: ReactNode }) {
const session = provider.getSession();
if (session) {
setCurrentPrincipal(session.webId);
setCurrentSession({ webId: session.webId, token: session.token });
if (!cancelled) setApState('authenticated');
} else {
setCurrentSession(undefined);
if (!cancelled) setApState('unauthenticated');
}
} catch (err) {
+7 -33
View File
@@ -1,39 +1,13 @@
import { authProvider as createActivityPodsAuthProvider } from '@activitypods/refine-providers';
import type { AuthProvider } from '@activitypods/refine-providers';
import { getRuntimeConfig } from '../../shared/utils/runtimeConfig';
import { resolveActivityPodsAuthConfig } from '../../shared/utils/runtimeConfig';
/**
* Resolved ActivityPods auth configuration.
*
* `clientId` names the app description document the backend serves, as required
* by the Solid-OIDC flow. `redirectUri` is fixed to `${origin}/login` because the
* app renders its own callback route there.
*/
export interface ResolvedAuthConfig {
clientId: string;
redirectUri: string;
podProviderUrl: string | undefined;
}
/**
* Resolve the auth configuration from runtime values.
*
* - `backendClientId` wins when explicitly set.
* - Otherwise, `backendUrl` is used to derive `${backendUrl}/app`.
* - Otherwise, fall back to `${origin}/app` so local dev still typechecks and
* can be exercised without a backend.
*/
export function resolveActivityPodsAuthConfig(origin: string): ResolvedAuthConfig {
const cfg = getRuntimeConfig();
const backendClientId = cfg.backendClientId?.trim();
const backendUrl = cfg.backendUrl?.trim();
const clientId = backendClientId || (backendUrl ? `${backendUrl}/app` : `${origin}/app`);
return {
clientId,
redirectUri: `${origin}/login`,
podProviderUrl: cfg.podProviderUrl?.trim() || undefined,
};
}
// Re-exported so existing callers (AuthGate, the flow-stage tests) keep their
// import surface; the implementation lives in the runtime-config module, the
// shared source of truth both the auth gate and the data provider resolve
// against — one clientId/redirect for the whole app.
export { resolveActivityPodsAuthConfig };
export type { ResolvedAuthConfig } from '../../shared/utils/runtimeConfig';
/** Build an ActivityPods auth provider with the Festipod-specific configuration. */
export function buildActivityPodsAuthProvider(origin: string): AuthProvider {