Clean up and fixes

This commit is contained in:
2026-02-08 02:32:50 +00:00
parent eb1bf9020e
commit da096af0fa
36 changed files with 2119 additions and 1751 deletions

View File

@@ -4,3 +4,10 @@
export { NavigationProvider, useNavigation } from './useNavigation.js';
export { AppProvider, useAppContext, useDialog, useStatus } from './useAppContext.js';
export {
useInvitations,
useInvitation,
useInvitationData,
useCreateInvitation,
useInvitationIds,
} from './useInvitations.js';

View File

@@ -1,10 +1,10 @@
/**
* App context hook for accessing controllers and app-level functions.
* App context hook for accessing AppService and app-level functions.
*/
import React, { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
import type { WalletController } from '../../controllers/wallet-controller.js';
import type { InvitationController } from '../../controllers/invitation-controller.js';
import { AppService } from '../../services/app.js';
import type { AppConfig } from '../../app.js';
import type { AppContextType, DialogState } from '../types.js';
/**
@@ -37,27 +37,50 @@ const StatusContext = createContext<StatusContextType | null>(null);
*/
interface AppProviderProps {
children: ReactNode;
walletController: WalletController;
invitationController: InvitationController;
config: AppConfig;
onExit: () => void;
}
/**
* App provider component.
* Provides controllers, dialog management, and app-level functions to children.
* Provides AppService, dialog management, and app-level functions to children.
*/
export function AppProvider({
children,
walletController,
invitationController,
config,
onExit,
}: AppProviderProps): React.ReactElement {
const [appService, setAppService] = useState<AppService | null>(null);
const [dialog, setDialog] = useState<DialogState | null>(null);
const [status, setStatusState] = useState<string>('Ready');
const [isWalletInitialized, setWalletInitialized] = useState(false);
// Promise resolver for confirm dialogs
const [confirmResolver, setConfirmResolver] = useState<((value: boolean) => void) | null>(null);
/**
* Initialize wallet with seed phrase and create AppService.
*/
const initializeWallet = useCallback(async (seed: string) => {
try {
// Create the AppService with the seed
const service = await AppService.create(seed, {
syncServerUrl: config.syncServerUrl,
engineConfig: {
databasePath: config.databasePath,
databaseFilename: config.databaseFilename,
},
invitationStoragePath: config.invitationStoragePath,
});
// Start the AppService (loads existing invitations)
await service.start();
// Set the service and mark as initialized
setAppService(service);
setWalletInitialized(true);
} catch (error) {
// Re-throw the error so the caller can handle it
throw error;
}
}, [config]);
/**
* Show an error dialog.
@@ -88,7 +111,6 @@ export function AppProvider({
*/
const confirm = useCallback((message: string): Promise<boolean> => {
return new Promise((resolve) => {
setConfirmResolver(() => resolve);
setDialog({
visible: true,
type: 'confirm',
@@ -113,15 +135,15 @@ export function AppProvider({
}, []);
const appValue: AppContextType = {
walletController,
invitationController,
appService,
initializeWallet,
isWalletInitialized,
config,
showError,
showInfo,
confirm,
exit: onExit,
setStatus,
isWalletInitialized,
setWalletInitialized,
};
const dialogValue: DialogContextType = {

View File

@@ -0,0 +1,144 @@
/**
* Performance-optimized invitation hooks.
* Uses useSyncExternalStore for fine-grained reactivity.
*/
import { useSyncExternalStore, useMemo, useCallback } from 'react';
import type { Invitation } from '../../services/invitation.js';
import type { XOInvitation } from '@xo-cash/types';
import { useAppContext } from './useAppContext.js';
/**
* Get all invitations reactively.
* Re-renders when invitations are added or removed.
*/
export function useInvitations(): Invitation[] {
const { appService } = useAppContext();
const subscribe = useCallback(
(callback: () => void) => {
if (!appService) {
return () => {};
}
// Subscribe to invitation list changes
const onAdded = () => callback();
const onRemoved = () => callback();
appService.on('invitation-added', onAdded);
appService.on('invitation-removed', onRemoved);
return () => {
appService.off('invitation-added', onAdded);
appService.off('invitation-removed', onRemoved);
};
},
[appService]
);
const getSnapshot = useCallback(() => {
return appService?.invitations ?? [];
}, [appService]);
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}
/**
* Get a single invitation by ID with selective re-rendering.
* Only re-renders when the specific invitation is updated.
*/
export function useInvitation(invitationId: string | null): Invitation | null {
const { appService } = useAppContext();
const subscribe = useCallback(
(callback: () => void) => {
if (!appService || !invitationId) {
return () => {};
}
// Find the invitation instance
const invitation = appService.invitations.find(
(inv) => inv.data.invitationIdentifier === invitationId
);
if (!invitation) {
return () => {};
}
// Subscribe to this specific invitation's updates
const onUpdated = () => callback();
const onStatusChanged = () => callback();
invitation.on('invitation-updated', onUpdated);
invitation.on('invitation-status-changed', onStatusChanged);
// Also subscribe to list changes in case the invitation is removed
const onRemoved = () => callback();
appService.on('invitation-removed', onRemoved);
return () => {
invitation.off('invitation-updated', onUpdated);
invitation.off('invitation-status-changed', onStatusChanged);
appService.off('invitation-removed', onRemoved);
};
},
[appService, invitationId]
);
const getSnapshot = useCallback(() => {
if (!appService || !invitationId) {
return null;
}
return (
appService.invitations.find(
(inv) => inv.data.invitationIdentifier === invitationId
) ?? null
);
}, [appService, invitationId]);
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}
/**
* Get invitation data with memoization.
* Returns stable references to prevent unnecessary re-renders.
*/
export function useInvitationData(invitationId: string | null): XOInvitation | null {
const invitation = useInvitation(invitationId);
return useMemo(() => {
return invitation?.data ?? null;
}, [invitation?.data.invitationIdentifier, invitation?.data.commits?.length]);
}
/**
* Hook to create invitations.
* Returns a memoized function to create invitations.
*/
export function useCreateInvitation() {
const { appService } = useAppContext();
return useCallback(
async (invitation: XOInvitation | string): Promise<Invitation> => {
if (!appService) {
throw new Error('AppService not initialized');
}
return await appService.createInvitation(invitation);
},
[appService]
);
}
/**
* Hook to get all invitations with their IDs.
* Useful for lists where you only need IDs (prevents re-renders on data changes).
*/
export function useInvitationIds(): string[] {
const invitations = useInvitations();
return useMemo(() => {
return invitations.map((inv) => inv.data.invitationIdentifier);
}, [invitations]);
}