diff --git a/apps/remix-ide-e2e/src/helpers/init.ts b/apps/remix-ide-e2e/src/helpers/init.ts index d6f1a5d76ee..70ae8672307 100644 --- a/apps/remix-ide-e2e/src/helpers/init.ts +++ b/apps/remix-ide-e2e/src/helpers/init.ts @@ -141,10 +141,6 @@ function initModules(browser: NightwatchBrowser, callback: VoidFunction) { .click('*[data-id="github-configSwitch"]') .setValue('[data-id="settingsTabgist-access-token"]', process.env.gist_token) .click('[data-id="settingsTabSavegithub-config"]') - .waitForElementVisible('*[data-id="topbar-themeIcon-toggle"]') - .click('*[data-id="topbar-themeIcon-toggle"]') - .waitForElementVisible('*[data-id="topbar-themeIcon-light"]') - .click('*[data-id="topbar-themeIcon-light"]') // .click('[data-id="settingsTabThemeLabelFlatly"]') // e2e tests were initially developed with Flatly. Some tests are failing with the default one (Dark), because the dark theme put uppercase everywhere. .perform(() => { callback() }) } diff --git a/apps/remix-ide-e2e/src/tests/generalSettings.test.ts b/apps/remix-ide-e2e/src/tests/generalSettings.test.ts index dcd14a73135..8acfb57a365 100644 --- a/apps/remix-ide-e2e/src/tests/generalSettings.test.ts +++ b/apps/remix-ide-e2e/src/tests/generalSettings.test.ts @@ -58,48 +58,59 @@ module.exports = { .pause(100) .assert.containsText('[data-id="settingsTabgist-access-token"]', '') }, - // These e2e should be enabled after settings panel refactoring - // 'Should load dark theme ': function (browser: NightwatchBrowser) { - // browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]', 5000) - // .scrollAndClick('*[data-id="settingsTabThemeLabelDark"]') - // .pause(2000) - // .checkElementStyle(':root', '--primary', remixIdeThemes.dark.primary) - // .checkElementStyle(':root', '--secondary', remixIdeThemes.dark.secondary) - // .checkElementStyle(':root', '--success', remixIdeThemes.dark.success) - // .checkElementStyle(':root', '--info', remixIdeThemes.dark.info) - // .checkElementStyle(':root', '--warning', remixIdeThemes.dark.warning) - // .checkElementStyle(':root', '--danger', remixIdeThemes.dark.danger) - // }, - // 'Should load light theme ': function (browser: NightwatchBrowser) { - // browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]', 5000) - // .scrollAndClick('*[data-id="settingsTabThemeLabelLight"]') - // .pause(2000) - // .checkElementStyle(':root', '--primary', remixIdeThemes.light.primary) - // .checkElementStyle(':root', '--secondary', remixIdeThemes.light.secondary) - // .checkElementStyle(':root', '--success', remixIdeThemes.light.success) - // .checkElementStyle(':root', '--info', remixIdeThemes.light.info) - // .checkElementStyle(':root', '--warning', remixIdeThemes.light.warning) - // .checkElementStyle(':root', '--danger', remixIdeThemes.light.danger) - // }, + 'Should switch to Dark theme from Appearance section': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[data-id="topbar-settingsIcon"]') + .click('*[data-id="topbar-settingsIcon"]') + .waitForElementVisible('*[data-id="settings-sidebar-general"]') + .click('*[data-id="settings-sidebar-general"]') + .waitForElementVisible('*[data-id="settingsTabthemeLabel"]') + .click('*[data-id="settingsTabDropdownToggletheme"]') + .waitForElementVisible('*[data-id="settingsTabDropdownItemDark"]') + .click('*[data-id="settingsTabDropdownItemDark"]') + .pause(2000) + .checkElementStyle(':root', '--bs-primary', remixIdeThemes.dark.primary) + .checkElementStyle(':root', '--bs-secondary', remixIdeThemes.dark.secondary) + .checkElementStyle(':root', '--bs-success', remixIdeThemes.dark.success) + .checkElementStyle(':root', '--bs-info', remixIdeThemes.dark.info) + .checkElementStyle(':root', '--bs-warning', remixIdeThemes.dark.warning) + .checkElementStyle(':root', '--bs-danger', remixIdeThemes.dark.danger) + }, + + 'Should switch to Light theme from Appearance section': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[data-id="settingsTabthemeLabel"]') + .click('*[data-id="settingsTabDropdownToggletheme"]') + .waitForElementVisible('*[data-id="settingsTabDropdownItemLight"]') + .click('*[data-id="settingsTabDropdownItemLight"]') + .pause(2000) + .checkElementStyle(':root', '--bs-primary', remixIdeThemes.light.primary) + .checkElementStyle(':root', '--bs-secondary', remixIdeThemes.light.secondary) + .checkElementStyle(':root', '--bs-success', remixIdeThemes.light.success) + .checkElementStyle(':root', '--bs-info', remixIdeThemes.light.info) + .checkElementStyle(':root', '--bs-warning', remixIdeThemes.light.warning) + .checkElementStyle(':root', '--bs-danger', remixIdeThemes.light.danger) + .end() + }, } const remixIdeThemes = { dark: { primary: '#007aa6', - secondary: '#595c76', - success: '#32ba89', - info: '#086CB5', - warning: '#c97539', - danger: '#b84040' + secondary: '#444', + success: '#00bc8c', + info: '#3498db', + warning: '#f39c12', + danger: '#e74c3c' }, light: { primary: '#007aa6', - secondary: '#b3bcc483', - success: '#32ba89', - info: '#007aa6', - warning: '#c97539', - danger: '#b84040' + secondary: '#a2a3bd', + success: '#18bc9c', + info: '#3498db', + warning: '#f39c12', + danger: '#e74c3c' } } diff --git a/libs/remix-ui/login/src/lib/login-button.tsx b/libs/remix-ui/login/src/lib/login-button.tsx index 23dcd6464d6..062c1ee9cbf 100644 --- a/libs/remix-ui/login/src/lib/login-button.tsx +++ b/libs/remix-ui/login/src/lib/login-button.tsx @@ -21,6 +21,27 @@ export const LoginButton: React.FC = ({ }) => { const { isAuthenticated, user, credits, logout, login } = useAuth() const [showModal, setShowModal] = useState(false) + const [themes, setThemes] = useState>([]) + const [currentTheme, setCurrentTheme] = useState('') + + useEffect(() => { + if (plugin && typeof plugin.call === 'function') { + (async () => { + try { + const themeModule = await plugin.call('theme', 'getThemes') + if (themeModule) { + setThemes(themeModule) + } + const active = await plugin.call('theme', 'currentTheme') + if (active) { + setCurrentTheme(active.name) + } + } catch (err) { + console.log('[LoginButton] Theme module not available:', err) + } + })() + } + }, [plugin]) const handleLogout = async () => { await logout() @@ -70,6 +91,17 @@ export const LoginButton: React.FC = ({ return user.sub } + const handleThemeChange = async (themeName: string) => { + if (plugin && typeof plugin.call === 'function') { + try { + await plugin.call('theme', 'switchTheme', themeName) + setCurrentTheme(themeName) + } catch (err) { + console.error('[LoginButton] Failed to switch theme:', err) + } + } + } + if (!isAuthenticated) { return ( <> @@ -111,6 +143,9 @@ export const LoginButton: React.FC = ({ onManageAccounts={handleManageAccounts} getProviderDisplayName={getProviderDisplayName} getUserDisplayName={getUserDisplayName} + themes={themes} + currentTheme={currentTheme} + onThemeChange={handleThemeChange} /> ) } diff --git a/libs/remix-ui/login/src/lib/user-menu-compact.css b/libs/remix-ui/login/src/lib/user-menu-compact.css new file mode 100644 index 00000000000..bd0f046b651 --- /dev/null +++ b/libs/remix-ui/login/src/lib/user-menu-compact.css @@ -0,0 +1,168 @@ +.user-menu-compact-button { + gap: 8px; +} + +.user-menu-compact-avatar { + width: 25px; + height: 25px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.user-menu-compact-info { + display: flex; + flex-direction: column; + align-items: flex-start; + line-height: 1.2; +} + +.user-menu-compact-name { + font-weight: 500; +} + +.user-menu-dropdown { + position: absolute; + right: 0; + top: 100%; + min-width: 240px; + z-index: 2000; + background-color: var(--bs-secondary-bg, #333446); + border: 1px solid var(--bs-border-color, #444); + border-radius: 8px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); + padding: 0; +} + +.user-menu-dropdown-header { + background-color: var(--bs-primary, #007aa6); + color: white; + padding: 12px 16px; + border-radius: 8px 8px 0 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + gap: 12px; +} + +.user-menu-dropdown-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + border: 2px solid rgba(255, 255, 255, 0.3); + flex-shrink: 0; +} + +.user-menu-dropdown-name { + font-weight: 600; + font-size: 0.95rem; + flex: 1; +} + +.user-menu-provider { + padding: 8px 16px; + background-color: var(--bs-body-bg, #222336); + border-bottom: 1px solid var(--bs-border-color, #444); +} + +.user-menu-items-container { + padding: 4px 0; +} + +.user-menu-item { + padding: 8px 12px; + color: var(--bs-body-color, #a2a3bd); + display: flex; + align-items: center; + border: none; + background-color: transparent; + width: 100%; + text-align: left; + cursor: pointer; + transition: background-color 0.2s; +} + +.user-menu-item:hover { + background-color: var(--bs-body-bg, #222336); +} + +.user-menu-item-icon { + width: 16px; + text-align: center; + margin-right: 12px; +} + +.user-menu-credits-item { + padding: 8px 12px; + color: var(--bs-body-color, #a2a3bd); + display: flex; + align-items: center; + justify-content: space-between; + cursor: default; + background-color: transparent; + transition: background-color 0.2s; +} + +.user-menu-credits-item:hover { + background-color: var(--bs-body-bg, #222336); +} + +.user-menu-credits-icon { + width: 16px; + text-align: center; + margin-right: 12px; +} + +.user-menu-credits-balance { + font-size: 0.95rem; + font-weight: 600; +} + +.user-menu-divider { + margin: 8px 0; + border-color: var(--bs-border-color, #444); +} + +.user-menu-item-danger { + padding: 8px 12px; + color: var(--bs-danger, #e74c3c); + display: flex; + align-items: center; + border: none; + background-color: transparent; + width: 100%; + text-align: left; + cursor: pointer; + transition: background-color 0.2s; + font-weight: 500; +} + +.user-menu-item-danger:hover { + background-color: var(--bs-body-bg, #222336); +} + +.user-menu-theme-toggle { + display: flex; + align-items: center; + padding: 8px 12px; +} + +.user-menu-theme-toggle .btn { + padding: 0; + border: none; + background: transparent; +} + +.user-menu-theme-toggle .btn:hover { + background: transparent; +} + +.user-menu-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1999; +} diff --git a/libs/remix-ui/login/src/lib/user-menu-compact.tsx b/libs/remix-ui/login/src/lib/user-menu-compact.tsx index e20005df9f7..e60d32c3f49 100644 --- a/libs/remix-ui/login/src/lib/user-menu-compact.tsx +++ b/libs/remix-ui/login/src/lib/user-menu-compact.tsx @@ -1,6 +1,13 @@ import React, { useState, useEffect } from 'react' import { AuthUser, AuthProvider, LinkedAccount, AccountsResponse } from '@remix-api' import type { Credits } from '../../../app/src/lib/remix-app/context/auth-context' +import { ToggleSwitch } from '@remix-ui/toggle' +import './user-menu-compact.css' + +interface Theme { + name: string + quality: string +} interface UserMenuCompactProps { user: AuthUser @@ -13,6 +20,9 @@ interface UserMenuCompactProps { getProviderDisplayName: (provider: string) => string getUserDisplayName: () => string getLinkedAccounts?: () => Promise + themes?: Theme[] + currentTheme?: string + onThemeChange?: (themeName: string) => void } const getProviderIcon = (provider: AuthProvider | string) => { @@ -36,119 +46,166 @@ export const UserMenuCompact: React.FC = ({ onManageAccounts, getProviderDisplayName, getUserDisplayName, - getLinkedAccounts + getLinkedAccounts, + themes, + currentTheme, + onThemeChange }) => { const [showDropdown, setShowDropdown] = useState(false) - // All available providers including GitHub - const allProviders: AuthProvider[] = ['google', 'github', 'discord', 'siwe'] - return (
{showDropdown && ( <> -
-
+
+
{user.picture && ( -
- Avatar -
+ Avatar )} -
{getUserDisplayName()}
+
+ {getUserDisplayName()} +
{/* Connected Account */} {user.provider && ( -
+
{getProviderDisplayName(user.provider)}
)} - {credits && showCredits && ( - <> -
-
-
- Credits: - {credits.balance.toLocaleString()} + {/* Menu Items */} +
+ {/* Account Settings */} + {onManageAccounts && ( + + )} + + {/* Credits */} + {credits && showCredits && ( +
+
+ + Credits
+ + {credits.balance.toLocaleString()} +
- - )} -
+ )} + +
- {/* Manage Accounts */} - {onManageAccounts && ( + {/* Report a Bug */} - )} -
- + {/* Request a Feature */} + + + {/* Documentation */} + + +
+ + {/* Theme Selection */} + {themes && themes.length > 0 && onThemeChange && (() => { + // Find dark and light themes + const darkTheme = themes.find(t => t.quality.toLowerCase() === 'dark') + const lightTheme = themes.find(t => t.quality.toLowerCase() === 'light') + const isDarkMode = currentTheme && darkTheme && currentTheme.toLowerCase() === darkTheme.name.toLowerCase() + + return ( +
+ + {isDarkMode ? 'Dark Mode' : 'Light Mode'} + { + if (isDarkMode && lightTheme) { + onThemeChange(lightTheme.name) + } else if (!isDarkMode && darkTheme) { + onThemeChange(darkTheme.name) + } + }} + /> +
+ ) + })()} + +
+ + {/* Sign Out */} + +
setShowDropdown(false)} /> diff --git a/libs/remix-ui/settings/src/lib/account-manager.tsx b/libs/remix-ui/settings/src/lib/account-manager.tsx deleted file mode 100644 index a2c53b6897d..00000000000 --- a/libs/remix-ui/settings/src/lib/account-manager.tsx +++ /dev/null @@ -1,486 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { endpointUrls } from '@remix-endpoints-helper' - -interface LinkedAccount { - id: number - user_id: string - provider: string - name?: string - picture?: string - isPrimary: boolean - isLinked: boolean - has_access_token?: boolean - created_at: string - last_login_at?: string -} - -interface AccountsResponse { - primary: LinkedAccount - accounts: LinkedAccount[] -} - -interface Credits { - balance: number - free_credits: number - paid_credits: number -} - -interface Transaction { - id: number - amount: number - type: 'free_grant' | 'purchase' | 'usage' | 'refund' - reason?: string - metadata?: any - created_at: string -} - -const getProviderIcon = (provider: string) => { - switch (provider) { - case 'github': - return - case 'google': - return - case 'discord': - return - case 'siwe': - return - default: - return - } -} - -const getProviderColor = (provider: string) => { - switch (provider) { - case 'github': - return 'bg-secondary text-white' - case 'google': - return 'bg-primary text-white' - case 'discord': - return 'bg-info text-white' - case 'siwe': - return 'bg-warning text-dark' - default: - return 'bg-dark text-white' - } -} - -interface AccountManagerProps { - plugin: any -} - -export const AccountManager: React.FC = ({ plugin }) => { - const [accounts, setAccounts] = useState([]) - const [primary, setPrimary] = useState(null) - const [credits, setCredits] = useState(null) - const [transactions, setTransactions] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [showAllTransactions, setShowAllTransactions] = useState(false) - const [enableLogin, setEnableLogin] = useState(false) - - const loadAccounts = async () => { - try { - setLoading(true) - setError(null) - - // Get token from localStorage (stored by auth plugin) - const token = localStorage.getItem('remix_access_token') - const headers: Record = { - 'Content-Type': 'application/json' - } - - if (token) { - headers['Authorization'] = `Bearer ${token}` - } - - const response = await fetch(`${endpointUrls.sso}/accounts`, { - credentials: 'include', - headers - }) - - if (!response.ok) { - if (response.status === 401) { - setError('Not logged in. Please log in with Google, GitHub, Discord, or wallet to manage accounts.') - return - } - throw new Error('Failed to load accounts') - } - - const data: AccountsResponse = await response.json() - setPrimary(data.primary) - setAccounts(data.accounts) - - // Load credits - await loadCredits() - - // Load transactions - await loadTransactions() - } catch (err: any) { - console.error('Error loading accounts:', err) - setError(err.message || 'Failed to load accounts') - } finally { - setLoading(false) - } - } - - const loadCredits = async () => { - try { - // Get token from localStorage (stored by auth plugin) - const token = localStorage.getItem('remix_access_token') - const headers: Record = { - 'Content-Type': 'application/json' - } - - if (token) { - headers['Authorization'] = `Bearer ${token}` - } - - const response = await fetch(`${endpointUrls.credits}/balance`, { - credentials: 'include', - headers - }) - - if (response.status === 401) { - // User is not authenticated, clear data - setCredits(null) - return - } - - if (response.ok) { - const data = await response.json() - setCredits(data) - } - } catch (err) { - console.error('Error loading credits:', err) - // Don't set error state, just log - credits are optional - } - } - - const loadTransactions = async () => { - try { - // Get token from localStorage (stored by auth plugin) - const token = localStorage.getItem('remix_access_token') - const headers: Record = { - 'Content-Type': 'application/json' - } - - if (token) { - headers['Authorization'] = `Bearer ${token}` - } - - const response = await fetch(`${endpointUrls.credits}/transactions`, { - credentials: 'include', - headers - }) - - if (response.status === 401) { - // User is not authenticated, clear data - setTransactions([]) - return - } - - if (response.ok) { - const data = await response.json() - setTransactions(data.transactions || []) - } - } catch (err) { - console.error('Error loading transactions:', err) - // Don't set error state, just log - transactions are optional - } - } - - useEffect(() => { - // Check enableLogin flag - const checkLoginEnabled = () => { - const enabled = localStorage.getItem('enableLogin') === 'true'; - setEnableLogin(enabled); - }; - checkLoginEnabled(); - - loadAccounts() - - // Listen for auth state changes via plugin events (login/logout) - const onAuthStateChanged = async (_payload: any) => { - // Reload everything when auth state changes - await loadAccounts() - // Also recheck enableLogin flag when auth state changes - checkLoginEnabled() - } - - try { - plugin.on('auth', 'authStateChanged', onAuthStateChanged) - } catch (e) { - // noop: plugin may not be available in some contexts - } - - return () => { - try { - plugin.off('auth', 'authStateChanged') - } catch (e) { - // ignore - } - } - }, []) - - const handleLinkProvider = async (provider: string) => { - try { - // Call the auth plugin to link the account - await plugin.call('auth', 'linkAccount', provider) - - // Reload accounts after linking - await loadAccounts() - } catch (error: any) { - console.error('Failed to link account:', error) - alert(`Failed to link ${provider}: ${error.message}`) - } - } - - const handleLinkGitHub = () => { - handleLinkProvider('github') - } - - const handleLinkGoogle = () => { - handleLinkProvider('google') - } - - const handleLinkDiscord = () => { - handleLinkProvider('discord') - } - - const handleLinkSIWE = () => { - handleLinkProvider('siwe') - } - - if (!enableLogin) { - return null; - } - - if (loading) { - return ( -
-
- Loading... -
- Loading accounts... -
- ) - } - - if (error) { - return ( -
- - {error} -
- ) - } - - if (!accounts || accounts.length === 0) { - return ( -
-

No accounts found. Please log in first.

-
- ) - } - - return ( -
- {/* Credits Overview */} - {credits && ( -
-
-
- - Credits Balance -
-
-
-
-
{credits.balance.toLocaleString()}
- Total Credits -
-
-
-
-
{credits.free_credits.toLocaleString()}
- Free Credits -
-
-
-
-
{credits.paid_credits.toLocaleString()}
- Paid Credits -
-
-
-

- - Credits are shared across all your linked accounts -

-
-
- )} - - {/* Transaction History */} - {transactions && transactions.length > 0 && ( -
-
-
-
- - Recent Transactions -
- {transactions.length > 5 && ( - - )} -
-
- {(showAllTransactions ? transactions : transactions.slice(0, 5)).map((tx) => ( -
-
-
-
- 0 ? 'badge-success' : 'badge-danger'} mr-2`}> - {tx.amount > 0 ? '+' : ''}{tx.amount} - - {tx.reason || tx.type} -
-
- {new Date(tx.created_at).toLocaleString()} -
- {tx.metadata && ( -
- {typeof tx.metadata === 'string' ? tx.metadata : JSON.stringify(tx.metadata)} -
- )} -
- - {tx.type.replace('_', ' ')} - -
-
- ))} -
-
-
- )} - -
-
- - Connected Accounts -
-

- Link multiple authentication providers to access your account from anywhere. All linked accounts share the same credits and subscriptions. -

-
- -
- {accounts.map((account) => ( -
-
-
- {getProviderIcon(account.provider)} -
-
-
- {account.provider} - {account.isPrimary && ( - Primary - )} - {account.has_access_token && ( - - Token Stored - - )} -
- {account.name && ( -
{account.name}
- )} -
- Connected: {new Date(account.created_at).toLocaleDateString()} -
- {account.last_login_at && ( -
- Last login: {new Date(account.last_login_at).toLocaleString()} -
- )} -
- {account.picture && ( - {account.name - )} -
-
- ))} -
- -
-
-
- - Link Additional Accounts -
-

- Connect more authentication providers to your account. Accounts with matching emails are automatically linked. -

-
- {!accounts.some(a => a.provider === 'github') && ( - - )} - {!accounts.some(a => a.provider === 'google') && ( - - )} - {!accounts.some(a => a.provider === 'discord') && ( - - )} - {!accounts.some(a => a.provider === 'siwe') && ( - - )} -
-
-
- -
- - Automatic Linking: When you log in with a new provider using the same email, accounts are automatically linked! -
-
- ) -} diff --git a/libs/remix-ui/settings/src/lib/account-settings/account-utils.tsx b/libs/remix-ui/settings/src/lib/account-settings/account-utils.tsx new file mode 100644 index 00000000000..767673473f2 --- /dev/null +++ b/libs/remix-ui/settings/src/lib/account-settings/account-utils.tsx @@ -0,0 +1,79 @@ +import React from 'react' +import { endpointUrls } from '@remix-endpoints-helper' + +export interface LinkedAccount { + id: number + user_id: string + provider: string + name?: string + picture?: string + isPrimary: boolean + isLinked: boolean + has_access_token?: boolean + created_at: string + last_login_at?: string +} + +export interface AccountsResponse { + primary: LinkedAccount + accounts: LinkedAccount[] +} + +export const getProviderIcon = (provider: string) => { + switch (provider) { + case 'github': + return + case 'google': + return + case 'discord': + return + case 'siwe': + return + default: + return + } +} + +export const getProviderColor = (provider: string) => { + switch (provider) { + case 'github': + return 'bg-secondary text-white' + case 'google': + return 'bg-primary text-white' + case 'discord': + return 'bg-info text-white' + case 'siwe': + return 'bg-warning text-dark' + default: + return 'bg-dark text-white' + } +} + +export const loadAccountsFromAPI = async (): Promise => { + const token = localStorage.getItem('remix_access_token') + const headers: Record = { + 'Content-Type': 'application/json' + } + + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + + const response = await fetch(`${endpointUrls.sso}/accounts`, { + credentials: 'include', + headers + }) + + if (!response.ok) { + if (response.status === 401) { + throw new Error('Not logged in. Please log in with Google, GitHub, Discord, or wallet to manage accounts.') + } + throw new Error('Failed to load accounts') + } + + return await response.json() +} + +export const linkAccountProvider = async (plugin: any, provider: string): Promise => { + await plugin.call('auth', 'linkAccount', provider) +} diff --git a/libs/remix-ui/settings/src/lib/account-settings/connected-accounts.tsx b/libs/remix-ui/settings/src/lib/account-settings/connected-accounts.tsx new file mode 100644 index 00000000000..1c658749f81 --- /dev/null +++ b/libs/remix-ui/settings/src/lib/account-settings/connected-accounts.tsx @@ -0,0 +1,226 @@ +import React, { useState, useEffect } from 'react' +import { LinkedAccount, loadAccountsFromAPI, linkAccountProvider, getProviderIcon, getProviderColor } from './account-utils' + +interface ConnectedAccountsProps { + plugin: any +} + +export const ConnectedAccounts: React.FC = ({ plugin }) => { + const [accounts, setAccounts] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [enableLogin, setEnableLogin] = useState(false) + + const loadAccounts = async () => { + try { + setLoading(true) + setError(null) + + const data = await loadAccountsFromAPI() + setAccounts(data.accounts) + } catch (err: any) { + console.error('Error loading accounts:', err) + setError(err.message || 'Failed to load accounts') + } finally { + setLoading(false) + } + } + + useEffect(() => { + const checkLoginEnabled = () => { + const enabled = localStorage.getItem('enableLogin') === 'true' + setEnableLogin(enabled) + } + checkLoginEnabled() + + loadAccounts() + + const onAuthStateChanged = async (_payload: any) => { + await loadAccounts() + checkLoginEnabled() + } + + try { + plugin.on('auth', 'authStateChanged', onAuthStateChanged) + } catch (e) { + // noop + } + + return () => { + try { + plugin.off('auth', 'authStateChanged') + } catch (e) { + // ignore + } + } + }, []) + + const handleLinkProvider = async (provider: string) => { + try { + await linkAccountProvider(plugin, provider) + await loadAccounts() + } catch (error: any) { + console.error('Failed to link account:', error) + alert(`Failed to link ${provider}: ${error.message}`) + } + } + + const handleLinkGitHub = () => handleLinkProvider('github') + const handleLinkGoogle = () => handleLinkProvider('google') + const handleLinkDiscord = () => handleLinkProvider('discord') + const handleLinkSIWE = () => handleLinkProvider('siwe') + + if (!enableLogin) { + return null + } + + if (loading) { + return ( +
+
+ Loading... +
+ Loading accounts... +
+ ) + } + + if (error) { + return ( +
+ + {error} +
+ ) + } + + if (!accounts || accounts.length === 0) { + return ( +
+

No accounts found. Please log in first.

+
+ ) + } + + return ( +
+
+ {accounts.map((account) => ( +
+
+
+ {getProviderIcon(account.provider)} +
+
+
+ {account.provider} + {account.isPrimary && ( + Primary + )} + {account.has_access_token && ( + + Token Stored + + )} +
+ {account.name && ( +
{account.name}
+ )} +
+
+ {account.picture && ( + {account.name + )} +
+
+
+ + + Connected: {new Date(account.created_at).toLocaleDateString()} + + {account.last_login_at && ( + + + Last login: {new Date(account.last_login_at).toLocaleString()} + + )} +
+
+ ))} +
+ +
+
+ Link Additional Accounts +
+

+ Connect more authentication providers to your account. Accounts with matching emails are automatically linked. +

+
+ {!accounts.some(a => a.provider === 'github') && ( + + )} + {!accounts.some(a => a.provider === 'google') && ( + + )} + {!accounts.some(a => a.provider === 'discord') && ( + + )} + {!accounts.some(a => a.provider === 'siwe') && ( + + )} +
+
+ +
+
+ +
+ Automatic Linking
+ When you log in with a new provider using the same email, accounts are automatically linked! +
+
+
+
+ ) +} diff --git a/libs/remix-ui/settings/src/lib/account-settings/credits-balance.tsx b/libs/remix-ui/settings/src/lib/account-settings/credits-balance.tsx new file mode 100644 index 00000000000..597ed89a2bf --- /dev/null +++ b/libs/remix-ui/settings/src/lib/account-settings/credits-balance.tsx @@ -0,0 +1,221 @@ +import React, { useState, useEffect } from 'react' +import { endpointUrls } from '@remix-endpoints-helper' + +interface Credits { + balance: number + free_credits: number + paid_credits: number +} + +interface Transaction { + id: number + amount: number + type: 'free_grant' | 'purchase' | 'usage' | 'refund' + reason?: string + metadata?: any + created_at: string +} + +interface CreditsBalanceProps { + plugin: any +} + +export const CreditsBalance: React.FC = ({ plugin }) => { + const [credits, setCredits] = useState(null) + const [transactions, setTransactions] = useState([]) + const [loading, setLoading] = useState(true) + const [showAllTransactions, setShowAllTransactions] = useState(false) + const [enableLogin, setEnableLogin] = useState(false) + + const loadCredits = async () => { + try { + const token = localStorage.getItem('remix_access_token') + const headers: Record = { + 'Content-Type': 'application/json' + } + + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + + const response = await fetch(`${endpointUrls.credits}/balance`, { + credentials: 'include', + headers + }) + + if (response.status === 401) { + setCredits(null) + return + } + + if (response.ok) { + const data = await response.json() + setCredits(data) + } + } catch (err) { + console.error('Error loading credits:', err) + } finally { + setLoading(false) + } + } + + const loadTransactions = async () => { + try { + const token = localStorage.getItem('remix_access_token') + const headers: Record = { + 'Content-Type': 'application/json' + } + + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + + const response = await fetch(`${endpointUrls.credits}/transactions`, { + credentials: 'include', + headers + }) + + if (response.status === 401) { + setTransactions([]) + return + } + + if (response.ok) { + const data = await response.json() + setTransactions(data.transactions || []) + } + } catch (err) { + console.error('Error loading transactions:', err) + } + } + + useEffect(() => { + const checkLoginEnabled = () => { + const enabled = localStorage.getItem('enableLogin') === 'true' + setEnableLogin(enabled) + } + checkLoginEnabled() + + const loadData = async () => { + await loadCredits() + await loadTransactions() + } + + loadData() + + const onAuthStateChanged = async (_payload: any) => { + await loadData() + checkLoginEnabled() + } + + try { + plugin.on('auth', 'authStateChanged', onAuthStateChanged) + } catch (e) { + // noop + } + + return () => { + try { + plugin.off('auth', 'authStateChanged') + } catch (e) { + // ignore + } + } + }, []) + + if (!enableLogin) { + return null + } + + if (loading) { + return ( +
+
+ Loading... +
+ Loading credits... +
+ ) + } + + if (!credits) { + return null + } + + return ( +
+
+
+
+
+
{credits.balance.toLocaleString()}
+ Total Credits +
+
+
+
+
{credits.free_credits.toLocaleString()}
+ Free Credits +
+
+
+
+
{credits.paid_credits.toLocaleString()}
+ Paid Credits +
+
+
+

+ + Credits are shared across all your linked accounts +

+
+ + {transactions && transactions.length > 0 && ( +
+
+
+ + Recent Transactions +
+ {transactions.length > 5 && ( + + )} +
+
+ {(showAllTransactions ? transactions : transactions.slice(0, 5)).map((tx) => ( +
+
+
+
+ 0 ? 'badge-success' : 'badge-danger'} mr-2`}> + {tx.amount > 0 ? '+' : ''}{tx.amount} + + {tx.reason || tx.type} +
+
+ {new Date(tx.created_at).toLocaleString()} +
+ {tx.metadata && ( +
+ {typeof tx.metadata === 'string' ? tx.metadata : JSON.stringify(tx.metadata)} +
+ )} +
+ + {tx.type.replace('_', ' ')} + +
+
+ ))} +
+
+ )} +
+ ) +} diff --git a/libs/remix-ui/settings/src/lib/account-settings/index.ts b/libs/remix-ui/settings/src/lib/account-settings/index.ts new file mode 100644 index 00000000000..f5d0064fe37 --- /dev/null +++ b/libs/remix-ui/settings/src/lib/account-settings/index.ts @@ -0,0 +1,4 @@ +export { ProfileSection } from './profile-section' +export { ConnectedAccounts } from './connected-accounts' +export { CreditsBalance } from './credits-balance' +export * from './account-utils' diff --git a/libs/remix-ui/settings/src/lib/account-settings/profile-section.tsx b/libs/remix-ui/settings/src/lib/account-settings/profile-section.tsx new file mode 100644 index 00000000000..199bb7dfb37 --- /dev/null +++ b/libs/remix-ui/settings/src/lib/account-settings/profile-section.tsx @@ -0,0 +1,306 @@ +import React, { useState, useEffect } from 'react' + +interface UserProfile { + username: string + email: string + avatar_url: string + avatar_file?: File +} + +interface ProfileSectionProps { + plugin: any +} + +export const ProfileSection: React.FC = ({ plugin }) => { + const [profile, setProfile] = useState(null) + const [loading, setLoading] = useState(true) + const [editedProfile, setEditedProfile] = useState(null) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + const [avatarPreview, setAvatarPreview] = useState(null) + const [hasChanges, setHasChanges] = useState(false) + const [loginProvider, setLoginProvider] = useState(null) + const fileInputRef = React.useRef(null) + + const loadProfile = async () => { + try { + setLoading(true) + setError(null) + + // Get user data from auth plugin + try { + const user = await plugin.call('auth', 'getUser') + + if (user) { + // Store the login provider + setLoginProvider(user.provider || null) + + // Map AuthUser to UserProfile + const profileData: UserProfile = { + username: user.name || '', + email: user.email || '', + avatar_url: user.picture || '' + } + setProfile(profileData) + setEditedProfile(profileData) + } + } catch (authErr) { + console.log('Auth plugin not available or user not logged in') + } + } catch (err: any) { + console.error('Error loading profile:', err) + setError(err.message || 'Failed to load profile') + } finally { + setLoading(false) + } + } + + useEffect(() => { + loadProfile() + + const onAuthStateChanged = async (_payload: any) => { + await loadProfile() + } + + try { + plugin.on('auth', 'authStateChanged', onAuthStateChanged) + } catch (e) { + // noop + } + + return () => { + try { + plugin.off('auth', 'authStateChanged') + } catch (e) { + // ignore + } + } + }, []) + + const handleFieldChange = (field: keyof UserProfile, value: string) => { + setEditedProfile(prev => prev ? { ...prev, [field]: value } : null) + setHasChanges(true) + } + + const handleAvatarUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) { + // Validate file type + if (!file.type.startsWith('image/')) { + setError('Please select a valid image file') + return + } + + // Validate file size (max 5MB) + if (file.size > 5 * 1024 * 1024) { + setError('Image size should be less than 5MB') + return + } + + // Create preview URL + const reader = new FileReader() + reader.onloadend = () => { + setAvatarPreview(reader.result as string) + setEditedProfile(prev => prev ? { ...prev, avatar_file: file } : null) + setHasChanges(true) + setError(null) + } + reader.readAsDataURL(file) + } + } + + const handleUploadClick = () => { + fileInputRef.current?.click() + } + + const handleCancel = () => { + setEditedProfile(profile) + setAvatarPreview(null) + setError(null) + setHasChanges(false) + } + + const handleSave = async () => { + if (!editedProfile) return + + try { + setSaving(true) + setError(null) + + // TODO: Implement profile update API when backend is ready + // For now, just update local state + console.log('Saving profile:', editedProfile) + + if (editedProfile.avatar_file) { + console.log('Avatar file to upload:', editedProfile.avatar_file.name, editedProfile.avatar_file.size) + // TODO: Upload the avatar file to server and get URL back + // const formData = new FormData() + // formData.append('avatar', editedProfile.avatar_file) + // const uploadResponse = await fetch('/api/upload-avatar', { method: 'POST', body: formData }) + // const { avatarUrl } = await uploadResponse.json() + // editedProfile.avatar_url = avatarUrl + } + + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 500)) + + // If we have avatar preview, keep it as the avatar_url for now + if (avatarPreview && editedProfile.avatar_file) { + editedProfile.avatar_url = avatarPreview + } + + setProfile(editedProfile) + setAvatarPreview(null) + setHasChanges(false) + + // Show success message (optional) + console.log('Profile updated successfully (local only - API not implemented yet)') + } catch (err: any) { + console.error('Error updating profile:', err) + setError(err.message || 'Failed to update profile') + } finally { + setSaving(false) + } + } + + if (loading) { + return ( +
+
+ Loading... +
+ Loading profile... +
+ ) + } + + // Use profile data or fallback to editedProfile or empty values + const displayProfile = profile || editedProfile || { username: '', email: '', avatar_url: '' } + + // Only allow editing if logged in with email + const isEditable = loginProvider === 'email' + + return ( +
+ {error && ( +
+ + {error} +
+ )} + + {!isEditable && loginProvider && ( +
+ + Profile editing is only available for email login. You are currently logged in with {loginProvider}. +
+ )} + +
+
+
+
+ {(avatarPreview || editedProfile?.avatar_url || displayProfile.avatar_url) ? ( + Profile Avatar { + const target = e.target as HTMLImageElement + target.src = 'https://via.placeholder.com/100?text=Not+Available' + }} + /> + ) : ( +
+ Not available +
+ )} +
+
+ + +
+
+ +
+
+ + handleFieldChange('username', e.target.value)} + placeholder={isEditable ? "Enter username" : (!editedProfile?.username || editedProfile.username === '') ? "Not available" : ""} + disabled={!isEditable} + readOnly={!isEditable} + /> +
+ +
+ + handleFieldChange('email', e.target.value)} + placeholder={isEditable ? "Enter email" : (!editedProfile?.email || editedProfile.email === '') ? "Not available" : ""} + disabled={!isEditable} + readOnly={!isEditable} + /> +
+ + {hasChanges && ( +
+ + +
+ )} +
+
+
+
+ ) +} diff --git a/libs/remix-ui/settings/src/lib/remix-ui-settings.css b/libs/remix-ui/settings/src/lib/remix-ui-settings.css index 372aa6121ba..caac80fb382 100644 --- a/libs/remix-ui/settings/src/lib/remix-ui-settings.css +++ b/libs/remix-ui/settings/src/lib/remix-ui-settings.css @@ -1,3 +1,7 @@ +.remix-settings-main { + padding-right: 1rem !important; +} + @media (max-width: 1100px) { .remix-settings-sidebar { min-width: 0 !important; @@ -9,6 +13,7 @@ .remix-settings-main { padding-left: 3em !important; + padding-right: 1rem !important; } } /* For 13-inch Mac */ @@ -23,6 +28,7 @@ .remix-settings-main { padding-left: 3em !important; + padding-right: 1rem !important; } } diff --git a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx index 04b2210ad5e..8e15bd5a359 100644 --- a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx +++ b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx @@ -36,6 +36,18 @@ const settingsSections: SettingsSection[] = [ label: 'settings.generalSettings', description: 'settings.generalSettingsDescription', subSections: [ + { + title: 'Appearance', + options: [{ + name: 'theme', + label: 'settings.theme', + type: 'select', + selectOptions: settingsConfig.themes.map((theme) => ({ + label: theme.name + ' (' + theme.quality + ')', + value: theme.name + })) + }] + }, { title: 'Code editor', options: [{ @@ -70,32 +82,45 @@ const settingsSections: SettingsSection[] = [ label: 'settings.enableSaveEnvState', type: 'toggle' }] + } + ] + }, + { + key: 'account', + label: 'settings.account', + description: 'settings.accountDescription', + requiresAuth: true, // Special flag for auth-required sections + subSections: [ + { + title: 'Profile', + options: [{ + name: 'profile-section', + label: '', + type: 'custom' as const, + customComponent: 'profileSection' + }] }, { - title: 'Appearance', + title: 'Credits Balance', options: [{ - name: 'theme', - label: 'settings.theme', - type: 'select', - selectOptions: settingsConfig.themes.map((theme) => ({ - label: theme.name + ' (' + theme.quality + ')', - value: theme.name - })) + name: 'credits-balance', + label: '', + type: 'custom' as const, + customComponent: 'creditsBalance' + }] + }, + { + title: 'Connected Accounts', + description: 'Link multiple authentication providers to access your account from anywhere. All linked accounts share the same credits and subscriptions.', + options: [{ + name: 'connected-accounts', + label: '', + type: 'custom' as const, + customComponent: 'connectedAccounts' }] } ] }, - { key: 'account', label: 'settings.account', description: 'settings.accountDescription', subSections: [ - { - options: [{ - name: 'account-manager', - label: 'settings.linkedAccounts', - description: 'settings.linkedAccountsDescription', - type: 'custom' as const, - customComponent: 'accountManager' - }] - } - ]}, { key: 'analytics', label: 'settings.analytics', description: 'settings.analyticsDescription', subSections: [ { options: [{ name: 'matomo-analytics', @@ -265,7 +290,7 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => { }) }) - props.plugin.on('theme', 'themeChanged', (theme) => { + props.plugin.on('theme', 'themeChanged', (theme: any) => { setState((prevState) => { dispatch({ type: 'SET_VALUE', payload: { name: 'theme', value: theme.name } }) return { @@ -275,11 +300,11 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => { }) }) - props.plugin.on('settings', 'copilotChoiceUpdated', (isChecked) => { + props.plugin.on('settings', 'copilotChoiceUpdated', (isChecked: any) => { dispatch({ type: 'SET_VALUE', payload: { name: 'copilot/suggest/activate', value: isChecked } }) }) - props.plugin.on('settings', 'matomoPerfAnalyticsChoiceUpdated', (isChecked) => { + props.plugin.on('settings', 'matomoPerfAnalyticsChoiceUpdated', (isChecked: any) => { dispatch({ type: 'SET_VALUE', payload: { name: 'matomo-perf-analytics', value: isChecked } }) }) @@ -343,7 +368,7 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
-
+

@@ -356,7 +381,7 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => { {/* Sidebar */}
    {filteredSections.map((section, index) => ( diff --git a/libs/remix-ui/settings/src/lib/settings-section.tsx b/libs/remix-ui/settings/src/lib/settings-section.tsx index 8a8dee001a3..522e788077f 100644 --- a/libs/remix-ui/settings/src/lib/settings-section.tsx +++ b/libs/remix-ui/settings/src/lib/settings-section.tsx @@ -7,7 +7,7 @@ import { ThemeContext } from '@remix-ui/home-tab' import type { ViewPlugin } from '@remixproject/engine-web' import { CustomTooltip } from '@remix-ui/helper' import { IMCPServerManager } from './mcp-server-manager' -import { AccountManager } from './account-manager' +import { ProfileSection, CreditsBalance, ConnectedAccounts } from './account-settings' type SettingsSectionUIProps = { plugin: ViewPlugin, @@ -20,6 +20,8 @@ type ButtonOptions = SettingsSection['subSections'][0]['options'][0]['buttonOpti export const SettingsSectionUI: React.FC = ({ plugin, section, state, dispatch }) => { const [formUIData, setFormUIData] = useState<{ [key in keyof SettingsState]: Record }>({} as any) + const [isLoggedIn, setIsLoggedIn] = useState(true) // Default to true for non-auth sections + const [authLoading, setAuthLoading] = useState(false) const theme = useContext(ThemeContext) const isDark = theme.name === 'dark' const intl = useIntl() @@ -38,6 +40,43 @@ export const SettingsSectionUI: React.FC = ({ plugin, se } }, [section]) + // Check authentication for sections that require it + useEffect(() => { + if (section?.requiresAuth) { + const checkAuth = async () => { + try { + setAuthLoading(true) + const user = await plugin.call('auth', 'getUser') + setIsLoggedIn(!!user) + } catch (err) { + setIsLoggedIn(false) + } finally { + setAuthLoading(false) + } + } + + checkAuth() + + const onAuthStateChanged = async () => { + await checkAuth() + } + + try { + plugin.on('auth', 'authStateChanged', onAuthStateChanged) + } catch (e) { + // noop + } + + return () => { + try { + plugin.off('auth', 'authStateChanged') + } catch (e) { + // ignore + } + } + } + }, [section, plugin]) + const handleToggle = (name: string) => { if (state[name]) { const newValue = !state[name].value @@ -78,47 +117,89 @@ export const SettingsSectionUI: React.FC = ({ plugin, se <>

    {}

    {} - {(section.subSections || []).map((subSection, subSectionIndex) => { + + {/* Show loading state for auth-required sections */} + {section.requiresAuth && authLoading && ( +
    +
    + Loading... +
    + Loading... +
    + )} + + {/* Show warning for auth-required sections when not logged in */} + {section.requiresAuth && !authLoading && !isLoggedIn && ( +
    +
    + + Not logged in. Please log in with Google, GitHub, Discord, or wallet to manage accounts. +
    +
    + )} + + {/* Show subsections only if auth is not required OR user is logged in */} + {(!section.requiresAuth || (section.requiresAuth && isLoggedIn && !authLoading)) && (section.subSections || []).map((subSection, subSectionIndex) => { const isLastItem = subSectionIndex === section.subSections.length - 1 return ( -
    +
    {subSection.title &&
    {subSection.title}
    } + {subSection.description &&

    {subSection.description}

    }
    -
    +
    {subSection.options.map((option, optionIndex) => { const isFirstOption = optionIndex === 0 const isLastOption = optionIndex === subSection.options.length - 1 const toggleValue = state[option.name] && typeof state[option.name].value === 'boolean' ? state[option.name].value as boolean : false const selectValue = state[option.name] && typeof state[option.name].value === 'string' ? state[option.name].value as string : '' + const isAccountSection = section.key === 'account' + const paddingClass = isAccountSection + ? (isLastOption ? 'pt-0 pb-0' : isFirstOption ? 'border-bottom pb-1' : 'border-bottom py-1') + : (isLastOption ? 'pt-2 pb-0' : isFirstOption ? 'border-bottom pb-2' : 'border-bottom py-2') + return ( -
    -
    -
    - - {option.labelIconTooltip ? - }> : - option.labelIcon && - } -
    -
    - {option.type === 'toggle' && handleToggle(option.name)} disabled = {option.name === "matomo-analytics" ? true : false}/>} - {option.type === 'select' &&
    } - {option.type === 'button' && } - {option.type === 'custom' && option.customComponent === 'mcpServerManager' && } - {option.type === 'custom' && option.customComponent === 'accountManager' && } +
    + {option.label && option.label.length > 0 && ( +
    +
    + + {option.labelIconTooltip ? + }> : + option.labelIcon && + } +
    +
    + {option.type === 'toggle' && handleToggle(option.name)} disabled = {option.name === "matomo-analytics" ? true : false}/>} + {option.type === 'select' &&
    } + {option.type === 'button' && } + {option.type === 'custom' && option.customComponent === 'mcpServerManager' && } + {option.type === 'custom' && option.customComponent === 'profileSection' && } + {option.type === 'custom' && option.customComponent === 'creditsBalance' && } + {option.type === 'custom' && option.customComponent === 'connectedAccounts' && } +
    -
    - {option.description && {typeof option.description === 'string' ? : option.description}} + )} + {option.description && option.label && option.label.length > 0 && {typeof option.description === 'string' ? : option.description}} {option.type === 'custom' && option.customComponent === 'mcpServerManager' && (
    )} - {option.type === 'custom' && option.customComponent === 'accountManager' && ( + {option.type === 'custom' && option.customComponent === 'profileSection' && ( +
    + +
    + )} + {option.type === 'custom' && option.customComponent === 'creditsBalance' && ( +
    + +
    + )} + {option.type === 'custom' && option.customComponent === 'connectedAccounts' && (
    - +
    )} { diff --git a/libs/remix-ui/settings/src/lib/settingsReducer.ts b/libs/remix-ui/settings/src/lib/settingsReducer.ts index 470de3a0193..fd56343d8ea 100644 --- a/libs/remix-ui/settings/src/lib/settingsReducer.ts +++ b/libs/remix-ui/settings/src/lib/settingsReducer.ts @@ -206,6 +206,18 @@ export const initialState: SettingsState = { value: '', isLoading: false }, + 'profile-section': { + value: '', + isLoading: false + }, + 'credits-balance': { + value: '', + isLoading: false + }, + 'connected-accounts': { + value: '', + isLoading: false + }, 'ollama-config': { value: ollamaConfig, isLoading: false diff --git a/libs/remix-ui/settings/src/types/index.ts b/libs/remix-ui/settings/src/types/index.ts index eb385ea798b..525c32fbca9 100644 --- a/libs/remix-ui/settings/src/types/index.ts +++ b/libs/remix-ui/settings/src/types/index.ts @@ -41,8 +41,10 @@ export interface SettingsSection { key: string label: string description: string, + requiresAuth?: boolean, // Flag to indicate this section requires authentication subSections: { title?: string, + description?: string, options: { name: keyof SettingsState, label: string, @@ -116,6 +118,9 @@ export interface SettingsState { 'mcp/servers/enable': ConfigState, 'mcp-server-management': ConfigState, 'account-manager': ConfigState, + 'profile-section': ConfigState, + 'credits-balance': ConfigState, + 'connected-accounts': ConfigState, 'ollama-config': ConfigState, 'ollama-endpoint': ConfigState, toaster: ConfigState diff --git a/libs/remix-ui/top-bar/src/lib/remix-ui-topbar.tsx b/libs/remix-ui/top-bar/src/lib/remix-ui-topbar.tsx index 614a3e21c9b..112b9c5c43f 100644 --- a/libs/remix-ui/top-bar/src/lib/remix-ui-topbar.tsx +++ b/libs/remix-ui/top-bar/src/lib/remix-ui-topbar.tsx @@ -42,12 +42,9 @@ export function RemixUiTopbar() { const [latestReleaseNotesUrl, setLatestReleaseNotesUrl] = useState('') const [currentReleaseVersion, setCurrentReleaseVersion] = useState('') const [menuItems, setMenuItems] = useState([]) - const [showTheme, setShowTheme] = useState(false) const subMenuIconRef = useRef(null) - const themeIconRef = useRef(null) const [showSubMenuFlyOut, setShowSubMenuFlyOut] = useState(false) - useOnClickOutside([subMenuIconRef, themeIconRef], () => setShowSubMenuFlyOut(false)) - useOnClickOutside([themeIconRef], () => setShowTheme(false)) + useOnClickOutside([subMenuIconRef], () => setShowSubMenuFlyOut(false)) const workspaceRenameInput = useRef() const [leftPanelHidden, setLeftPanelHidden] = useState(false) const [bottomPanelHidden, setBottomPanelHidden] = useState(false) @@ -624,65 +621,13 @@ export function RemixUiTopbar() { plugin={plugin} variant="compact" showCredits={true} - className="ms-2" + className="ms-3" /> )} - - { - setShowTheme(!showTheme) - }} - > - { - setShowTheme(!showTheme) - }} - > - Theme - - - { - plugin.call('theme', 'switchTheme', 'Light') - }} - data-id="topbar-themeIcon-light" - > - - Light - - { - plugin.call('theme', 'switchTheme', 'Dark') - }} - data-id="topbar-themeIcon-dark" - > - - Dark - - - { const isActive = await plugin.call('manager', 'isActive', 'settings') if (!isActive) await plugin.call('manager', 'activatePlugin', 'settings')