spry-apps-dropdown 2.0.2 → 3.0.0

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.
package/README.md CHANGED
@@ -1,4 +1,57 @@
1
1
  # spry-apps-dropdown
2
+ ## Integration Guide
3
+
4
+ ### 1. OIDC/Keycloak Setup
5
+
6
+ This package requires OIDC authentication. Use `react-oidc-context` or `@react-keycloak/web` for best results.
7
+
8
+ **Example OIDC config:**
9
+
10
+ ```tsx
11
+ const oidcConfig = {
12
+ authority: 'https://auth.sprylogin.com/realms/sprylogin',
13
+ client_id: 'my-app-client',
14
+ redirect_uri: window.location.origin,
15
+ post_logout_redirect_uri: window.location.origin,
16
+ onSigninCallback: () => {
17
+ window.history.replaceState({}, document.title, window.location.pathname)
18
+ },
19
+ }
20
+ ```
21
+
22
+ ### 2. Multi-Account Management
23
+
24
+ The package supports multi-account switching, add-account, and soft logout. All account state is stored in localStorage for cross-app/iframe sync.
25
+
26
+ **Syncing across apps/iframe:**
27
+ - Use the same localStorage key (`spry_accounts`) in all apps.
28
+ - When an account is added, removed, or switched, update localStorage and reload state in all apps.
29
+ - For iframe/bridge integration, use a shared localStorage or a custom syncBridge client.
30
+
31
+ ### 3. Add Another Account Flow
32
+
33
+ When adding another account:
34
+ - A flag (`spry_add_account_pending`) is set in localStorage.
35
+ - After soft logout, the app checks this flag and triggers OIDC login for the new account.
36
+ - On callback, the new account is added and set as active.
37
+
38
+ ### 4. Cross-App Sync
39
+
40
+ To sync active account and account removal across apps:
41
+ - Listen for changes to `spry_accounts` in localStorage (using the `storage` event or polling).
42
+ - When the accounts state changes, update your app's state and UI.
43
+ - If using an iframe bridge, ensure the bridge propagates account changes to all connected apps.
44
+
45
+ ### 5. Troubleshooting
46
+
47
+ - If new accounts are not added after login, ensure your OIDC provider updates the user context before the callback runs.
48
+ - If cross-app sync is not working, check that all apps use the same localStorage key and listen for changes.
49
+ - For CORS or redirect issues, verify your OIDC config and Keycloak setup.
50
+
51
+ ### 6. Example Integration
52
+
53
+ See the Quick Start and Usage sections below for full code examples.
54
+
2
55
 
3
56
  A React component library for displaying Spry apps dropdown and profile menu with dynamic API integration. Features a Google-style UI with beautiful animations.
4
57
 
@@ -11,16 +64,53 @@ A React component library for displaying Spry apps dropdown and profile menu wit
11
64
  - 📱 Responsive layout
12
65
  - 🎯 TypeScript support
13
66
  - 🔌 Easy integration
14
- - 👤 Profile menu with avatar, account management, and sign out
67
+ - 👤 Profile menu with avatar, account management, and sign out (always shows Manage your Account)
15
68
  - 🎛️ Combined TopBar component with both apps dropdown and profile menu
16
69
 
17
70
  ## Installation
18
71
 
19
72
  ```bash
20
- npm install spry-apps-dropdown
73
+ npm install spry-apps-dropdown react-oidc-context oidc-client-ts
21
74
  ```
22
75
 
23
- That's it! All dependencies are included. Just make sure your project already has React installed (any React project does).
76
+ This package expects `react`, `react-dom`, `react-oidc-context`, and `oidc-client-ts` to be provided by the consuming app.
77
+
78
+ ## React OIDC Quick Start (Lowest Effort)
79
+
80
+ ```tsx
81
+ import React from 'react'
82
+ import ReactDOM from 'react-dom/client'
83
+ import { SpryAuthProvider, TopBar, useSpryAccountManager } from 'spry-apps-dropdown'
84
+
85
+ const oidcConfig = {
86
+ authority: 'https://auth.sprylogin.com/realms/sprylogin',
87
+ client_id: 'my-app-client',
88
+ redirect_uri: window.location.origin,
89
+ post_logout_redirect_uri: window.location.origin,
90
+ onSigninCallback: () => {
91
+ window.history.replaceState({}, document.title, window.location.pathname)
92
+ },
93
+ }
94
+
95
+ function App() {
96
+ const accountManager = useSpryAccountManager()
97
+
98
+ return (
99
+ <TopBar
100
+ apiUrl="https://your-api.com"
101
+ accountManager={accountManager}
102
+ />
103
+ )
104
+ }
105
+
106
+ ReactDOM.createRoot(document.getElementById('root')!).render(
107
+ <React.StrictMode>
108
+ <SpryAuthProvider config={oidcConfig}>
109
+ <App />
110
+ </SpryAuthProvider>
111
+ </React.StrictMode>
112
+ )
113
+ ```
24
114
 
