diff --git a/sdks/js/packages/core/react/components/organization/profile.tsx b/sdks/js/packages/core/react/components/organization/profile.tsx
index f9e0ec72d..7342e8a4b 100644
--- a/sdks/js/packages/core/react/components/organization/profile.tsx
+++ b/sdks/js/packages/core/react/components/organization/profile.tsx
@@ -30,7 +30,8 @@ export const OrganizationProfile = ({
showAPIKeys = false,
showPreferences = false,
hideToast = false,
- customScreens = []
+ customScreens = [],
+ onLogout = () => {}
}: OrganizationProfileProps) => {
const memoryHistory = createMemoryHistory({
initialEntries: [defaultRoute]
@@ -50,7 +51,8 @@ export const OrganizationProfile = ({
showAPIKeys,
hideToast,
showPreferences,
- customRoutes
+ customRoutes,
+ onLogout
}
});
return ;
diff --git a/sdks/js/packages/core/react/components/organization/routes.tsx b/sdks/js/packages/core/react/components/organization/routes.tsx
index e32e1d4fc..dd51ae3fe 100644
--- a/sdks/js/packages/core/react/components/organization/routes.tsx
+++ b/sdks/js/packages/core/react/components/organization/routes.tsx
@@ -47,6 +47,7 @@ import ServiceUserPage from './api-keys/service-user';
import { DeleteServiceAccount } from './api-keys/delete';
import { DeleteServiceAccountKey } from './api-keys/service-user/delete';
import ManageServiceUserProjects from './api-keys/service-user/projects';
+import { SessionsPage, RevokeSessionConfirm } from './sessions';
export interface CustomScreen {
name: string;
path: string;
@@ -63,6 +64,7 @@ export interface OrganizationProfileProps {
showPreferences?: boolean;
hideToast?: boolean;
customScreens?: CustomScreen[];
+ onLogout?: () => void;
}
export interface CustomRoutes {
@@ -78,7 +80,7 @@ type RouterContext = Pick<
| 'showAPIKeys'
| 'hideToast'
| 'showPreferences'
-> & { customRoutes: CustomRoutes };
+> & { customRoutes: CustomRoutes; onLogout?: () => void };
export function getCustomRoutes(customScreens: CustomScreen[] = []) {
return (
@@ -349,6 +351,21 @@ const deleteServiceAccountKeyRoute = createRoute({
component: DeleteServiceAccountKey
});
+const sessionsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/sessions',
+ component: SessionsPage
+});
+
+const revokeSessionRoute = createRoute({
+ getParentRoute: () => sessionsRoute,
+ path: '/revoke',
+ component: RevokeSessionConfirm,
+ validateSearch: (search: Record) => ({
+ sessionId: search.sessionId as string | undefined,
+ }),
+});
+
interface getRootTreeOptions {
customScreens?: CustomScreen[];
}
@@ -357,6 +374,7 @@ export function getRootTree({ customScreens = [] }: getRootTreeOptions) {
return rootRoute.addChildren([
indexRoute.addChildren([deleteOrgRoute]),
securityRoute,
+ sessionsRoute.addChildren([revokeSessionRoute]),
membersRoute.addChildren([inviteMemberRoute, removeMemberRoute]),
teamsRoute.addChildren([addTeamRoute]),
domainsRoute.addChildren([
diff --git a/sdks/js/packages/core/react/components/organization/sessions/index.tsx b/sdks/js/packages/core/react/components/organization/sessions/index.tsx
new file mode 100644
index 000000000..b0191b81c
--- /dev/null
+++ b/sdks/js/packages/core/react/components/organization/sessions/index.tsx
@@ -0,0 +1,3 @@
+export { SessionsPage } from './sessions-page';
+export { RevokeSessionConfirm } from './revoke-session-confirm';
+export { RevokeSessionFinalConfirm } from './revoke-session-final-confirm';
\ No newline at end of file
diff --git a/sdks/js/packages/core/react/components/organization/sessions/revoke-session-confirm.tsx b/sdks/js/packages/core/react/components/organization/sessions/revoke-session-confirm.tsx
new file mode 100644
index 000000000..0760545ba
--- /dev/null
+++ b/sdks/js/packages/core/react/components/organization/sessions/revoke-session-confirm.tsx
@@ -0,0 +1,152 @@
+import { useState, useMemo } from 'react';
+import { useNavigate, useSearch, useRouteContext } from '@tanstack/react-router';
+import {
+ Button,
+ Text,
+ Dialog,
+ Flex,
+ List,
+ Skeleton
+} from '@raystack/apsara/v1';
+import { useSessions } from '../../../hooks/useSessions';
+import { useMutation } from '@connectrpc/connect-query';
+import { FrontierServiceQueries } from '@raystack/proton/frontier';
+import { RevokeSessionFinalConfirm } from './revoke-session-final-confirm';
+import styles from './sessions.module.css';
+
+export const RevokeSessionConfirm = () => {
+ const navigate = useNavigate({ from: '/sessions/revoke' });
+ const search = useSearch({ from: '/sessions/revoke' }) as { sessionId?: string };
+ const { sessions, revokeSession, isRevokingSession } = useSessions();
+ const { onLogout } = useRouteContext({ from: '__root__' }) as { onLogout?: () => void };
+ const [isFinalConfirmOpen, setIsFinalConfirmOpen] = useState(false);
+
+ const { mutate: logout } = useMutation(FrontierServiceQueries.authLogout, {
+ onSuccess: () => {
+ if (onLogout) {
+ onLogout();
+ }
+ },
+ onError: (error) => {
+ console.error('Failed to logout:', error);
+ // Fallback to regular session revocation
+ if (search.sessionId) {
+ revokeSession(search.sessionId);
+ navigate({ to: '/sessions' });
+ }
+ },
+ });
+
+ // Find the session data based on sessionId from URL params
+ const sessionData = useMemo(() => {
+ if (!search.sessionId || sessions.length === 0) {
+ return null;
+ }
+
+ const session = sessions.find(s => s.id === search.sessionId);
+ if (!session) {
+ console.error('Not found');
+ return null;
+ }
+
+ return session;
+ }, [search.sessionId, sessions]);
+
+ const handleRevokeClick = () => {
+ setIsFinalConfirmOpen(true);
+ };
+
+ const handleFinalConfirm = () => {
+ if (!search.sessionId) return;
+
+ if (sessionData?.isCurrent) {
+ logout({});
+ return;
+ }
+
+ revokeSession(search.sessionId);
+ navigate({ to: '/sessions' });
+ };
+
+
+ return (
+ <>
+ {sessionData ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ );
+};
diff --git a/sdks/js/packages/core/react/components/organization/sessions/revoke-session-final-confirm.tsx b/sdks/js/packages/core/react/components/organization/sessions/revoke-session-final-confirm.tsx
new file mode 100644
index 000000000..35fc4a43d
--- /dev/null
+++ b/sdks/js/packages/core/react/components/organization/sessions/revoke-session-final-confirm.tsx
@@ -0,0 +1,88 @@
+import {
+ Button,
+ toast,
+ Dialog,
+ Flex,
+ Text
+} from '@raystack/apsara/v1';
+import styles from './sessions.module.css';
+
+const getErrorMessage = (error: unknown): string => {
+ if (error && typeof error === 'object' && 'status' in error && error.status === 500) {
+ return 'Something went wrong';
+ }
+ return error instanceof Error ? error.message : 'Something went wrong';
+};
+
+interface RevokeSessionFinalConfirmProps {
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+ onConfirm: () => void;
+ isLoading?: boolean;
+ isCurrentSession?: boolean;
+}
+
+export const RevokeSessionFinalConfirm = ({
+ isOpen,
+ onOpenChange,
+ onConfirm,
+ isLoading = false,
+ isCurrentSession = false
+}: RevokeSessionFinalConfirmProps) => {
+ const handleConfirm = async () => {
+ try {
+ onConfirm();
+ onOpenChange(false);
+ } catch (error: any) {
+ toast.error('Failed to revoke session', {
+ description: getErrorMessage(error)
+ });
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/sdks/js/packages/core/react/components/organization/sessions/sessions-page.tsx b/sdks/js/packages/core/react/components/organization/sessions/sessions-page.tsx
new file mode 100644
index 000000000..4fe94002a
--- /dev/null
+++ b/sdks/js/packages/core/react/components/organization/sessions/sessions-page.tsx
@@ -0,0 +1,115 @@
+'use client';
+
+import {
+ Text,
+ Flex,
+ Headline,
+ Button,
+ Skeleton,
+} from '@raystack/apsara/v1';
+import { Outlet, useNavigate } from '@tanstack/react-router';
+import { useSessions } from '../../../hooks/useSessions';
+import styles from './sessions.module.css';
+
+export const SessionsPage = () => {
+ const navigate = useNavigate({ from: '/sessions' });
+ const { sessions, isLoading, error } = useSessions();
+
+
+ const handleRevoke = (sessionId: string) => {
+ navigate({ to: '/sessions/revoke', search: { sessionId } });
+ };
+
+ const renderSessionsHeader = () => (
+
+ Sessions
+
+ Devices logged into this account.
+
+
+ );
+
+ if (isLoading) {
+ return (
+
+
+
+ {renderSessionsHeader()}
+
+
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ {renderSessionsHeader()}
+
+
+
+ {error}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {renderSessionsHeader()}
+
+
+
+ {sessions.length === 0 ? (
+
+
+ No active sessions found.
+
+
+ ) : (
+ sessions.map((session) => (
+
+
+
+ {session.browser} on {session.operatingSystem}
+
+
+ {session.location}
+ •
+ {session.isCurrent ? (
+ Current session
+ ) : (
+ Last active {session.lastActive}
+ )}
+
+
+
+
+ ))
+ )}
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/sdks/js/packages/core/react/components/organization/sessions/sessions.module.css b/sdks/js/packages/core/react/components/organization/sessions/sessions.module.css
new file mode 100644
index 000000000..6899a8d27
--- /dev/null
+++ b/sdks/js/packages/core/react/components/organization/sessions/sessions.module.css
@@ -0,0 +1,49 @@
+.header {
+ margin-bottom: var(--rs-space-9);
+}
+
+.container {
+ padding: var(--rs-space-9) var(--rs-space-11);
+ max-height: 80vh;
+ overflow-y: auto;
+}
+
+.sessionsList {
+ max-height: 75vh;
+ overflow-y: auto;
+}
+
+.sessionItem {
+ padding: var(--rs-space-7) 0 var(--rs-space-7) 0;
+ border-bottom: 0.9px solid var(--rs-color-border-base-primary);
+}
+
+/* Revoke Session Confirm START */
+.revokeSessionConfirmHeader {
+ border-bottom: none;
+}
+
+.revokeSessionConfirmBody {
+ padding: 0 var(--rs-space-7) var(--rs-space-5) var(--rs-space-7);
+ border-bottom: 0.9px solid var(--rs-color-border-base-primary);
+}
+
+.revokeSessionFinalConfirmBody {
+ padding: 0 var(--rs-space-7) var(--rs-space-5) var(--rs-space-7);
+ border-bottom: 0.9px solid var(--rs-color-border-base-primary);
+}
+
+.listItem {
+ border-bottom: 0.9px solid var(--rs-color-border-base-primary);
+ padding-top: var(--rs-space-5);
+ padding-bottom: var(--rs-space-5);
+}
+
+.listItem:last-child {
+ border-bottom: none;
+}
+
+.listRoot {
+ gap: 0;
+}
+/* Revoke Session Confirm END */
\ No newline at end of file
diff --git a/sdks/js/packages/core/react/components/organization/sidebar/helpers.ts b/sdks/js/packages/core/react/components/organization/sidebar/helpers.ts
index d169ffd84..11fafdd5b 100644
--- a/sdks/js/packages/core/react/components/organization/sidebar/helpers.ts
+++ b/sdks/js/packages/core/react/components/organization/sidebar/helpers.ts
@@ -99,7 +99,12 @@ export const getUserNavItems = (options: getUserNavItemsOptions = {}) => {
name: 'Preferences',
to: '/preferences',
show: options?.showPreferences
- }
+ },
+ {
+ name: 'Sessions',
+ to: '/sessions',
+ show: true
+ },
];
const customRoutes = getCustomRoutes(options?.customRoutes);
return [...routes, ...customRoutes].filter(
diff --git a/sdks/js/packages/core/react/components/window/index.tsx b/sdks/js/packages/core/react/components/window/index.tsx
index 65a5aa4fa..5c206464d 100644
--- a/sdks/js/packages/core/react/components/window/index.tsx
+++ b/sdks/js/packages/core/react/components/window/index.tsx
@@ -24,6 +24,7 @@ export const Window = ({
const [zoom, setZoom] = useState(false);
const [isCloseActive, setCloseActive] = useState(false);
const [isZoomActive, setZoomActive] = useState(false);
+
return (