@b3dotfun/sdk 0.1.69-alpha.7 → 0.1.69-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpendStakeB3.js +1 -1
  2. package/dist/cjs/anyspend/react/components/AnySpendStakeB3ExactIn.js +1 -1
  3. package/dist/cjs/anyspend/react/components/checkout/CheckoutSuccess.d.ts +2 -1
  4. package/dist/cjs/anyspend/react/components/checkout/CheckoutSuccess.js +5 -3
  5. package/dist/cjs/app.shared.js +9 -7
  6. package/dist/cjs/global-account/react/components/B3DynamicModal.js +3 -0
  7. package/dist/cjs/global-account/react/components/ManageAccount/SessionDurationContent.d.ts +5 -0
  8. package/dist/cjs/global-account/react/components/ManageAccount/SessionDurationContent.js +57 -0
  9. package/dist/cjs/global-account/react/components/ManageAccount/SettingsContent.js +12 -29
  10. package/dist/cjs/global-account/react/hooks/useFirstEOA.d.ts +8 -8
  11. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +10 -1
  12. package/dist/cjs/shared/utils/session-duration.d.ts +15 -0
  13. package/dist/cjs/shared/utils/session-duration.js +69 -0
  14. package/dist/esm/anyspend/react/components/AnySpendStakeB3.js +2 -2
  15. package/dist/esm/anyspend/react/components/AnySpendStakeB3ExactIn.js +2 -2
  16. package/dist/esm/anyspend/react/components/checkout/CheckoutSuccess.d.ts +2 -1
  17. package/dist/esm/anyspend/react/components/checkout/CheckoutSuccess.js +5 -3
  18. package/dist/esm/app.shared.js +9 -7
  19. package/dist/esm/global-account/react/components/B3DynamicModal.js +3 -0
  20. package/dist/esm/global-account/react/components/ManageAccount/SessionDurationContent.d.ts +5 -0
  21. package/dist/esm/global-account/react/components/ManageAccount/SessionDurationContent.js +52 -0
  22. package/dist/esm/global-account/react/components/ManageAccount/SettingsContent.js +12 -29
  23. package/dist/esm/global-account/react/hooks/useFirstEOA.d.ts +8 -8
  24. package/dist/esm/global-account/react/stores/useModalStore.d.ts +10 -1
  25. package/dist/esm/shared/utils/session-duration.d.ts +15 -0
  26. package/dist/esm/shared/utils/session-duration.js +64 -0
  27. package/dist/styles/index.css +1 -1
  28. package/dist/types/anyspend/react/components/checkout/CheckoutSuccess.d.ts +2 -1
  29. package/dist/types/global-account/react/components/ManageAccount/SessionDurationContent.d.ts +5 -0
  30. package/dist/types/global-account/react/hooks/useFirstEOA.d.ts +8 -8
  31. package/dist/types/global-account/react/stores/useModalStore.d.ts +10 -1
  32. package/dist/types/shared/utils/session-duration.d.ts +15 -0
  33. package/package.json +1 -1
  34. package/src/anyspend/react/components/AnySpendStakeB3.tsx +2 -2
  35. package/src/anyspend/react/components/AnySpendStakeB3ExactIn.tsx +2 -2
  36. package/src/anyspend/react/components/checkout/CheckoutSuccess.tsx +13 -3
  37. package/src/app.shared.ts +9 -8
  38. package/src/global-account/react/components/B3DynamicModal.tsx +3 -0
  39. package/src/global-account/react/components/ManageAccount/SessionDurationContent.tsx +107 -0
  40. package/src/global-account/react/components/ManageAccount/SettingsContent.tsx +28 -30
  41. package/src/global-account/react/stores/useModalStore.ts +11 -0
  42. package/src/shared/utils/session-duration.ts +64 -0
@@ -425,6 +425,15 @@ export interface SendModalProps extends BaseModalProps {
425
425
  /** Callback function called when send is successful */
426
426
  onSuccess?: (txHash?: string) => void;
427
427
  }
