diff --git a/.changeset/late-cycles-lick.md b/.changeset/late-cycles-lick.md new file mode 100644 index 00000000..0de79bcf --- /dev/null +++ b/.changeset/late-cycles-lick.md @@ -0,0 +1,18 @@ +--- +'@reown/appkit-scaffold-react-native': minor +'@reown/appkit-ethers5-react-native': minor +'@reown/appkit-common-react-native': minor +'@reown/appkit-ethers-react-native': minor +'@reown/appkit-wallet-react-native': minor +'@reown/appkit-wagmi-react-native': minor +'@reown/appkit-core-react-native': minor +'@reown/appkit-ui-react-native': minor +'@reown/appkit-auth-ethers-react-native': minor +'@reown/appkit-auth-wagmi-react-native': minor +'@reown/appkit-coinbase-ethers-react-native': minor +'@reown/appkit-coinbase-wagmi-react-native': minor +'@reown/appkit-scaffold-utils-react-native': minor +'@reown/appkit-siwe-react-native': minor +--- + +feat: added ability to change themeMode and override accent color diff --git a/apps/native/App.tsx b/apps/native/App.tsx index 9c39a08c..672675e6 100644 --- a/apps/native/App.tsx +++ b/apps/native/App.tsx @@ -76,7 +76,7 @@ createAppKit({ debug: true, features: { email: true, - socials: ['x', 'farcaster', 'discord', 'apple'], + socials: ['x', 'discord', 'apple'], emailShowWallets: true, swaps: true } diff --git a/packages/common/src/utils/TypeUtil.ts b/packages/common/src/utils/TypeUtil.ts index 208e8fee..a25bdcd6 100644 --- a/packages/common/src/utils/TypeUtil.ts +++ b/packages/common/src/utils/TypeUtil.ts @@ -87,3 +87,9 @@ export interface TransactionQuantity { } export type SocialProvider = 'apple' | 'x' | 'discord' | 'farcaster'; + +export type ThemeMode = 'dark' | 'light'; + +export interface ThemeVariables { + accent?: string; +} diff --git a/packages/core/src/__tests__/controllers/ThemeController.test.ts b/packages/core/src/__tests__/controllers/ThemeController.test.ts index 456db8f3..7285c8d7 100644 --- a/packages/core/src/__tests__/controllers/ThemeController.test.ts +++ b/packages/core/src/__tests__/controllers/ThemeController.test.ts @@ -4,7 +4,7 @@ import { ThemeController } from '../../index'; describe('ThemeController', () => { it('should have valid default state', () => { expect(ThemeController.state).toEqual({ - themeMode: 'dark', + themeMode: undefined, themeVariables: {} }); }); diff --git a/packages/core/src/controllers/ThemeController.ts b/packages/core/src/controllers/ThemeController.ts index 53871dbb..f3453b00 100644 --- a/packages/core/src/controllers/ThemeController.ts +++ b/packages/core/src/controllers/ThemeController.ts @@ -1,15 +1,15 @@ import { proxy, subscribe as sub } from 'valtio'; -import type { ThemeMode, ThemeVariables } from '../utils/TypeUtil'; +import type { ThemeMode, ThemeVariables } from '@reown/appkit-common-react-native'; // -- Types --------------------------------------------- // export interface ThemeControllerState { - themeMode: ThemeMode; + themeMode?: ThemeMode; themeVariables: ThemeVariables; } // -- State --------------------------------------------- // const state = proxy({ - themeMode: 'dark', + themeMode: undefined, themeVariables: {} }); diff --git a/packages/core/src/utils/TypeUtil.ts b/packages/core/src/utils/TypeUtil.ts index 5b3babaf..41c6e626 100644 --- a/packages/core/src/utils/TypeUtil.ts +++ b/packages/core/src/utils/TypeUtil.ts @@ -1,5 +1,10 @@ import { type EventEmitter } from 'events'; -import type { Balance, SocialProvider, Transaction } from '@reown/appkit-common-react-native'; +import type { + Balance, + SocialProvider, + ThemeMode, + Transaction +} from '@reown/appkit-common-react-native'; export interface BaseError { message?: string; @@ -140,14 +145,6 @@ export type RequestCache = | 'only-if-cached' | 'reload'; -// -- ThemeController Types --------------------------------------------------- - -export type ThemeMode = 'dark' | 'light'; - -export interface ThemeVariables { - accent?: string; -} - // -- BlockchainApiController Types --------------------------------------------- export interface BlockchainApiIdentityRequest { address: string; diff --git a/packages/scaffold/src/client.ts b/packages/scaffold/src/client.ts index cc3ed1d9..a5a3fc9e 100644 --- a/packages/scaffold/src/client.ts +++ b/packages/scaffold/src/client.ts @@ -10,8 +10,6 @@ import type { EventsControllerState, PublicStateControllerState, ThemeControllerState, - ThemeMode, - ThemeVariables, Connector, ConnectedWalletInfo, Features @@ -33,7 +31,12 @@ import { ThemeController, TransactionsController } from '@reown/appkit-core-react-native'; -import { ConstantsUtil, ErrorUtil } from '@reown/appkit-common-react-native'; +import { + ConstantsUtil, + ErrorUtil, + type ThemeMode, + type ThemeVariables +} from '@reown/appkit-common-react-native'; // -- Types --------------------------------------------------------------------- export interface LibraryOptions { diff --git a/packages/scaffold/src/modal/w3m-account-button/index.tsx b/packages/scaffold/src/modal/w3m-account-button/index.tsx index 329ffb3b..8bb37376 100644 --- a/packages/scaffold/src/modal/w3m-account-button/index.tsx +++ b/packages/scaffold/src/modal/w3m-account-button/index.tsx @@ -4,10 +4,11 @@ import { CoreHelperUtil, NetworkController, ModalController, - AssetUtil + AssetUtil, + ThemeController } from '@reown/appkit-core-react-native'; -import { AccountButton as AccountButtonUI } from '@reown/appkit-ui-react-native'; +import { AccountButton as AccountButtonUI, ThemeProvider } from '@reown/appkit-ui-react-native'; import { ApiController } from '@reown/appkit-core-react-native'; import type { StyleProp, ViewStyle } from 'react-native'; @@ -27,22 +28,25 @@ export function AccountButton({ balance, disabled, style, testID }: AccountButto profileName } = useSnapshot(AccountController.state); const { caipNetwork } = useSnapshot(NetworkController.state); + const { themeMode, themeVariables } = useSnapshot(ThemeController.state); const networkImage = AssetUtil.getNetworkImage(caipNetwork); const showBalance = balance === 'show'; return ( - ModalController.open()} - address={address} - profileName={profileName} - networkSrc={networkImage} - imageHeaders={ApiController._getApiHeaders()} - avatarSrc={profileImage} - disabled={disabled} - style={style} - balance={showBalance ? CoreHelperUtil.formatBalance(balanceVal, balanceSymbol) : ''} - testID={testID} - /> + + ModalController.open()} + address={address} + profileName={profileName} + networkSrc={networkImage} + imageHeaders={ApiController._getApiHeaders()} + avatarSrc={profileImage} + disabled={disabled} + style={style} + balance={showBalance ? CoreHelperUtil.formatBalance(balanceVal, balanceSymbol) : ''} + testID={testID} + /> + ); } diff --git a/packages/scaffold/src/modal/w3m-connect-button/index.tsx b/packages/scaffold/src/modal/w3m-connect-button/index.tsx index bd699abf..98f0c0e1 100644 --- a/packages/scaffold/src/modal/w3m-connect-button/index.tsx +++ b/packages/scaffold/src/modal/w3m-connect-button/index.tsx @@ -1,7 +1,8 @@ import { useSnapshot } from 'valtio'; -import { ModalController } from '@reown/appkit-core-react-native'; +import { ModalController, ThemeController } from '@reown/appkit-core-react-native'; import { ConnectButton as ConnectButtonUI, + ThemeProvider, type ConnectButtonProps as ConnectButtonUIProps } from '@reown/appkit-ui-react-native'; @@ -23,17 +24,20 @@ export function ConnectButton({ testID }: ConnectButtonProps) { const { open, loading } = useSnapshot(ModalController.state); + const { themeMode, themeVariables } = useSnapshot(ThemeController.state); return ( - ModalController.open()} - size={size} - loading={loading || open} - style={style} - testID={testID} - disabled={disabled} - > - {loading || open ? loadingLabel : label} - + + ModalController.open()} + size={size} + loading={loading || open} + style={style} + testID={testID} + disabled={disabled} + > + {loading || open ? loadingLabel : label} + + ); } diff --git a/packages/scaffold/src/modal/w3m-modal/index.tsx b/packages/scaffold/src/modal/w3m-modal/index.tsx index 61ef0a29..fe2db865 100644 --- a/packages/scaffold/src/modal/w3m-modal/index.tsx +++ b/packages/scaffold/src/modal/w3m-modal/index.tsx @@ -2,7 +2,7 @@ import { useSnapshot } from 'valtio'; import { useCallback, useEffect } from 'react'; import { useWindowDimensions, StatusBar } from 'react-native'; import Modal from 'react-native-modal'; -import { Card } from '@reown/appkit-ui-react-native'; +import { Card, ThemeProvider } from '@reown/appkit-ui-react-native'; import { AccountController, ApiController, @@ -16,7 +16,8 @@ import { TransactionsController, type CaipAddress, type AppKitFrameProvider, - WebviewController + WebviewController, + ThemeController } from '@reown/appkit-core-react-native'; import { SIWEController } from '@reown/appkit-siwe-react-native'; @@ -31,6 +32,7 @@ export function AppKit() { const { connectors, connectedConnector } = useSnapshot(ConnectorController.state); const { caipAddress, isConnected } = useSnapshot(AccountController.state); const { frameViewVisible, webviewVisible } = useSnapshot(WebviewController.state); + const { themeMode, themeVariables } = useSnapshot(ThemeController.state); const { height } = useWindowDimensions(); const { isLandscape } = useCustomDimensions(); const portraitHeight = height - 120; @@ -117,28 +119,32 @@ export function AppKit() { return ( <> - - -
- - - - - {!!showAuth && AuthView && } - {!!showAuth && SocialView && } + + + +
+ + + + + {!!showAuth && AuthView && } + {!!showAuth && SocialView && } + ); } diff --git a/packages/scaffold/src/modal/w3m-network-button/index.tsx b/packages/scaffold/src/modal/w3m-network-button/index.tsx index ddffa20f..353a1804 100644 --- a/packages/scaffold/src/modal/w3m-network-button/index.tsx +++ b/packages/scaffold/src/modal/w3m-network-button/index.tsx @@ -6,9 +6,10 @@ import { AssetUtil, EventsController, ModalController, - NetworkController + NetworkController, + ThemeController } from '@reown/appkit-core-react-native'; -import { NetworkButton as NetworkButtonUI } from '@reown/appkit-ui-react-native'; +import { NetworkButton as NetworkButtonUI, ThemeProvider } from '@reown/appkit-ui-react-native'; export interface NetworkButtonProps { disabled?: boolean; @@ -19,6 +20,7 @@ export function NetworkButton({ disabled, style }: NetworkButtonProps) { const { isConnected } = useSnapshot(AccountController.state); const { caipNetwork } = useSnapshot(NetworkController.state); const { loading } = useSnapshot(ModalController.state); + const { themeMode, themeVariables } = useSnapshot(ThemeController.state); const onNetworkPress = () => { ModalController.open({ view: 'Networks' }); @@ -29,16 +31,18 @@ export function NetworkButton({ disabled, style }: NetworkButtonProps) { }; return ( - - {caipNetwork?.name ?? (isConnected ? 'Unknown Network' : 'Select Network')} - + + + {caipNetwork?.name ?? (isConnected ? 'Unknown Network' : 'Select Network')} + + ); } diff --git a/packages/scaffold/src/views/w3m-account-default-view/components/auth-buttons.tsx b/packages/scaffold/src/views/w3m-account-default-view/components/auth-buttons.tsx index 70524cb5..43e3cd38 100644 --- a/packages/scaffold/src/views/w3m-account-default-view/components/auth-buttons.tsx +++ b/packages/scaffold/src/views/w3m-account-default-view/components/auth-buttons.tsx @@ -33,7 +33,14 @@ export function AuthButtons({ ) : ( - + {text} diff --git a/packages/scaffold/src/views/w3m-account-default-view/index.tsx b/packages/scaffold/src/views/w3m-account-default-view/index.tsx index 7449c251..dba1584a 100644 --- a/packages/scaffold/src/views/w3m-account-default-view/index.tsx +++ b/packages/scaffold/src/views/w3m-account-default-view/index.tsx @@ -239,7 +239,8 @@ export function AccountDefaultView() { diff --git a/packages/ui/package.json b/packages/ui/package.json index f91110be..fa5b4dc6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -38,6 +38,7 @@ "access": "public" }, "dependencies": { + "polished": "4.3.1", "qrcode": "1.5.3" }, "peerDependencies": { diff --git a/packages/ui/src/context/ThemeContext.tsx b/packages/ui/src/context/ThemeContext.tsx new file mode 100644 index 00000000..29805193 --- /dev/null +++ b/packages/ui/src/context/ThemeContext.tsx @@ -0,0 +1,44 @@ +import { useColorScheme } from 'react-native'; +import { createContext, useContext, type ReactNode } from 'react'; +import type { ThemeMode, ThemeVariables } from '@reown/appkit-common-react-native'; + +import { DarkTheme, LightTheme, getAccentColors } from '../utils/ThemeUtil'; + +type ThemeContextType = { + themeMode?: ThemeMode; + themeVariables?: ThemeVariables; +}; + +export const ThemeContext = createContext(undefined); + +interface ThemeProviderProps { + children: ReactNode; + themeMode?: ThemeMode; + themeVariables?: ThemeVariables; +} + +export function ThemeProvider({ children, themeMode, themeVariables }: ThemeProviderProps) { + return ( + {children} + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + const scheme = useColorScheme(); + + // If the theme mode is not set, use the system color scheme + const themeMode = context?.themeMode ?? scheme; + const themeVariables = context?.themeVariables ?? {}; + + let Theme = themeMode === 'dark' ? DarkTheme : LightTheme; + + if (themeVariables.accent) { + Theme = { + ...Theme, + ...getAccentColors(themeVariables.accent) + }; + } + + return Theme; +} diff --git a/packages/ui/src/hooks/useTheme.ts b/packages/ui/src/hooks/useTheme.ts index eec122ac..4a55a0cc 100644 --- a/packages/ui/src/hooks/useTheme.ts +++ b/packages/ui/src/hooks/useTheme.ts @@ -1,9 +1,5 @@ -import { useColorScheme } from 'react-native'; -import { DarkTheme, LightTheme } from '../utils/ThemeUtil'; +import { useTheme as useThemeContext } from '../context/ThemeContext'; -export function useTheme() { - const scheme = useColorScheme(); - const Theme = scheme === 'dark' ? DarkTheme : LightTheme; - - return Theme; -} +export const useTheme = () => { + return useThemeContext(); +}; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index fc780cff..b7a7251c 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -85,4 +85,5 @@ export { TransactionUtil } from './utils/TransactionUtil'; export { Spacing, BorderRadius } from './utils/ThemeUtil'; export { useTheme } from './hooks/useTheme'; +export { ThemeProvider } from './context/ThemeContext'; export { useAnimatedValue } from './hooks/useAnimatedValue'; diff --git a/packages/ui/src/utils/ThemeUtil.ts b/packages/ui/src/utils/ThemeUtil.ts index 9671a12d..00d1e0d2 100644 --- a/packages/ui/src/utils/ThemeUtil.ts +++ b/packages/ui/src/utils/ThemeUtil.ts @@ -1,5 +1,23 @@ +import { transparentize, darken, lighten } from 'polished'; + import type { SpacingType, ThemeKeys } from './TypesUtil'; +export const getAccentColors = (baseAccentColor: string) => { + return { + 'accent-100': baseAccentColor, + 'accent-090': lighten(0.05, baseAccentColor), + 'accent-080': lighten(0.1, baseAccentColor), + 'accent-020': darken(0.1, baseAccentColor), + 'accent-glass-090': transparentize(0.1, baseAccentColor), + 'accent-glass-080': transparentize(0.2, baseAccentColor), + 'accent-glass-020': transparentize(0.8, baseAccentColor), + 'accent-glass-015': transparentize(0.85, baseAccentColor), + 'accent-glass-010': transparentize(0.9, baseAccentColor), + 'accent-glass-005': transparentize(0.95, baseAccentColor), + 'accent-glass-002': transparentize(0.98, baseAccentColor) + }; +}; + export const DarkTheme: { [key in ThemeKeys]: string } = { 'accent-100': '#667DFF', 'accent-090': '#7388FD', diff --git a/yarn.lock b/yarn.lock index 5b846cbb..abf64c8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6823,6 +6823,7 @@ __metadata: version: 0.0.0-use.local resolution: "@reown/appkit-ui-react-native@workspace:packages/ui" dependencies: + polished: "npm:4.3.1" qrcode: "npm:1.5.3" peerDependencies: react: ">=17" @@ -19220,6 +19221,15 @@ __metadata: languageName: node linkType: hard +"polished@npm:4.3.1": + version: 4.3.1 + resolution: "polished@npm:4.3.1" + dependencies: + "@babel/runtime": "npm:^7.17.8" + checksum: 45480d4c7281a134281cef092f6ecc202a868475ff66a390fee6e9261386e16f3047b4de46a2f2e1cf7fb7aa8f52d30b4ed631a1e3bcd6f303ca31161d4f07fe + languageName: node + linkType: hard + "polished@npm:^4.2.2": version: 4.2.2 resolution: "polished@npm:4.2.2"