From 70784c451c153a19709cf05635d144001a9eeb32 Mon Sep 17 00:00:00 2001 From: Max Vasin Limsukhawat Date: Tue, 10 Mar 2026 15:50:28 -0600 Subject: [PATCH 1/6] feat: add session duration management to user preferences - Introduced SessionDurationContent component for configuring session duration. - Updated B3DynamicModal to include session duration option. - Enhanced SettingsContent to navigate to session duration settings. - Implemented session duration utility functions for managing preferences. - Refactored authentication logic to utilize dynamic session duration from user preferences. --- packages/sdk/src/app.shared.ts | 13 +-- .../react/components/B3DynamicModal.tsx | 3 + .../ManageAccount/SessionDurationContent.tsx | 108 ++++++++++++++++++ .../ManageAccount/SettingsContent.tsx | 65 ++++++----- .../react/stores/useModalStore.ts | 11 ++ .../sdk/src/shared/utils/session-duration.ts | 56 +++++++++ 6 files changed, 218 insertions(+), 38 deletions(-) create mode 100644 packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx create mode 100644 packages/sdk/src/shared/utils/session-duration.ts diff --git a/packages/sdk/src/app.shared.ts b/packages/sdk/src/app.shared.ts index 712017e3..69510116 100644 --- a/packages/sdk/src/app.shared.ts +++ b/packages/sdk/src/app.shared.ts @@ -2,12 +2,11 @@ import { ClientApplication } from "@b3dotfun/b3-api"; import { AuthenticationClient } from "@feathersjs/authentication-client"; import Cookies from "js-cookie"; import { B3_AUTH_COOKIE_NAME } from "./shared/constants"; +import { getSessionDurationDays } from "./shared/utils/session-duration"; export const B3_API_URL = process.env.EXPO_PUBLIC_B3_API || process.env.NEXT_PUBLIC_B3_API || process.env.PUBLIC_B3_API || "https://api.b3.fun"; -const DEV_USER_GROUP = 4; - export const authenticate = async ( app: ClientApplication, accessToken: string, @@ -33,12 +32,10 @@ export const authenticate = async ( }, ); - // Extend cookie expiration to 30 days for dev users - if (response?.user?.userGroups?.includes(DEV_USER_GROUP)) { - const token = Cookies.get(B3_AUTH_COOKIE_NAME); - if (token) { - Cookies.set(B3_AUTH_COOKIE_NAME, token, { expires: 30 }); - } + const token = Cookies.get(B3_AUTH_COOKIE_NAME); + if (token) { + const days = getSessionDurationDays(response?.user?.preferences, params?.partnerId); + Cookies.set(B3_AUTH_COOKIE_NAME, token, days > 0 ? { expires: days } : {}); } return response; diff --git a/packages/sdk/src/global-account/react/components/B3DynamicModal.tsx b/packages/sdk/src/global-account/react/components/B3DynamicModal.tsx index 5f671f88..75ae08d1 100644 --- a/packages/sdk/src/global-account/react/components/B3DynamicModal.tsx +++ b/packages/sdk/src/global-account/react/components/B3DynamicModal.tsx @@ -27,6 +27,7 @@ import { LinkAccount } from "./LinkAccount/LinkAccount"; import { LinkNewAccount } from "./LinkAccount/LinkNewAccount"; import { ManageAccount } from "./ManageAccount/ManageAccount"; import NotificationsContent from "./ManageAccount/NotificationsContent"; +import SessionDurationContent from "./ManageAccount/SessionDurationContent"; import { RequestPermissions } from "./RequestPermissions/RequestPermissions"; import { Send } from "./Send/Send"; import { SignInWithB3Flow } from "./SignInWithB3/SignInWithB3Flow"; @@ -172,6 +173,8 @@ export function B3DynamicModal() { return ; case "notifications": return ; + case "sessionDuration": + return ; // Add other modal types here default: return null; diff --git a/packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx b/packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx new file mode 100644 index 00000000..4502c0c3 --- /dev/null +++ b/packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx @@ -0,0 +1,108 @@ +import app from "@b3dotfun/sdk/global-account/app"; +import { useAuthentication, useModalStore } from "@b3dotfun/sdk/global-account/react"; +import { + getSessionDurationDays, + SESSION_DURATION_OPTIONS, + SessionDurationDays, + setSessionDurationDays, +} from "@b3dotfun/sdk/shared/utils/session-duration"; +import { useState } from "react"; +import { Chain } from "thirdweb"; +import ModalHeader from "../ModalHeader/ModalHeader"; + +interface SessionDurationContentProps { + partnerId: string; + chain: Chain; +} + +const LABELS: Record = { + 0: "Session only", + 1: "1 day", + 7: "7 days", + 14: "14 days", + 30: "30 days", +}; + +const DESCRIPTIONS: Record = { + 0: "Sign out when browser closes", + 1: "Stay signed in for 1 day", + 7: "Stay signed in for 7 days", + 14: "Stay signed in for 2 weeks", + 30: "Stay signed in for 30 days", +}; + +const SessionDurationContent = ({ partnerId, chain }: SessionDurationContentProps) => { + const { user, setUser } = useAuthentication(partnerId); + const navigateBack = useModalStore(state => state.navigateBack); + const [sessionDays, setSessionDays] = useState(() => + getSessionDurationDays(user?.preferences, partnerId), + ); + const [saving, setSaving] = useState(false); + + const handleSelect = async (days: SessionDurationDays) => { + setSessionDurationDays(days, partnerId); + setSessionDays(days); + if (user?.userId) { + setSaving(true); + try { + const updated = await app.service("users").patch(user.userId, { + preferences: { + ...user.preferences, + [partnerId]: { ...(user.preferences as any)?.[partnerId], sessionDuration: days }, + }, + }); + setUser(updated); + } catch { + // non-critical — localStorage cache is still updated + } finally { + setSaving(false); + } + } + }; + + return ( +
+ + +
+ {SESSION_DURATION_OPTIONS.map(days => ( + + ))} +
+
+ ); +}; + +export default SessionDurationContent; diff --git a/packages/sdk/src/global-account/react/components/ManageAccount/SettingsContent.tsx b/packages/sdk/src/global-account/react/components/ManageAccount/SettingsContent.tsx index 028b1dda..0eda4c68 100644 --- a/packages/sdk/src/global-account/react/components/ManageAccount/SettingsContent.tsx +++ b/packages/sdk/src/global-account/react/components/ManageAccount/SettingsContent.tsx @@ -1,5 +1,6 @@ import { useAuthentication, useModalStore } from "@b3dotfun/sdk/global-account/react"; import { client } from "@b3dotfun/sdk/shared/utils/thirdweb"; +import { getSessionDurationDays } from "@b3dotfun/sdk/shared/utils/session-duration"; import { Loader2 } from "lucide-react"; import { useState } from "react"; import { Chain } from "thirdweb"; @@ -9,6 +10,14 @@ import ModalHeader from "../ModalHeader/ModalHeader"; import SettingsMenuItem from "./SettingsMenuItem"; import SettingsProfileCard from "./SettingsProfileCard"; +const DURATION_LABELS: Record = { + 0: "Session only", + 1: "1 day", + 7: "7 days", + 14: "14 days", + 30: "30 days", +}; + const SettingsContent = ({ partnerId, onLogout, @@ -20,46 +29,29 @@ const SettingsContent = ({ }) => { const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType); const setB3ModalOpen = useModalStore(state => state.setB3ModalOpen); - const { logout } = useAuthentication(partnerId); + const { logout, user } = useAuthentication(partnerId); const [logoutLoading, setLogoutLoading] = useState(false); - const { data: profilesRaw = [] } = useProfiles({ client }); + const sessionDays = getSessionDurationDays(user?.preferences, partnerId); + const { data: profilesRaw = [] } = useProfiles({ client }); const profiles = profilesRaw.filter((profile: any) => !["custom_auth_endpoint"].includes(profile.type)); - const handleNavigate = (type: "home" | "swap" | "linkAccount" | "avatarEditor" | "notifications") => { + const handleNavigate = ( + type: "home" | "swap" | "linkAccount" | "avatarEditor" | "notifications" | "sessionDuration", + ) => { if (type === "home") { - setB3ModalContentType({ - type: "manageAccount", - chain, - partnerId, - onLogout, - activeTab: "home", - }); + setB3ModalContentType({ type: "manageAccount", chain, partnerId, onLogout, activeTab: "home" }); } else if (type === "swap") { - setB3ModalContentType({ - type: "manageAccount", - chain, - partnerId, - onLogout, - activeTab: "tokens", - }); + setB3ModalContentType({ type: "manageAccount", chain, partnerId, onLogout, activeTab: "tokens" }); } else if (type === "linkAccount") { - setB3ModalContentType({ - type: "linkAccount", - chain, - partnerId, - }); + setB3ModalContentType({ type: "linkAccount", chain, partnerId }); } else if (type === "notifications") { - setB3ModalContentType({ - type: "notifications", - chain, - partnerId, - }); + setB3ModalContentType({ type: "notifications", chain, partnerId }); + } else if (type === "sessionDuration") { + setB3ModalContentType({ type: "sessionDuration", chain, partnerId }); } else { - setB3ModalContentType({ - type: "avatarEditor", - }); + setB3ModalContentType({ type: "avatarEditor" }); } setB3ModalOpen(true); }; @@ -111,6 +103,19 @@ const SettingsContent = ({ subtitle="Manage your notifications" onClick={() => handleNavigate("notifications")} /> + + + + } + title="Stay signed in" + subtitle={DURATION_LABELS[sessionDays] ?? `${sessionDays} days`} + onClick={() => handleNavigate("sessionDuration")} + /> {/* Logout Section */} diff --git a/packages/sdk/src/global-account/react/stores/useModalStore.ts b/packages/sdk/src/global-account/react/stores/useModalStore.ts index fd33105b..f6ecc4d6 100644 --- a/packages/sdk/src/global-account/react/stores/useModalStore.ts +++ b/packages/sdk/src/global-account/react/stores/useModalStore.ts @@ -451,6 +451,16 @@ export interface SendModalProps extends BaseModalProps { onSuccess?: (txHash?: string) => void; } +/** + * Props for the Session Duration modal + * Allows users to configure how long they stay signed in + */ +export interface SessionDurationModalProps extends BaseModalProps { + type: "sessionDuration"; + partnerId: string; + chain: Chain; +} + /** * Props for the Notifications modal * Allows users to manage notification settings and channels @@ -677,6 +687,7 @@ export type ModalContentType = | DepositModalProps | SendModalProps | NotificationsModalProps + | SessionDurationModalProps | AnySpendCollectorClubPurchaseProps | AnySpendDepositModalProps | AnySpendWorkflowTriggerModalProps diff --git a/packages/sdk/src/shared/utils/session-duration.ts b/packages/sdk/src/shared/utils/session-duration.ts new file mode 100644 index 00000000..13cb7e0f --- /dev/null +++ b/packages/sdk/src/shared/utils/session-duration.ts @@ -0,0 +1,56 @@ +const STORAGE_KEY_PREFIX = "b3-session-duration"; +const DEFAULT_DAYS = 7; + +// 0 = session cookie (expires when browser closes) +export const SESSION_DURATION_OPTIONS = [0, 1, 7, 14, 30] as const; +export type SessionDurationDays = (typeof SESSION_DURATION_OPTIONS)[number]; + +function storageKey(partnerId?: string) { + return partnerId ? `${STORAGE_KEY_PREFIX}_${partnerId}` : STORAGE_KEY_PREFIX; +} + +/** + * Read session duration for a specific partner. + * + * preferences shape: { [partnerId]: { sessionDuration: number }, sessionDuration?: number } + * + * Priority: user.preferences[partnerId].sessionDuration + * → user.preferences.sessionDuration (global fallback) + * → localStorage (per-partner) → localStorage (global) → default 7d + */ +export function getSessionDurationDays(userPreferences?: Record, partnerId?: string): SessionDurationDays { + if (userPreferences) { + if (partnerId) { + const v = userPreferences[partnerId]?.sessionDuration; + if (SESSION_DURATION_OPTIONS.includes(v as SessionDurationDays)) return v as SessionDurationDays; + } + const v = userPreferences["sessionDuration"]; + if (SESSION_DURATION_OPTIONS.includes(v as SessionDurationDays)) return v as SessionDurationDays; + } + try { + if (partnerId) { + const stored = localStorage.getItem(storageKey(partnerId)); + if (stored !== null) { + const parsed = Number(stored); + if (SESSION_DURATION_OPTIONS.includes(parsed as SessionDurationDays)) return parsed as SessionDurationDays; + } + } + const stored = localStorage.getItem(STORAGE_KEY_PREFIX); + if (stored !== null) { + const parsed = Number(stored); + if (SESSION_DURATION_OPTIONS.includes(parsed as SessionDurationDays)) return parsed as SessionDurationDays; + } + } catch { + // localStorage unavailable (e.g. SSR) + } + return DEFAULT_DAYS; +} + +/** Cache the preference locally so it's available immediately on next login */ +export function setSessionDurationDays(days: SessionDurationDays, partnerId?: string): void { + try { + localStorage.setItem(storageKey(partnerId), String(days)); + } catch { + // ignore + } +} From 01fe321cffead38c0573f8849416def741661fed Mon Sep 17 00:00:00 2001 From: Max Vasin Limsukhawat Date: Tue, 10 Mar 2026 15:55:13 -0600 Subject: [PATCH 2/6] refactor: update SessionDurationContent to use SESSION_DURATION_LABELS and remove hardcoded labels - Modified B3DynamicModal to pass partnerId to SessionDurationContent. - Updated SessionDurationContent to utilize SESSION_DURATION_LABELS for consistency. - Removed hardcoded duration labels from SettingsContent, now using SESSION_DURATION_LABELS. - Added SESSION_DURATION_LABELS to session-duration utility for better maintainability. --- .../react/components/B3DynamicModal.tsx | 2 +- .../ManageAccount/SessionDurationContent.tsx | 22 +++++++------------ .../ManageAccount/SettingsContent.tsx | 12 ++-------- .../sdk/src/shared/utils/session-duration.ts | 8 +++++++ 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/packages/sdk/src/global-account/react/components/B3DynamicModal.tsx b/packages/sdk/src/global-account/react/components/B3DynamicModal.tsx index 75ae08d1..4753b3d9 100644 --- a/packages/sdk/src/global-account/react/components/B3DynamicModal.tsx +++ b/packages/sdk/src/global-account/react/components/B3DynamicModal.tsx @@ -174,7 +174,7 @@ export function B3DynamicModal() { case "notifications": return ; case "sessionDuration": - return ; + return ; // Add other modal types here default: return null; diff --git a/packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx b/packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx index 4502c0c3..81f91e4e 100644 --- a/packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx +++ b/packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx @@ -2,27 +2,18 @@ import app from "@b3dotfun/sdk/global-account/app"; import { useAuthentication, useModalStore } from "@b3dotfun/sdk/global-account/react"; import { getSessionDurationDays, + SESSION_DURATION_LABELS, SESSION_DURATION_OPTIONS, SessionDurationDays, setSessionDurationDays, } from "@b3dotfun/sdk/shared/utils/session-duration"; import { useState } from "react"; -import { Chain } from "thirdweb"; import ModalHeader from "../ModalHeader/ModalHeader"; interface SessionDurationContentProps { partnerId: string; - chain: Chain; } -const LABELS: Record = { - 0: "Session only", - 1: "1 day", - 7: "7 days", - 14: "14 days", - 30: "30 days", -}; - const DESCRIPTIONS: Record = { 0: "Sign out when browser closes", 1: "Stay signed in for 1 day", @@ -31,7 +22,7 @@ const DESCRIPTIONS: Record = { 30: "Stay signed in for 30 days", }; -const SessionDurationContent = ({ partnerId, chain }: SessionDurationContentProps) => { +const SessionDurationContent = ({ partnerId }: SessionDurationContentProps) => { const { user, setUser } = useAuthentication(partnerId); const navigateBack = useModalStore(state => state.navigateBack); const [sessionDays, setSessionDays] = useState(() => @@ -40,6 +31,7 @@ const SessionDurationContent = ({ partnerId, chain }: SessionDurationContentProp const [saving, setSaving] = useState(false); const handleSelect = async (days: SessionDurationDays) => { + const previous = sessionDays; setSessionDurationDays(days, partnerId); setSessionDays(days); if (user?.userId) { @@ -48,12 +40,14 @@ const SessionDurationContent = ({ partnerId, chain }: SessionDurationContentProp const updated = await app.service("users").patch(user.userId, { preferences: { ...user.preferences, - [partnerId]: { ...(user.preferences as any)?.[partnerId], sessionDuration: days }, + [partnerId]: { ...(user.preferences as Record)?.[partnerId], sessionDuration: days }, }, }); setUser(updated); } catch { - // non-critical — localStorage cache is still updated + // Revert optimistic update so UI stays consistent with server state + setSessionDays(previous); + setSessionDurationDays(previous, partnerId); } finally { setSaving(false); } @@ -78,7 +72,7 @@ const SessionDurationContent = ({ partnerId, chain }: SessionDurationContentProp >
- {LABELS[days]} + {SESSION_DURATION_LABELS[days]} {DESCRIPTIONS[days]} diff --git a/packages/sdk/src/global-account/react/components/ManageAccount/SettingsContent.tsx b/packages/sdk/src/global-account/react/components/ManageAccount/SettingsContent.tsx index 0eda4c68..7e6cebbb 100644 --- a/packages/sdk/src/global-account/react/components/ManageAccount/SettingsContent.tsx +++ b/packages/sdk/src/global-account/react/components/ManageAccount/SettingsContent.tsx @@ -1,6 +1,6 @@ import { useAuthentication, useModalStore } from "@b3dotfun/sdk/global-account/react"; import { client } from "@b3dotfun/sdk/shared/utils/thirdweb"; -import { getSessionDurationDays } from "@b3dotfun/sdk/shared/utils/session-duration"; +import { getSessionDurationDays, SESSION_DURATION_LABELS } from "@b3dotfun/sdk/shared/utils/session-duration"; import { Loader2 } from "lucide-react"; import { useState } from "react"; import { Chain } from "thirdweb"; @@ -10,14 +10,6 @@ import ModalHeader from "../ModalHeader/ModalHeader"; import SettingsMenuItem from "./SettingsMenuItem"; import SettingsProfileCard from "./SettingsProfileCard"; -const DURATION_LABELS: Record = { - 0: "Session only", - 1: "1 day", - 7: "7 days", - 14: "14 days", - 30: "30 days", -}; - const SettingsContent = ({ partnerId, onLogout, @@ -113,7 +105,7 @@ const SettingsContent = ({ } title="Stay signed in" - subtitle={DURATION_LABELS[sessionDays] ?? `${sessionDays} days`} + subtitle={SESSION_DURATION_LABELS[sessionDays] ?? `${sessionDays} days`} onClick={() => handleNavigate("sessionDuration")} />
diff --git a/packages/sdk/src/shared/utils/session-duration.ts b/packages/sdk/src/shared/utils/session-duration.ts index 13cb7e0f..3b665bf6 100644 --- a/packages/sdk/src/shared/utils/session-duration.ts +++ b/packages/sdk/src/shared/utils/session-duration.ts @@ -46,6 +46,14 @@ export function getSessionDurationDays(userPreferences?: Record, pa return DEFAULT_DAYS; } +export const SESSION_DURATION_LABELS: Record = { + 0: "Session only", + 1: "1 day", + 7: "7 days", + 14: "14 days", + 30: "30 days", +}; + /** Cache the preference locally so it's available immediately on next login */ export function setSessionDurationDays(days: SessionDurationDays, partnerId?: string): void { try { From 7ee5525aa5664d0e8a3ee4629cade9773f4212b5 Mon Sep 17 00:00:00 2001 From: Max Vasin Limsukhawat Date: Tue, 10 Mar 2026 15:57:23 -0600 Subject: [PATCH 3/6] fix: add button type attribute to SessionDurationContent and SettingsContent - Added type="button" to buttons in SessionDurationContent and SettingsContent for better accessibility and to prevent unintended form submissions. - Updated handleBack prop in ModalHeader for consistency in SessionDurationContent. --- .../react/components/ManageAccount/SessionDurationContent.tsx | 3 ++- .../react/components/ManageAccount/SettingsContent.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx b/packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx index 81f91e4e..4299119f 100644 --- a/packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx +++ b/packages/sdk/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx @@ -56,11 +56,12 @@ const SessionDurationContent = ({ partnerId }: SessionDurationContentProps) => { return (
- +
{SESSION_DURATION_OPTIONS.map(days => (