25
115
  ## Usage
26
116
 
@@ -177,12 +267,20 @@ Profile menu component with automatic API integration.
177
267
  |------|------|----------|---------|-------------|
178
268
  | `apiUrl` | `string` | Yes | - | Base URL for profile API |
179
269
  | `onSignOut` | `() => void` | No | - | Callback when user signs out |
180
- | `onManageAccount` | `() => void` | No | - | Callback for manage account button |
270
+ | `onManageAccount` | `() => void` | No | - | Callback for Manage your Account button. If not provided, it redirects to `https://sprylogin.com/my-account/` |
181
271
  | `getAuthToken` | `() => string \| null \| Promise<string \| null>` | No | - | Function to get auth token |
182
272
  | `headers` | `Record<string, string>` | No | - | Custom headers for API requests |
183
273
  | `refetchInterval` | `number` | No | `300000` (5 min) | Auto-refetch interval in ms |
184
274
  | `cacheTime` | `number` | No | `300000` (5 min) | Cache duration in ms |
185
275
 
276
+ ### Manage Account behavior
277
+
278
+ The Manage your Account button is always shown. It uses this order:
279
+
280
+ 1. `onManageAccount` callback (if provided)
281
+ 2. `profile.manageAccountUrl` (if present)
282
+ 3. Default: `https://sprylogin.com/my-account/`
283
+
186
284
  ### `AppsDropdownConnected`
187
285
 
188
286
  The main component with automatic API integration.
@@ -0,0 +1,21 @@
1
+ /**
2
+ * AccountList Component
3
+ * Renders a dropdown list of stored accounts with ability to switch or remove
4
+ * Shows stale account badges for expired refresh tokens
5
+ */
6
+ import React from 'react';
7
+ import type { StoredAccount } from './types';
8
+ export interface AccountListProps {
9
+ accounts: StoredAccount[];
10
+ activeAccountId: string | null;
11
+ isLoading?: boolean;
12
+ onSwitchAccount: (accountId: string) => Promise<void>;
13
+ onAddAccount: () => Promise<void>;
14
+ onRemoveAccount: (accountId: string) => Promise<void>;
15
+ onRefreshAccount: (accountId: string) => Promise<void>;
16
+ }
17
+ /**
18
+ * AccountList component
19
+ * Renders all stored accounts with switching, adding, and removal UI
20
+ */
21
+ export declare const AccountList: React.ForwardRefExoticComponent<AccountListProps & React.RefAttributes<HTMLDivElement>>;
@@ -1,2 +1,5 @@
1
- import type { ProfileMenuProps } from './types';
2
- export declare function ProfileMenu({ profile, isLoading, onSignOut, onManageAccount, }: ProfileMenuProps): import("react/jsx-runtime").JSX.Element;
1
+ import type { ProfileMenuProps, UseAccountManagerReturn } from './types';
2
+ export interface ProfileMenuWithAccountsProps extends ProfileMenuProps {
3
+ accountManager?: UseAccountManagerReturn;
4
+ }
5
+ export declare function ProfileMenu({ profile, isLoading, onSignOut, onManageAccount, accountManager, }: ProfileMenuWithAccountsProps): import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,9 @@
1
- import type { UseProfileDataOptions } from './types';
1
+ import type { UseProfileDataOptions, UseAccountManagerReturn } from './types';
2
2
  interface ProfileMenuConnectedProps extends UseProfileDataOptions {
3
3
  apiUrl: string;
4
4
  onSignOut?: () => void;
5
5
  onManageAccount?: () => void;
6
+ accountManager?: UseAccountManagerReturn;
6
7
  }
