Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 19 additions & 18 deletions src/arrange-v4/app/books/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
'use client';

import { useState, useEffect } from 'react';
import { getUserInfo } from '@/lib/graphService';
import { useStore } from '@/lib/store/useStore';
import type { Book } from '@/lib/store/types';
import { useGraphToken } from '@/lib/hooks/useGraphToken';
import { useAuthClient } from '@/lib/auth/useAuthClient';
import { useSetTopBarActions } from '@/components/TopBarProvider';
import CalendarList from '@/components/CalendarList';
import CreateCalendar from '@/components/CreateCalendar';
import styles from './page.module.css';

export default function BooksPage() {
const { acquireToken, isAuthenticated, inProgress, handleLogin: graphLogin, instance } = useGraphToken();
const auth = useAuthClient();
const { isAuthenticated, busy } = auth;
const store = useStore();
const [books, setBooks] = useState<Book[]>([]);
const [loading, setLoading] = useState(false);
Expand All @@ -20,17 +20,19 @@ export default function BooksPage() {

const handleLogin = async () => {
try {
await graphLogin();
await auth.login();
} catch (error) {
console.error('Login failed:', error);
setError('Login failed. Please try again.');
}
};

const handleLogout = () => {
instance.logoutPopup({
postLogoutRedirectUri: '/',
});
const handleLogout = async () => {
try {
await auth.logout();
} catch (error) {
console.error('Logout failed:', error);
}
};

const fetchBooks = async () => {
Expand All @@ -40,9 +42,8 @@ export default function BooksPage() {
setError(null);

try {
const accessToken = await acquireToken();
const userInfo = await getUserInfo(accessToken);
setUserName(userInfo.displayName || userInfo.userPrincipalName || '');
const user = auth.getUser();
setUserName(user?.displayName || user?.email || '');

const allBooks = await store.listBooks();
setBooks(allBooks);
Expand Down Expand Up @@ -78,20 +79,20 @@ export default function BooksPage() {
};

useEffect(() => {
if (isAuthenticated && inProgress === 'none') {
if (isAuthenticated && !busy) {
fetchBooks();
}
}, [isAuthenticated, inProgress]);
}, [isAuthenticated, busy]);

useSetTopBarActions(
null,
!isAuthenticated ? (
<button
onClick={handleLogin}
disabled={inProgress !== 'none'}
disabled={busy}
className={`${styles.button} ${styles.buttonPrimary}`}
>
{inProgress !== 'none' ? 'Signing in...' : 'Sign In'}
{busy ? 'Signing in...' : 'Sign In'}
</button>
) : (
<>
Expand All @@ -114,7 +115,7 @@ export default function BooksPage() {
</button>
</>
),
[isAuthenticated, inProgress, loading],
[isAuthenticated, busy, loading],
);

return (
Expand Down Expand Up @@ -147,10 +148,10 @@ export default function BooksPage() {
</p>
<button
onClick={handleLogin}
disabled={inProgress !== 'none'}
disabled={busy}
className={`${styles.button} ${styles.buttonPrimary}`}
>
{inProgress !== 'none' ? 'Signing in...' : 'Sign In with Microsoft'}
{busy ? 'Signing in...' : 'Sign In with Microsoft'}
</button>
</div>
)}
Expand Down
17 changes: 9 additions & 8 deletions src/arrange-v4/app/cancelled/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import React, { useState, useEffect, useCallback, useRef, Suspense } from 'react
import { useStore } from '@/lib/store/useStore';
import type { TodoItem, TodoItemWithId } from '@/lib/store/types';
import { formatRelativeDate } from '@/lib/dateUtils';
import { useGraphToken } from '@/lib/hooks/useGraphToken';
import { useAuthClient } from '@/lib/auth/useAuthClient';
import { useBookId } from '@/lib/hooks/useBookId';
import { useSetTopBarActions } from '@/components/TopBarProvider';
import ViewTodoItem from '@/components/ViewTodoItem';
import Link from 'next/link';
import styles from './page.module.css';

function CancelledPageContent() {
const { isAuthenticated, inProgress, handleLogin: graphLogin } = useGraphToken();
const auth = useAuthClient();
const { isAuthenticated, busy } = auth;
const store = useStore();
const { bookId, books, handleBookSwitch, error: bookError } = useBookId('/cancelled');

Expand Down Expand Up @@ -50,10 +51,10 @@ function CancelledPageContent() {
}, [isAuthenticated, bookId, store]);

useEffect(() => {
if (isAuthenticated && inProgress === 'none' && bookId) {
if (isAuthenticated && !busy && bookId) {
fetchEvents();
}
}, [isAuthenticated, inProgress, bookId, fetchEvents]);
}, [isAuthenticated, busy, bookId, fetchEvents]);

const handleDeleteSelected = () => {
if (selectedIds.size === 0) return;
Expand All @@ -62,7 +63,7 @@ function CancelledPageContent() {

const handleLogin = async () => {
try {
await graphLogin();
await auth.login();
} catch (err) {
console.error('Login failed:', err);
setError('Login failed. Please try again.');
Expand All @@ -87,10 +88,10 @@ function CancelledPageContent() {
!isAuthenticated ? (
<button
onClick={handleLogin}
disabled={inProgress !== 'none'}
disabled={busy}
className={`${styles.button} ${styles.buttonPrimary}`}
>
{inProgress !== 'none' ? 'Signing in...' : 'Sign In'}
{busy ? 'Signing in...' : 'Sign In'}
</button>
) : (
<>
Expand All @@ -110,7 +111,7 @@ function CancelledPageContent() {
</button>
</>
),
[isAuthenticated, inProgress, loading, deleting, bookId, books, selectedIds.size],
[isAuthenticated, busy, loading, deleting, bookId, books, selectedIds.size],
);

const toggleSelect = (id: string) => {
Expand Down
17 changes: 9 additions & 8 deletions src/arrange-v4/app/matrix/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useStore } from '@/lib/store/useStore';
import { TodoItem, TodoItemWithId, TodoStatus, ALL_STATUSES, STATUS_LABELS } from '@/lib/store/types';
import { formatRelativeDate } from '@/lib/dateUtils';
import { hasSessionSweepRun, isSessionSweepInProgress, markSessionSweepInProgress, clearSessionSweepInProgress, markSessionSweepDone } from '@/lib/bookStorage';
import { useGraphToken } from '@/lib/hooks/useGraphToken';
import { useAuthClient } from '@/lib/auth/useAuthClient';
import { useBookId } from '@/lib/hooks/useBookId';
import { useSetTopBarActions } from '@/components/TopBarProvider';
import AddTodoItem from '@/components/AddTodoItem';
Expand Down Expand Up @@ -187,7 +187,8 @@ export default function MatrixPage() {
}

function MatrixPageContent() {
const { isAuthenticated, inProgress, handleLogin: graphLogin } = useGraphToken();
const auth = useAuthClient();
const { isAuthenticated, busy } = auth;
const store = useStore();
const { bookId, books, handleBookSwitch, error: bookError } = useBookId('/matrix');
const bookIdRef = useRef(bookId);
Expand Down Expand Up @@ -527,18 +528,18 @@ function MatrixPageContent() {

const handleLogin = async () => {
try {
await graphLogin();
await auth.login();
} catch (error) {
console.error('Login failed:', error);
setError('Login failed. Please try again.');
}
};

useEffect(() => {
if (isAuthenticated && inProgress === 'none' && bookId) {
if (isAuthenticated && !busy && bookId) {
fetchEvents();
}
}, [isAuthenticated, inProgress, bookId]);
}, [isAuthenticated, busy, bookId]);

// Push page actions into the shared top bar
useSetTopBarActions(
Expand All @@ -558,10 +559,10 @@ function MatrixPageContent() {
!isAuthenticated ? (
<button
onClick={handleLogin}
disabled={inProgress !== 'none'}
disabled={busy}
className={`${styles.button} ${styles.buttonPrimary}`}
>
{inProgress !== 'none' ? 'Signing in...' : 'Sign In'}
{busy ? 'Signing in...' : 'Sign In'}
</button>
) : (
<>
Expand All @@ -575,7 +576,7 @@ function MatrixPageContent() {
</button>
</>
),
[isAuthenticated, inProgress, loading, bookId, books, allCategories],
[isAuthenticated, busy, loading, bookId, books, allCategories],
);

if (!bookId) {
Expand Down
49 changes: 20 additions & 29 deletions src/arrange-v4/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
'use client';

import { useMsal } from '@azure/msal-react';
import { loginRequest } from '@/lib/msalConfig';
import { useRouter } from 'next/navigation';
import { MultiBackendStore } from '@/lib/store/multiStore';
import { useAuthClient } from '@/lib/auth/useAuthClient';
import { useStore } from '@/lib/store/useStore';
import { normalizeBookId } from '@/lib/store/types';
import { getLastBookId } from '@/lib/bookStorage';
import { useState, useEffect } from 'react';
import styles from './page.module.css';

export default function Home() {
const { instance, accounts, inProgress } = useMsal();
const auth = useAuthClient();
const store = useStore();
const router = useRouter();
const [matrixAvailable, setMatrixAvailable] = useState<{ show: boolean; bookId?: string }>({ show: false });

const isAuthenticated = accounts.length > 0;
const { isAuthenticated, busy } = auth;

// Background availability check: silent-only token acquisition so we never
// open an unexpected popup from a useEffect. If the silent acquisition fails
Expand All @@ -25,17 +25,12 @@ export default function Home() {
useEffect(() => {
let cancelled = false;
const check = async () => {
if (!isAuthenticated || !accounts[0]) return;
if (!isAuthenticated) return;
try {
const response = await instance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});
// silentOnly: true — never open a popup from this background check.
await auth.acquireToken({ silentOnly: true });
if (cancelled) return;
const silentStore = new MultiBackendStore({
acquireToken: async () => response.accessToken,
});
const books = await silentStore.listBooks();
const books = await store.listBooks();
if (cancelled) return;

if (books.length === 1) {
Expand All @@ -53,8 +48,8 @@ export default function Home() {
} catch (error) {
if (cancelled) return;
// Silent failure is fine for this background check — don't open a popup.
// But reset matrixAvailable so stale data from a previous account or
// a previously-successful check doesn't linger.
// Reset matrixAvailable so stale data from a previous account or a
// previously-successful check doesn't linger.
setMatrixAvailable({ show: false });
console.error('Error checking matrix availability:', error);
}
Expand All @@ -64,24 +59,20 @@ export default function Home() {
return () => {
cancelled = true;
};
}, [isAuthenticated, accounts, instance]);
}, [isAuthenticated, auth, store]);

const handleLogin = async () => {
try {
const result = await instance.loginPopup(loginRequest);

// MSAL's React state hasn't re-rendered yet — useGraphToken would still
// return a stale closure that throws "no account". Build a one-shot store
// bound to the access token we just got back from loginPopup.
const postLoginStore = new MultiBackendStore({
acquireToken: async () => result.accessToken,
});
await auth.login();

// Wrap the post-login routing decision in its own try/catch. A transient
// Graph error must not leave the user stuck on the landing page after a
// backend error must not leave the user stuck on the landing page after a
// successful login — fall back to /books in that case.
// Safe to call store methods here: AuthClient.acquireToken reads its
// underlying SDK state fresh, so the post-login token works even before
// React has re-rendered with the new auth state.
try {
const books = await postLoginStore.listBooks();
const books = await store.listBooks();

if (books.length === 1) {
router.push(`/matrix?bookId=${encodeURIComponent(books[0].id)}`);
Expand Down Expand Up @@ -130,10 +121,10 @@ export default function Home() {
{!isAuthenticated ? (
<button
onClick={handleLogin}
disabled={inProgress !== 'none'}
disabled={busy}
className={`${styles.button} ${styles.buttonPrimary}`}
>
{inProgress !== 'none' ? 'Signing in...' : 'Get Started'}
{busy ? 'Signing in...' : 'Get Started'}
</button>
) : (
<>
Expand Down
17 changes: 9 additions & 8 deletions src/arrange-v4/app/scrum/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React, { useState, useEffect, useCallback, useMemo, Suspense } from 'react';
import { useStore } from '@/lib/store/useStore';
import { TodoItem, TodoItemWithId, TodoStatus, ALL_STATUSES, STATUS_LABELS } from '@/lib/store/types';
import { useGraphToken } from '@/lib/hooks/useGraphToken';
import { useAuthClient } from '@/lib/auth/useAuthClient';
import { useBookId } from '@/lib/hooks/useBookId';
import { useSetTopBarActions } from '@/components/TopBarProvider';
import AddTodoItem from '@/components/AddTodoItem';
Expand Down Expand Up @@ -70,7 +70,8 @@ function sortByPriority(items: TodoItemWithId[]) {
}

function ScrumPageContent() {
const { isAuthenticated, inProgress, handleLogin: graphLogin } = useGraphToken();
const auth = useAuthClient();
const { isAuthenticated, busy } = auth;
const store = useStore();
const { bookId, books, handleBookSwitch, error: bookError } = useBookId('/scrum');

Expand Down Expand Up @@ -161,10 +162,10 @@ function ScrumPageContent() {
}, [isAuthenticated, bookId, store]);

useEffect(() => {
if (isAuthenticated && inProgress === 'none' && bookId) {
if (isAuthenticated && !busy && bookId) {
fetchEvents();
}
}, [isAuthenticated, inProgress, bookId, fetchEvents]);
}, [isAuthenticated, busy, bookId, fetchEvents]);

const handleAddTodo = async (todoItem: TodoItem) => {
if (!bookId) throw new Error('No book selected');
Expand All @@ -180,7 +181,7 @@ function ScrumPageContent() {

const handleLogin = async () => {
try {
await graphLogin();
await auth.login();
} catch (err) {
console.error('Login failed:', err);
setError('Login failed. Please try again.');
Expand All @@ -204,10 +205,10 @@ function ScrumPageContent() {
!isAuthenticated ? (
<button
onClick={handleLogin}
disabled={inProgress !== 'none'}
disabled={busy}
className={`${styles.button} ${styles.buttonPrimary}`}
>
{inProgress !== 'none' ? 'Signing in...' : 'Sign In'}
{busy ? 'Signing in...' : 'Sign In'}
</button>
) : (
<>
Expand All @@ -221,7 +222,7 @@ function ScrumPageContent() {
</button>
</>
),
[isAuthenticated, inProgress, loading, bookId, books, allCategories],
[isAuthenticated, busy, loading, bookId, books, allCategories],
);

const handleDragStart = (todo: TodoItemWithId) => {
Expand Down
Loading