428
+ /**
429
+ * Props for the Session Duration modal
430
+ * Allows users to configure how long they stay signed in
431
+ */
432
+ export interface SessionDurationModalProps extends BaseModalProps {
433
+ type: "sessionDuration";
434
+ partnerId: string;
435
+ chain: Chain;
436
+ }
428
437
  /**
429
438
  * Props for the Notifications modal
430
439
  * Allows users to manage notification settings and channels
@@ -626,7 +635,7 @@ export interface AnySpendDepositModalProps extends BaseModalProps {
626
635
  /**
627
636
  * Union type of all possible modal content types
628
637
  */
629
- export type ModalContentType = SignInWithB3ModalProps | RequestPermissionsModalProps | ManageAccountModalProps | AnySpendModalProps | AnyspendOrderDetailsProps | AnySpendNftProps | AnySpendJoinTournamentProps | AnySpendFundTournamentProps | AnySpendOrderHistoryProps | AnySpendStakeB3Props | AnySpendStakeB3ExactInProps | AnySpendStakeUpsideProps | AnySpendStakeUpsideExactInProps | AnySpendDepositUpsideProps | AnySpendBuySpinProps | AnySpendSignatureMintProps | AnySpendBondKitProps | LinkAccountModalProps | LinkNewAccountModalProps | AnySpendDepositHypeProps | AvatarEditorModalProps | DepositModalProps | SendModalProps | NotificationsModalProps | AnySpendCollectorClubPurchaseProps | AnySpendDepositModalProps | AnySpendWorkflowTriggerModalProps | AnySpendCheckoutTriggerModalProps;
638
+ export type ModalContentType = SignInWithB3ModalProps | RequestPermissionsModalProps | ManageAccountModalProps | AnySpendModalProps | AnyspendOrderDetailsProps | AnySpendNftProps | AnySpendJoinTournamentProps | AnySpendFundTournamentProps | AnySpendOrderHistoryProps | AnySpendStakeB3Props | AnySpendStakeB3ExactInProps | AnySpendStakeUpsideProps | AnySpendStakeUpsideExactInProps | AnySpendDepositUpsideProps | AnySpendBuySpinProps | AnySpendSignatureMintProps | AnySpendBondKitProps | LinkAccountModalProps | LinkNewAccountModalProps | AnySpendDepositHypeProps | AvatarEditorModalProps | DepositModalProps | SendModalProps | NotificationsModalProps | SessionDurationModalProps | AnySpendCollectorClubPurchaseProps | AnySpendDepositModalProps | AnySpendWorkflowTriggerModalProps | AnySpendCheckoutTriggerModalProps;
630
639
  /**
631
640
  * State interface for the modal store
632
641
  */
