Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: modal-theme-update-on-state-change #1676

Closed
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
7 changes: 3 additions & 4 deletions builder/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import WormholeBridge, {
TESTNET_CHAINS,
TestnetChainName,
Theme,
WormholeConnectConfig,
defaultTheme,
} from "@wormhole-foundation/wormhole-connect";
import { useCallback, useEffect, useMemo, useState } from "react";
Expand Down Expand Up @@ -458,9 +457,8 @@ function App() {
},
[]
);
// NOTE: the WormholeBridge component is keyed by the stringified version of config
// because otherwise the component did not update on changes
const config: WormholeConnectConfig = useMemo(

const config = useMemo(
() => ({
env: "testnet", // always testnet for the builder
rpcs: testnetRpcs,
Expand Down Expand Up @@ -508,6 +506,7 @@ function App() {
showHamburgerMenu,
]
);

const [versionOrTag, setVersionOrTag] = useState<string>(version);
const handleVersionOrTagChange = useCallback((e: any, value: string) => {
setVersionOrTag(value);
Expand Down
84 changes: 13 additions & 71 deletions wormhole-connect/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,85 +1,27 @@
import * as React from 'react';
import { Provider } from 'react-redux';
import ScopedCssBaseline from '@mui/material/ScopedCssBaseline';
import { ThemeProvider, createTheme } from '@mui/material/styles';
// import Box from '@mui/material/Box';
import { PaletteMode } from '@mui/material';
// import IconButton from '@mui/material/IconButton';
// import Brightness4Icon from '@mui/icons-material/Brightness4';
// import Brightness7Icon from '@mui/icons-material/Brightness7';
import { ThemeProvider } from '@mui/material/styles';
import './App.css';
import { store } from './store';
import AppRouter from './AppRouter';
import { getDesignTokens } from './theme';
import { THEME_MODE } from './config';
import BackgroundImage from './components/Background/BackgroundImage';
import ErrorBoundary from './components/ErrorBoundary';
import { useWidgetStateManager } from 'config/configStateManager';

const ColorModeContext = React.createContext({
toggleColorMode: () => {
/* noop TODO ??? what is this for */
},
});

function App() {
const [mode, setMode] = React.useState<PaletteMode>(THEME_MODE);
const colorMode = React.useMemo(
() => ({
// The dark mode switch would invoke this method
toggleColorMode: () => {
setMode((prevMode: PaletteMode) =>
prevMode === 'light' ? 'dark' : 'light',
);
},
}),
[],
);
// Update the theme only if the mode changes
const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]);

export default function App() {
const { themeState } = useWidgetStateManager();
return (
<Provider store={store}>
<ColorModeContext.Provider value={colorMode}>
<ThemeProvider theme={theme}>
<ScopedCssBaseline enableColorScheme>
<ErrorBoundary>
<BackgroundImage>
<AppRouter />
</BackgroundImage>
</ErrorBoundary>
</ScopedCssBaseline>
</ThemeProvider>
</ColorModeContext.Provider>
</Provider>
);
}

export default function ToggleColorMode() {
const [mode, setMode] = React.useState<'light' | 'dark'>('light');
const colorMode = React.useMemo(
() => ({
toggleColorMode: () => {
setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'));
},
}),
[],
);

const theme = React.useMemo(
() =>
createTheme({
palette: {
mode,
},
}),
[mode],
);

return (
<ColorModeContext.Provider value={colorMode}>
<ThemeProvider theme={theme}>
<App />
<ThemeProvider theme={themeState}>
<ScopedCssBaseline enableColorScheme>
<ErrorBoundary>
<BackgroundImage>
<AppRouter />
</BackgroundImage>
</ErrorBoundary>
</ScopedCssBaseline>
</ThemeProvider>
</ColorModeContext.Provider>
</Provider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { makeStyles } from 'tss-react/mui';

import { OPACITY } from '../../utils/style';
import { THEME } from 'config';
import { useWidgetStateManager } from 'config/configStateManager';

const colors = {
bg: '#030712',
Expand Down Expand Up @@ -116,9 +116,10 @@ type Props = {
};

function Background({ children }: Props) {
const { themeState } = useWidgetStateManager();
const { classes } = useStyles();

return THEME.background.default === 'wormhole' ? (
return themeState.palette.background.default === 'wormhole' ? (
<div className="container">
<div className={classes.bg}>
{children}
Expand Down
88 changes: 88 additions & 0 deletions wormhole-connect/src/config/configStateManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Theme, createTheme } from '@mui/material';
import React, {
ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { getDesignTokens } from '../theme';
import {
ConfigNetwork,
Networks,
WidgetNetworks,
WormholeConnectConfig,
envMapping,
} from './types';
import { getThemeFromConfig } from 'config';

const processEnv = import.meta.env.REACT_APP_CONNECT_ENV?.toLowerCase();

// creating global conext for the theme solves the
// rerender issue when changing widget theme
// we need state vars of network change and theme change
// as these two config items cause a change in the config

// idealy i think some more of the config (RPCS, TOKENS) should be managed
// as state because when config changes its important these items update
// more so than the optional/extra config items that dont really change
interface WidgetStateManagerContext {
themeState: Theme;
networkState: WidgetNetworks;
config: WormholeConnectConfig;
}

const WidgetStateManager = createContext({} as WidgetStateManagerContext);

export const WidgetStateManagerProvider: React.FC<{
config: WormholeConnectConfig;
children: ReactNode;
}> = ({ config, children }) => {
const [networkState, setNeworkState] = useState(config.env as WidgetNetworks);

const createThemeFromConfig = useCallback(
(config: WormholeConnectConfig) => {
const mode = config && config.mode ? config.mode : 'dark';
const defaultTheme = getThemeFromConfig(config);
return createTheme(getDesignTokens(mode, defaultTheme));
},
[createTheme, getDesignTokens],
);

const [themeState, setThemeState] = useState<Theme>(() => {
return createThemeFromConfig(config);
});

const toggleTheme = React.useCallback(
(config: WormholeConnectConfig) => {
setThemeState(() => {
return createThemeFromConfig(config);
});
},
[setThemeState],
);
useEffect(() => {
toggleTheme(config);
}, [config, toggleTheme]);

useEffect(() => {
const env = (config.env || processEnv || Networks.testnet) as ConfigNetwork;
setNeworkState(envMapping[env] || WidgetNetworks.TESTNET);
}, [config.env]);

return (
<WidgetStateManager.Provider
value={{
networkState,
themeState,
config,
}}
>
{children}
</WidgetStateManager.Provider>
);
};

export const useWidgetStateManager = (): WidgetStateManagerContext =>
useContext(WidgetStateManager);
18 changes: 9 additions & 9 deletions wormhole-connect/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import {
WormholeConnectConfig,
Route,
} from './types';
import { dark, light } from '../theme';
import { validateConfigs, validateDefaults } from './utils';
import { Alignment } from 'components/Header';
import { dark, light } from '../theme';

const el = document.getElementById('wormhole-connect');
export const el = document.getElementById('wormhole-connect');
if (!el)
throw new Error('must specify an anchor element with id wormhole-connect');
const configJson = el.getAttribute('config');
Expand All @@ -41,6 +41,13 @@ const getPageHeader = (): { text: string; align: Alignment } => {
}
};

export const getThemeFromConfig = (config: WormholeConnectConfig) => {
const mode = config && config.mode ? config.mode : 'dark';
const baseTheme = mode === 'dark' ? dark : light;
const customTheme = config && config?.customTheme;
return customTheme ? Object.assign({}, baseTheme, customTheme) : baseTheme;
};

export const ENV = getEnv();
export const isMainnet = ENV === 'MAINNET';
export const sdkConfig = WormholeContext.getConfig(ENV);
Expand Down Expand Up @@ -129,13 +136,6 @@ export const GRAPHQL =

export const GAS_ESTIMATES = NETWORK_DATA.gasEstimates;

export const THEME_MODE = config && config.mode ? config.mode : 'dark';
export const CUSTOM_THEME = config && config.customTheme;
const BASE_THEME = THEME_MODE === 'dark' ? dark : light;
export const THEME = CUSTOM_THEME
? Object.assign({}, BASE_THEME, CUSTOM_THEME)
: BASE_THEME;

export const CTA = config && config.cta;
export const BRIDGE_DEFAULTS =
config && validateDefaults(config.bridgeDefaults);
Expand Down
21 changes: 21 additions & 0 deletions wormhole-connect/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ import {
import { Alignment } from 'components/Header';
import { ExtendedTheme } from 'theme';

export enum WidgetNetworks {
MAINNET = 'MAINNET',
TESTNET = 'TESTNET',
DEVNET = 'DEVNET',
}

export enum Networks {
mainnet = 'mainnet',
testnet = 'testnet',
devnet = 'devnet',
}

export const envMapping = {
[Networks.mainnet]: WidgetNetworks.MAINNET,
[Networks.devnet]: WidgetNetworks.DEVNET,
[Networks.testnet]: WidgetNetworks.TESTNET,
};

export type ConfigNetwork = keyof typeof Networks;
export type WidgetNetwork = keyof typeof WidgetNetworks;

export enum Icon {
'AVAX' = 1,
'BNB',
Expand Down
74 changes: 55 additions & 19 deletions wormhole-connect/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,59 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import React, { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
import ErrorBoundary from './components/ErrorBoundary';
import { WidgetStateManagerProvider } from 'config/configStateManager';
import ErrorBoundary from 'components/ErrorBoundary';
import { WormholeConnectConfig } from 'config/types';

export * from './theme';
const IndexPage = () => {
const [config, setConfig] = useState<WormholeConnectConfig>({});

const root = ReactDOM.createRoot(
document.getElementById('wormhole-connect') as HTMLElement,
);
root.render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>,
);
useEffect(() => {
const el = document.getElementById('wormhole-connect');

if (!el) {
throw new Error(
'must specify an anchor element with id wormhole-connect',
);
}

const updateConfig = () => {
const newConfig = JSON.parse(el.getAttribute('config') || '{}');
setConfig(newConfig);
};

updateConfig();

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
// Create a MutationObserver to watch for changes in the attributes of el
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'config'
) {
updateConfig();
}
}
});

observer.observe(el, { attributes: true });

return () => {
observer.disconnect();
};
}, [setConfig]);

return (
<React.StrictMode>
<ErrorBoundary>
<WidgetStateManagerProvider config={config}>
<App />
</WidgetStateManagerProvider>
</ErrorBoundary>
</React.StrictMode>
);
};

createRoot(document.getElementById('wormhole-connect')! as HTMLElement).render(
<IndexPage />,
);
Loading