7
- export declare function ProfileMenuConnected({ apiUrl, onSignOut, onManageAccount, ...options }: ProfileMenuConnectedProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function ProfileMenuConnected({ apiUrl, onSignOut, onManageAccount, accountManager, ...options }: ProfileMenuConnectedProps): import("react/jsx-runtime").JSX.Element;
8
9
  export {};
@@ -0,0 +1,15 @@
1
+ import type { AuthProviderBaseProps } from 'react-oidc-context';
2
+ import { UserManager, type UserManagerSettings } from 'oidc-client-ts';
3
+ import type { KeycloakConfig } from './types';
4
+ interface SpryAuthContextValue {
5
+ keycloakConfig: KeycloakConfig;
6
+ userManager: UserManager;
7
+ syncBridgeUrl?: string;
8
+ }
9
+ export interface SpryAuthProviderProps extends AuthProviderBaseProps {
10
+ config: UserManagerSettings;
11
+ syncBridgeUrl?: string;
12
+ }
13
+ export declare function SpryAuthProvider({ children, config, syncBridgeUrl, onSigninCallback, matchSignoutCallback, onSignoutCallback, onRemoveUser, skipSigninCallback, }: SpryAuthProviderProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function useSpryAuthContext(): SpryAuthContextValue;
15
+ export {};
package/dist/TopBar.d.ts CHANGED
@@ -6,4 +6,5 @@ import type { TopBarProps } from './types';
6
6
  export declare function TopBar({ apiUrl, profileApiUrl, onSignOut, getAuthToken, profileHeaders, showAppsDropdown, showProfileMenu, appsRefetchInterval, // 5 minutes
7
7
  appsCacheTime, // 5 minutes
8
8
  profileRefetchInterval, // 5 minutes
9
- profileCacheTime, }: TopBarProps): import("react/jsx-runtime").JSX.Element;
9
+ profileCacheTime, // 5 minutes
10
+ accountManager, }: TopBarProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Account storage utilities for multi-account management
3
+ *
4
+ * ⚠️ SECURITY WARNING: Uses localStorage for cross-app/cross-tab persistence.
5
+ *
6
+ * Tokens are stored in localStorage which persists across browser sessions and tabs.
7
+ * This enables multi-account switching across 20+ apps in the Spry ecosystem.
8
+ *
9
+ * XSS RISK: localStorage is vulnerable to XSS attacks. Consuming applications MUST:
10
+ * - Implement strict Content Security Policy (CSP) headers
11
+ * - Avoid eval(), inline scripts, and dangerous DOM manipulation
12
+ * - Sanitize all user inputs
13
+ * - Use trusted dependencies only
14
+ * - Regularly audit for XSS vulnerabilities
15
+ *
16
+ * See INTEGRATION.md for complete security recommendations.
17
+ */
18
+ import type { AccountsState, StoredAccount, OIDCUser, StoredAccountMetadata } from './types';
19
+ /**
20
+ * Retrieve accounts state from localStorage
21
+ */
22
+ export declare function getAccountsState(): AccountsState;
23
+ /**
24
+ * Retrieve all stored accounts from localStorage
25
+ */
26
+ export declare function getAccounts(): StoredAccount[];
27
+ /**
28
+ * Persist accounts state to localStorage
29
+ */
30
+ export declare function saveAccountsState(state: AccountsState): void;
31
+ /**
32
+ * Persist accounts to localStorage
33
+ */
34
+ export declare function saveAccounts(accounts: StoredAccount[], activeAccountId?: string | null): void;
35
+ /**
36
+ * Add a new account to storage
37
+ * Returns the generated account ID
38
+ */
39
+ export declare function addAccount(user: OIDCUser): string;
40
+ /**
41
+ * Remove an account from storage
42
+ */
43
+ export declare function removeAccount(accountId: string): void;
44
+ /**
45
+ * Set an account as the active account
46
+ */
47
+ export declare function setActiveAccount(accountId: string | null): void;
48
+ /**
49
+ * Get the currently active account
50
+ */
51
+ export declare function getActiveAccount(): StoredAccount | null;
52
+ /**
53
+ * Get the active account ID
54
+ */
55
+ export declare function getActiveAccountId(): string | null;
56
+ /**
57
+ * Update tokens for a specific account
58
+ */
59
+ export declare function updateAccountTokens(accountId: string, user: OIDCUser): void;
60
+ /**
61
+ * Mark an account as stale (refresh token expired)
62
+ */
63
+ export declare function markAccountStale(accountId: string): void;
64
+ /**
65
+ * Clear ALL stored accounts (used on logout or manual reset)
66
+ */
67
+ export declare function clearAllAccounts(): void;
68
+ /**
69
+ * Get account metadata without the user object
70
+ * Useful for debugging or display
71
+ */
72
+ export declare function getAccountMetadata(accountId: string): StoredAccountMetadata | null;
73
+ /**
74
+ * Set flag indicating add account flow is in progress
75
+ */
76
+ export declare function setAddAccountInProgress(inProgress: boolean): void;
77
+ /**
78
+ * Check if add account flow is in progress
79
+ */
80
+ export declare function isAddAccountInProgress(): boolean;
81
+ /**
82
+ * Save return URL for redirect flow
83
+ */
84
+ export declare function setReturnUrl(url: string): void;
85
+ /**
86
+ * Get return URL from redirect flow
87
+ */
88
+ export declare function getReturnUrl(): string | null;
89
+ /**
90
+ * Clear return URL
91
+ */
92
+ export declare function clearReturnUrl(): void;
93
+ /**
94
+ * Save current user temporarily before redirect
95
+ */
96
+ export declare function saveTempCurrentUser(user: OIDCUser): void;
97
+ /**
98
+ * Get temporarily saved current user
99
+ */
100
+ export declare function getTempCurrentUser(): OIDCUser | null;
101
+ /**
102
+ * Clear temporarily saved current user
103
+ */
104
+ export declare function clearTempCurrentUser(): void;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Storage keys for multi-account management
3
+ *
4
+ * ⚠️ SECURITY WARNING: Using localStorage for cross-app/cross-tab persistence.
5
+ * Tokens are stored in localStorage which is vulnerable to XSS attacks.
6
+ * Consuming applications MUST implement Content Security Policy (CSP) headers
7
+ * and other XSS mitigations. See INTEGRATION.md for security recommendations.
8
+ */
9
+ export declare const STORAGE_KEYS: {
10
+ readonly ACCOUNTS: "spry_accounts";
11
+ readonly BRIDGE_ACCOUNTS: "spry_accounts_shared_state";
12
+ readonly ADD_ACCOUNT_IN_PROGRESS: "spry_add_account_in_progress";
13
+ readonly RETURN_URL: "spry_return_url";
14
+ readonly TEMP_CURRENT_USER: "spry_temp_current_user";
15
+ };
16
+ /**
17
+ * Token validation and refresh configuration
18
+ */
19
+ export declare const TOKEN_VALIDATION: {
20
+ readonly ACCESS_TOKEN_BUFFER_SECONDS: 60;
21
+ readonly REFRESH_TOKEN_BUFFER_SECONDS: 300;
22
+ readonly STALE_CHECK_INTERVAL_MS: 1000;
23
+ };
24
+ /**
25
+ * Error messages
26
+ */
27
+ export declare const ERROR_MESSAGES: {
28
+ readonly INVALID_TOKEN: "Invalid or malformed token";
29
+ readonly TOKEN_DECODE_FAILED: "Failed to decode token";
30
+ readonly ACCOUNT_NOT_FOUND: "Account not found";
31
+ readonly SIGNIN_REDIRECT_FAILED: "Failed to initiate sign-in redirect";
32
+ readonly LOGOUT_FAILED: "Failed to logout from Keycloak, but account will be removed locally.";
33
+ readonly TOKEN_REFRESH_FAILED: "Failed to refresh token for account";
34
+ readonly SSO_CLEAR_FAILED: "Failed to clear SSO session. Please sign out first if prompted by Keycloak.";
35
+ };
36
+ /**
37
+ * Warning/Info messages
38
+ */
39
+ export declare const INFO_MESSAGES: {
40
+ readonly LOCALSTORAGE_XSS_WARNING: "⚠️ Tokens stored in localStorage are vulnerable to XSS. Ensure CSP headers are configured.";
41
+ readonly ACCOUNT_MARKED_STALE: "Account refresh token expired. Click to re-authenticate.";
42
+ readonly NO_ACCOUNTS: "No additional accounts stored. Your primary auth remains active.";
43
+ readonly REDIRECT_IN_PROGRESS: "Redirecting to Keycloak for authentication...";
44
+ };