@@ -0,0 +1,15 @@
1
+ export declare const SESSION_DURATION_OPTIONS: readonly [0, 1, 7, 14, 30];
2
+ export type SessionDurationDays = (typeof SESSION_DURATION_OPTIONS)[number];
3
+ /**
4
+ * Read session duration for a specific partner.
5
+ *
6
+ * preferences shape: { [partnerId]: { sessionDuration: number }, sessionDuration?: number }
7
+ *
8
+ * Priority: user.preferences[partnerId].sessionDuration
9
+ * → user.preferences.sessionDuration (global fallback)
10
+ * → localStorage (per-partner) → localStorage (global) → default 7d
11
+ */
12
+ export declare function getSessionDurationDays(userPreferences?: Record<string, any>, partnerId?: string): SessionDurationDays;
13
+ export declare const SESSION_DURATION_LABELS: Record<SessionDurationDays, string>;
14
+ /** Cache the preference locally so it's available immediately on next login */
15
+ export declare function setSessionDurationDays(days: SessionDurationDays, partnerId?: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.69-alpha.7",
3
+ "version": "0.1.69-alpha.9",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -1,4 +1,4 @@
1
- import { ABI_ERC20_STAKING, B3_TOKEN, eqci } from "@b3dotfun/sdk/anyspend";
1
+ import { ABI_ERC20_STAKING, B3_TOKEN, eqci, getExplorerTxUrl } from "@b3dotfun/sdk/anyspend";
2
2
  import {
3
3
  Button,
4
4
  GlareCardRounded,
@@ -474,7 +474,7 @@ export function AnySpendStakeB3({
474
474
  >
475
475
  <div className="mb-6">
476
476
  <a
477
- href={`https://basescan.org/tx/${stakingTxHash}`}
477
+ href={getExplorerTxUrl(base.id, stakingTxHash)}
478
478
  target="_blank"
479
479
  rel="noopener noreferrer"
480
480
  className="text-as-primary/70 hover:text-as-primary block break-all text-center font-mono text-sm underline transition-colors"
@@ -1,4 +1,4 @@
1
- import { ABI_ERC20_STAKING, B3_TOKEN, eqci } from "@b3dotfun/sdk/anyspend";
1
+ import { ABI_ERC20_STAKING, B3_TOKEN, eqci, getExplorerTxUrl } from "@b3dotfun/sdk/anyspend";
2
2
  import { normalizeAddress } from "@b3dotfun/sdk/anyspend/utils";
3
3
  import {
4
4
  Button,
@@ -492,7 +492,7 @@ export function AnySpendStakeB3ExactIn({
492
492
  >
493
493
  <div className="mb-6">
494
494
  <a
495
- href={`https://basescan.org/tx/${stakingTxHash}`}
495
+ href={getExplorerTxUrl(base.id, stakingTxHash)}
496
496
  target="_blank"
497
497
  rel="noopener noreferrer"
498
498
  className="text-as-primary/70 hover:text-as-primary block break-all text-center font-mono text-sm underline transition-colors"
@@ -1,5 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { getExplorerTxUrl } from "@b3dotfun/sdk/anyspend";
4
+ import { b3 } from "viem/chains";
3
5
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
4
6
  import { ExternalLink } from "lucide-react";
5
7
  import { motion } from "motion/react";
@@ -9,13 +11,21 @@ import type { AnySpendCheckoutClasses } from "./AnySpendCheckout";
9
11
 
10
12
  interface CheckoutSuccessProps {
11
13
  txHash?: string;
14
+ dstChainId?: number;
12
15
  orderId?: string;
13
16
  returnUrl?: string;
14
17
  returnLabel?: string;
15
18
  classes?: AnySpendCheckoutClasses;
16
19
  }
17
20
 
18
- export function CheckoutSuccess({ txHash, orderId, returnUrl, returnLabel, classes }: CheckoutSuccessProps) {
21
+ export function CheckoutSuccess({
22
+ txHash,
23
+ dstChainId,
24
+ orderId,
25
+ returnUrl,
26
+ returnLabel,
27
+ classes,
28
+ }: CheckoutSuccessProps) {
19
29
  const { content, slots } = useAnySpendCustomization();
20
30
 
21
31
  if (slots.successScreen) {
@@ -29,7 +39,7 @@ export function CheckoutSuccess({ txHash, orderId, returnUrl, returnLabel, class
29
39
  : "Your payment has been processed successfully.",
30
40
  txHash,
31
41
  orderId,
32
- explorerUrl: txHash ? `https://explorer.b3.fun/tx/${txHash}` : undefined,
42
+ explorerUrl: txHash ? getExplorerTxUrl(dstChainId ?? b3.id, txHash) : undefined,
33
43
  onDone: () => {
34
44
  if (returnUrl) window.location.href = returnUrl;
35
45
  },
@@ -69,7 +79,7 @@ export function CheckoutSuccess({ txHash, orderId, returnUrl, returnLabel, class
69
79
  initial={{ opacity: 0 }}
70
80
  animate={{ opacity: 1 }}
71
81
  transition={{ duration: 0.3, delay: 0.5, ease: "easeOut" }}
72
- href={`https://explorer.b3.fun/tx/${txHash}`}
82
+ href={getExplorerTxUrl(dstChainId ?? b3.id, txHash)}
73
83
  target="_blank"
74
84
  rel="noopener noreferrer"
75
85
  className="anyspend-success-tx-link mt-4 flex items-center gap-1.5 text-sm text-blue-600 hover:underline dark:text-blue-400"
package/src/app.shared.ts CHANGED
@@ -2,12 +2,11 @@ import { ClientApplication } from "@b3dotfun/b3-api";
2
2
  import { AuthenticationClient } from "@feathersjs/authentication-client";
3
3
  import Cookies from "js-cookie";
4
4
  import { B3_AUTH_COOKIE_NAME } from "./shared/constants";
5
+ import { getSessionDurationDays } from "./shared/utils/session-duration";
5
6
 
6
7
  export const B3_API_URL =
7
8
  process.env.EXPO_PUBLIC_B3_API || process.env.NEXT_PUBLIC_B3_API || process.env.PUBLIC_B3_API || "https://api.b3.fun";
8
9
 
9
- const DEV_USER_GROUP = 4;
10
-
11
10
  export const authenticate = async (
12
11
  app: ClientApplication,
13
12
  accessToken: string,
@@ -33,12 +32,14 @@ export const authenticate = async (
33
32
  },
34
33
  );
35
34
 
36
- // Extend cookie expiration to 30 days for dev users
37
- if (response?.user?.userGroups?.includes(DEV_USER_GROUP)) {
38
- const token = Cookies.get(B3_AUTH_COOKIE_NAME);
39
- if (token) {
40
- Cookies.set(B3_AUTH_COOKIE_NAME, token, { expires: 30 });
41
- }
35
+ const token = Cookies.get(B3_AUTH_COOKIE_NAME);
36
+ if (token) {
37
+ const days = getSessionDurationDays(response?.user?.preferences, params?.partnerId);
38
+ Cookies.set(B3_AUTH_COOKIE_NAME, token, {
39
+ ...(days > 0 ? { expires: days } : {}),
40
+ secure: true,
41
+ sameSite: "Lax",
42
+ });
42
43
  }
43
44
 
44
45
  return response;
@@ -27,6 +27,7 @@ import { LinkAccount } from "./LinkAccount/LinkAccount";
27
27
  import { LinkNewAccount } from "./LinkAccount/LinkNewAccount";
28
28
  import { ManageAccount } from "./ManageAccount/ManageAccount";
29
29
  import NotificationsContent from "./ManageAccount/NotificationsContent";
30
+ import SessionDurationContent from "./ManageAccount/SessionDurationContent";
30
31
  import { RequestPermissions } from "./RequestPermissions/RequestPermissions";
31
32
  import { Send } from "./Send/Send";
32
33
  import { SignInWithB3Flow } from "./SignInWithB3/SignInWithB3Flow";
@@ -172,6 +173,8 @@ export function B3DynamicModal() {
172
173
  return <Send {...contentType} />;
173
174
  case "notifications":
174
175
  return <NotificationsContent {...contentType} />;
176
+ case "sessionDuration":
177
+ return <SessionDurationContent partnerId={contentType.partnerId} />;
175
178
  // Add other modal types here
176
179
  default:
177
180
  return null;
@@ -0,0 +1,107 @@
1
+ import app from "@b3dotfun/sdk/global-account/app";
2
+ import { useAuthentication, useModalStore } from "@b3dotfun/sdk/global-account/react";
3
+ import {
4
+ getSessionDurationDays,
5
+ SESSION_DURATION_LABELS,
6
+ SESSION_DURATION_OPTIONS,
7
+ SessionDurationDays,
8
+ setSessionDurationDays,
9
+ } from "@b3dotfun/sdk/shared/utils/session-duration";
10
+ import { useState } from "react";
11
+ import ModalHeader from "../ModalHeader/ModalHeader";
12
+
13
+ interface SessionDurationContentProps {
14
+ partnerId: string;
15
+ }
16
+
17
+ const DESCRIPTIONS: Record<SessionDurationDays, string> = {
18
+ 0: "Sign out when browser closes",
19
+ 1: "Stay signed in for 1 day",
20
+ 7: "Stay signed in for 7 days",
21
+ 14: "Stay signed in for 2 weeks",
22
+ 30: "Stay signed in for 30 days",
23
+ };
24
+
25
+ const SessionDurationContent = ({ partnerId }: SessionDurationContentProps) => {
26
+ const { user, setUser } = useAuthentication(partnerId);
27
+ const navigateBack = useModalStore(state => state.navigateBack);
28
+ const [sessionDays, setSessionDays] = useState<SessionDurationDays>(() =>
29
+ getSessionDurationDays(user?.preferences, partnerId),
30
+ );
31
+ const [saving, setSaving] = useState(false);
32
+
33
+ const handleSelect = async (days: SessionDurationDays) => {
34
+ const previous = sessionDays;
35
+ setSessionDurationDays(days, partnerId);
36
+ setSessionDays(days);
37
+ if (user?.userId) {
38
+ setSaving(true);
39
+ try {
40
+ const updated = await app.service("users").patch(user.userId, {
41
+ preferences: {
42
+ ...user.preferences,
43
+ [partnerId]: {
44
+ ...((((user.preferences as Record<string, unknown>) ?? {})[partnerId] as Record<string, unknown>) ?? {}),
45
+ sessionDuration: days,
46
+ },
47
+ },
48
+ });
49
+ setUser(updated);
50
+ } catch (error) {
51
+ console.error("Failed to save session duration preference:", error);
52
+ // Revert optimistic update so UI stays consistent with server state
53
+ setSessionDays(previous);
54
+ setSessionDurationDays(previous, partnerId);
55
+ } finally {
56
+ setSaving(false);
57
+ }
58
+ }
59
+ };
60
+
61
+ return (
62
+ <div className="flex h-[470px] flex-col">
63
+ <ModalHeader showBackButton={true} showCloseButton={false} title="Stay signed in" handleBack={navigateBack} />
64
+
65
+ <div className="flex flex-col gap-2 p-5">
66
+ {SESSION_DURATION_OPTIONS.map(days => (
67
+ <button
68
+ type="button"
69
+ key={days}
70
+ onClick={() => handleSelect(days)}
71
+ disabled={saving}
72
+ className={`flex items-center justify-between rounded-xl border px-4 py-3 transition-colors ${
73
+ sessionDays === days
74
+ ? "border-[#3f3f46] bg-[#f4f4f5] dark:border-white dark:bg-white/10"
75
+ : "border-[#e4e4e7] bg-transparent hover:bg-[#f4f4f5] dark:border-white/10 dark:hover:bg-white/5"
76
+ }`}
77
+ >
78
+ <div className="flex flex-col items-start gap-0.5">
79
+ <span className="font-neue-montreal-semibold text-[14px] leading-none tracking-[-0.28px] text-[#3f3f46] dark:text-white">
80
+ {SESSION_DURATION_LABELS[days]}
81
+ </span>
82
+ <span className="font-neue-montreal-medium text-[13px] leading-none tracking-[-0.26px] text-[#70707b] dark:text-white/50">
83
+ {DESCRIPTIONS[days]}
84
+ </span>
85
+ </div>
86
+ {sessionDays === days && (
87
+ <div className="flex size-5 items-center justify-center rounded-full bg-[#3f3f46] dark:bg-white">
88
+ <svg width="10" height="8" viewBox="0 0 10 8" fill="none">
89
+ <path
90
+ d="M1 4L3.5 6.5L9 1"
91
+ stroke="white"
92
+ strokeWidth="1.5"
93
+ strokeLinecap="round"
94
+ strokeLinejoin="round"
95
+ className="dark:stroke-[#3f3f46]"
96
+ />
97
+ </svg>
98
+ </div>
99
+ )}
100
+ </button>
101
+ ))}
102
+ </div>
103
+ </div>
104
+ );
105
+ };
106
+
107
+ export default SessionDurationContent;
@@ -1,5 +1,6 @@
1
1
  import { useAuthentication, useModalStore } from "@b3dotfun/sdk/global-account/react";
2
2
  import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
3
+ import { getSessionDurationDays, SESSION_DURATION_LABELS } from "@b3dotfun/sdk/shared/utils/session-duration";
3
4
  import { Loader2 } from "lucide-react";
4
5
  import { useState } from "react";
5
6
  import { Chain } from "thirdweb";
@@ -20,46 +21,29 @@ const SettingsContent = ({
20
21
  }) => {
21
22
  const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType);
22
23
  const setB3ModalOpen = useModalStore(state => state.setB3ModalOpen);
23
- const { logout } = useAuthentication(partnerId);
24
+ const { logout, user } = useAuthentication(partnerId);
24
25
  const [logoutLoading, setLogoutLoading] = useState(false);
25
26
 
26
- const { data: profilesRaw = [] } = useProfiles({ client });
27
+ const sessionDays = getSessionDurationDays(user?.preferences, partnerId);
27
28
 
29
+ const { data: profilesRaw = [] } = useProfiles({ client });
28
30
  const profiles = profilesRaw.filter((profile: any) => !["custom_auth_endpoint"].includes(profile.type));
29
31
 
30
- const handleNavigate = (type: "home" | "swap" | "linkAccount" | "avatarEditor" | "notifications") => {
32
+ const handleNavigate = (
33
+ type: "home" | "swap" | "linkAccount" | "avatarEditor" | "notifications" | "sessionDuration",
34
+ ) => {
31
35
  if (type === "home") {
32
- setB3ModalContentType({
33
- type: "manageAccount",
34
- chain,
35
- partnerId,
36
- onLogout,
37
- activeTab: "home",
38
- });
36
+ setB3ModalContentType({ type: "manageAccount", chain, partnerId, onLogout, activeTab: "home" });
39
37
  } else if (type === "swap") {
40
- setB3ModalContentType({
41
- type: "manageAccount",
42
- chain,
43
- partnerId,
44
- onLogout,
45
- activeTab: "tokens",
46
- });
38
+ setB3ModalContentType({ type: "manageAccount", chain, partnerId, onLogout, activeTab: "tokens" });
47
39
  } else if (type === "linkAccount") {
48
- setB3ModalContentType({
49
- type: "linkAccount",
50
- chain,
51
- partnerId,
52
- });
40
+ setB3ModalContentType({ type: "linkAccount", chain, partnerId });
53
41
  } else if (type === "notifications") {
54
- setB3ModalContentType({
55
- type: "notifications",
56
- chain,
57
- partnerId,
58
- });
42
+ setB3ModalContentType({ type: "notifications", chain, partnerId });
43
+ } else if (type === "sessionDuration") {
44
+ setB3ModalContentType({ type: "sessionDuration", chain, partnerId });
59
45
  } else {
60
- setB3ModalContentType({
61
- type: "avatarEditor",
62
- });
46
+ setB3ModalContentType({ type: "avatarEditor" });
63
47
  }
64
48
  setB3ModalOpen(true);
65
49
  };
@@ -111,11 +95,25 @@ const SettingsContent = ({
111
95
  subtitle="Manage your notifications"
112
96
  onClick={() => handleNavigate("notifications")}
113
97
  />
98
+ <SettingsMenuItem
99
+ icon={
100
+ <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
101
+ <path
102
+ 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"
103
+ fill="#F4F4F5"
104
+ />
105
+ </svg>
106
+ }
107
+ title="Stay signed in"
108
+ subtitle={SESSION_DURATION_LABELS[sessionDays] ?? `${sessionDays} days`}
109
+ onClick={() => handleNavigate("sessionDuration")}
110
+ />
114
111
  </div>
115
112
 
116
113
  {/* Logout Section */}
117
114
  <div className="mt-auto px-5 pb-5">
118
115
  <button
116
+ type="button"
119
117
  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"
120
118
  onClick={onLogoutEnhanced}
121
119
  disabled={logoutLoading}
@@ -451,6 +451,16 @@ export interface SendModalProps extends BaseModalProps {
451
451
  onSuccess?: (txHash?: string) => void;
452
452
  }
453
453
 
454
+ /**
455
+ * Props for the Session Duration modal
456
+ * Allows users to configure how long they stay signed in
457
+ */
458
+ export interface SessionDurationModalProps extends BaseModalProps {
459
+ type: "sessionDuration";
460
+ partnerId: string;
461
+ chain: Chain;
462
+ }
463
+
454
464
  /**
455
465
  * Props for the Notifications modal
456
466
  * Allows users to manage notification settings and channels
@@ -677,6 +687,7 @@ export type ModalContentType =
677
687
  | DepositModalProps
678
688
  | SendModalProps
679
689
  | NotificationsModalProps
690
+ | SessionDurationModalProps
680
691
  | AnySpendCollectorClubPurchaseProps
681
692
  | AnySpendDepositModalProps
682
693
  | AnySpendWorkflowTriggerModalProps
@@ -0,0 +1,64 @@
1
+ const STORAGE_KEY_PREFIX = "b3-session-duration";
2
+ const DEFAULT_DAYS = 7;
3
+
4
+ // 0 = session cookie (expires when browser closes)
5
+ export const SESSION_DURATION_OPTIONS = [0, 1, 7, 14, 30] as const;
6
+ export type SessionDurationDays = (typeof SESSION_DURATION_OPTIONS)[number];
7
+
8
+ function storageKey(partnerId?: string) {
9
+ return partnerId ? `${STORAGE_KEY_PREFIX}_${partnerId}` : STORAGE_KEY_PREFIX;
10
+ }
11
+
12
+ /**
13
+ * Read session duration for a specific partner.
14
+ *
15
+ * preferences shape: { [partnerId]: { sessionDuration: number }, sessionDuration?: number }
16
+ *
17
+ * Priority: user.preferences[partnerId].sessionDuration
18
+ * → user.preferences.sessionDuration (global fallback)
19
+ * → localStorage (per-partner) → localStorage (global) → default 7d
20
+ */
21
+ export function getSessionDurationDays(userPreferences?: Record<string, any>, partnerId?: string): SessionDurationDays {
22
+ if (userPreferences) {
23
+ if (partnerId) {
24
+ const v = userPreferences[partnerId]?.sessionDuration;
25
+ if (SESSION_DURATION_OPTIONS.includes(v as SessionDurationDays)) return v as SessionDurationDays;
26
+ }
27
+ const v = userPreferences["sessionDuration"];
28
+ if (SESSION_DURATION_OPTIONS.includes(v as SessionDurationDays)) return v as SessionDurationDays;
29
+ }
30
+ try {
31
+ if (partnerId) {
32
+ const stored = localStorage.getItem(storageKey(partnerId));
33
+ if (stored !== null) {
34
+ const parsed = Number(stored);
35
+ if (SESSION_DURATION_OPTIONS.includes(parsed as SessionDurationDays)) return parsed as SessionDurationDays;
36
+ }
37
+ }
38
+ const stored = localStorage.getItem(STORAGE_KEY_PREFIX);
39
+ if (stored !== null) {
40
+ const parsed = Number(stored);
41
+ if (SESSION_DURATION_OPTIONS.includes(parsed as SessionDurationDays)) return parsed as SessionDurationDays;
42
+ }
43
+ } catch {
44
+ // localStorage unavailable (e.g. SSR)
45
+ }
46
+ return DEFAULT_DAYS;
47
+ }
48
+
49
+ export const SESSION_DURATION_LABELS: Record<SessionDurationDays, string> = {
50
+ 0: "Session only",
51
+ 1: "1 day",
52
+ 7: "7 days",
53
+ 14: "14 days",
54
+ 30: "30 days",
55
+ };
56
+
57
+ /** Cache the preference locally so it's available immediately on next login */
58
+ export function setSessionDurationDays(days: SessionDurationDays, partnerId?: string): void {
59
+ try {
60
+ localStorage.setItem(storageKey(partnerId), String(days));
61
+ } catch {
62
+ // ignore
63
+ }
64
+ }