Skip to content
Merged
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
17 changes: 9 additions & 8 deletions packages/sdk/src/app.shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,12 +32,14 @@ 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 } : {}),
secure: true,
sameSite: "Lax",
});
}

return response;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -172,6 +173,8 @@ export function B3DynamicModal() {
return <Send {...contentType} />;
case "notifications":
return <NotificationsContent {...contentType} />;
case "sessionDuration":
return <SessionDurationContent partnerId={contentType.partnerId} />;
// Add other modal types here
default:
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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 ModalHeader from "../ModalHeader/ModalHeader";

interface SessionDurationContentProps {
partnerId: string;
}

const DESCRIPTIONS: Record<SessionDurationDays, string> = {
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 }: SessionDurationContentProps) => {
const { user, setUser } = useAuthentication(partnerId);
const navigateBack = useModalStore(state => state.navigateBack);
const [sessionDays, setSessionDays] = useState<SessionDurationDays>(() =>
getSessionDurationDays(user?.preferences, partnerId),
);
const [saving, setSaving] = useState(false);

const handleSelect = async (days: SessionDurationDays) => {
const previous = sessionDays;
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 Record<string, unknown>) ?? {})[partnerId] as Record<string, unknown>) ?? {}),
sessionDuration: days,
},
},
});
setUser(updated);
} catch (error) {
console.error("Failed to save session duration preference:", error);
// Revert optimistic update so UI stays consistent with server state
setSessionDays(previous);
setSessionDurationDays(previous, partnerId);
} finally {
setSaving(false);
}
}
};

return (
<div className="flex h-[470px] flex-col">
<ModalHeader showBackButton={true} showCloseButton={false} title="Stay signed in" handleBack={navigateBack} />

<div className="flex flex-col gap-2 p-5">
{SESSION_DURATION_OPTIONS.map(days => (
<button
type="button"
key={days}
onClick={() => handleSelect(days)}
disabled={saving}
className={`flex items-center justify-between rounded-xl border px-4 py-3 transition-colors ${
sessionDays === days
? "border-[#3f3f46] bg-[#f4f4f5] dark:border-white dark:bg-white/10"
: "border-[#e4e4e7] bg-transparent hover:bg-[#f4f4f5] dark:border-white/10 dark:hover:bg-white/5"
}`}
>
<div className="flex flex-col items-start gap-0.5">
<span className="font-neue-montreal-semibold text-[14px] leading-none tracking-[-0.28px] text-[#3f3f46] dark:text-white">
{SESSION_DURATION_LABELS[days]}
</span>
<span className="font-neue-montreal-medium text-[13px] leading-none tracking-[-0.26px] text-[#70707b] dark:text-white/50">
{DESCRIPTIONS[days]}
</span>
</div>
{sessionDays === days && (
<div className="flex size-5 items-center justify-center rounded-full bg-[#3f3f46] dark:bg-white">
<svg width="10" height="8" viewBox="0 0 10 8" fill="none">
<path
d="M1 4L3.5 6.5L9 1"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="dark:stroke-[#3f3f46]"
/>
</svg>
</div>
)}
</button>
))}
</div>
</div>
);
};

export default SessionDurationContent;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useAuthentication, useModalStore } from "@b3dotfun/sdk/global-account/react";
import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
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";
Expand All @@ -20,46 +21,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);
};
Expand Down Expand Up @@ -111,11 +95,25 @@ const SettingsContent = ({
subtitle="Manage your notifications"
onClick={() => handleNavigate("notifications")}
/>
<SettingsMenuItem
icon={
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 12C0 5.37258 5.37258 0 12 0H28C34.6274 0 40 5.37258 40 12V28C40 34.6274 34.6274 40 28 40H12C5.37258 40 0 34.6274 0 28V12Z"
fill="#F4F4F5"
/>
</svg>
}
title="Stay signed in"
subtitle={SESSION_DURATION_LABELS[sessionDays] ?? `${sessionDays} days`}
onClick={() => handleNavigate("sessionDuration")}
/>
</div>

{/* Logout Section */}
<div className="mt-auto px-5 pb-5">
<button
type="button"
className="b3-modal-sign-out-button border-b3-line hover:bg-b3-line bg-b3-background dark:bg-b3-background dark:border-b3-line dark:hover:bg-b3-line/80 flex w-full items-center justify-center gap-1.5 rounded-xl border border-solid p-3 transition-colors"
onClick={onLogoutEnhanced}
disabled={logoutLoading}
Expand Down
11 changes: 11 additions & 0 deletions packages/sdk/src/global-account/react/stores/useModalStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -677,6 +687,7 @@ export type ModalContentType =
| DepositModalProps
| SendModalProps
| NotificationsModalProps
| SessionDurationModalProps
| AnySpendCollectorClubPurchaseProps
| AnySpendDepositModalProps
| AnySpendWorkflowTriggerModalProps
Expand Down
64 changes: 64 additions & 0 deletions packages/sdk/src/shared/utils/session-duration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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<string, any>, 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;
}

export const SESSION_DURATION_LABELS: Record<SessionDurationDays, string> = {
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 {
localStorage.setItem(storageKey(partnerId), String(days));
} catch {
// ignore
}
}
Loading