diff --git a/MIGRATION.md b/MIGRATION.md index 400f8f2b2..c97297d13 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -34,7 +34,7 @@ ie. - `SequenceWalletProvider` (previously KitWalletProvider) - `SequenceHooksProvider` -Also for builder integration KitPreviewProvider was renamed `SequenceConnectPreviewProvider` +Also for builder integration KitPreviewProvider was renamed `SequenceConnectInlineProvider` ### Hooks diff --git a/examples/react/src/App.tsx b/examples/react/src/App.tsx index 6345eb780..fa735287f 100644 --- a/examples/react/src/App.tsx +++ b/examples/react/src/App.tsx @@ -5,6 +5,7 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom' import { Homepage } from './components/Homepage' import { ImmutableCallback } from './components/ImmutableCallback' +import { InlineDemo } from './components/InlineDemo' import { XAuthCallback } from './components/XAuthCallback' import { checkoutConfig, config } from './config' @@ -16,6 +17,7 @@ export const App = () => { } /> + } /> } /> } /> diff --git a/examples/react/src/components/Homepage.tsx b/examples/react/src/components/Homepage.tsx index 1929fed26..463c77b15 100644 --- a/examples/react/src/components/Homepage.tsx +++ b/examples/react/src/components/Homepage.tsx @@ -2,6 +2,7 @@ import { useOpenConnectModal, useWallets, WalletType } from '@0xsequence/connect import { Button, Card, CheckmarkIcon, Image, Text } from '@0xsequence/design-system' import { clsx } from 'clsx' import { Footer } from 'example-shared-components' +import { Link } from 'react-router-dom' import { Connected } from './Connected' @@ -34,6 +35,9 @@ export const Homepage = () => {
diff --git a/examples/react/src/components/InlineDemo.tsx b/examples/react/src/components/InlineDemo.tsx new file mode 100644 index 000000000..5ecc4c4c4 --- /dev/null +++ b/examples/react/src/components/InlineDemo.tsx @@ -0,0 +1,60 @@ +import { SequenceConnectInline, type SequenceConnectConfig } from '@0xsequence/connect' +import { Text } from '@0xsequence/design-system' +import { Footer } from 'example-shared-components' +import { useMemo } from 'react' +import { useNavigate } from 'react-router-dom' + +import { config } from '../config' + +export const InlineDemo = () => { + const navigate = useNavigate() + + const inlineConfig: SequenceConnectConfig = useMemo( + () => ({ + ...config, + connectConfig: { + ...config.connectConfig, + onConnectSuccess: (address: string) => { + console.log('Connected successfully with address:', address) + // Redirect to homepage after successful connection + navigate('/') + } + } + }), + [navigate] + ) + + return ( +
+
+
+ {/* Left side - Description */} +
+ + Inline Connect Demo + + + This demonstrates the SequenceConnectInline component, which renders the connect UI inline within your layout + instead of in a modal. + + + Perfect for custom layouts, embedded wallet experiences, or when you want the connect UI to be part of your page + flow. + + + Connect with your wallet and you'll be redirected to the homepage automatically. + +
+ + {/* Right side - Inline Connect UI */} +
+
+ +
+
+
+
+
+
+ ) +} diff --git a/packages/connect/README.md b/packages/connect/README.md index e4f39afe8..9e1dac7c1 100644 --- a/packages/connect/README.md +++ b/packages/connect/README.md @@ -6,6 +6,7 @@ Sequence Web SDK 🧰 is a library enabling developers to easily integrate web3 - Connect to popular web3 wallets eg: walletConnect, metamask ! 🦊 ⛓️ - Full-fledged embedded wallet for coins and collectibles 👛 🖼️ 🪙 - Fiat onramp 💵 💶 💴 💷 +- Inline connect UI for custom layouts and embedded experiences 🎨 View the [demo](https://0xsequence.github.io/web-sdk)! 👀 @@ -52,6 +53,7 @@ interface CreateConfigOptions { chainId: number }> ethAuth?: EthAuthSettings + onConnectSuccess?: (address: string) => void // callback fired when wallet connects wagmiConfig?: WagmiConfig // optional wagmiConfig overrides @@ -243,6 +245,76 @@ const MyReactComponent = () => { } ``` +### Inline Connect UI + +
+ Inline Connect UI +
+ +Instead of using a modal, you can render the connect UI inline within your layout using the `SequenceConnectInline` component. This is perfect for custom layouts, embedded wallet experiences, or when you want the connect UI to be part of your page flow. + +```js +import { SequenceConnectInline, createConfig } from '@0xsequence/connect' +import { useNavigate } from 'react-router-dom' + +const config = createConfig('waas', { + projectAccessKey: '', + chainIds: [1, 137], + defaultChainId: 1, + appName: 'Demo Dapp', + waasConfigKey: '', + + // Optional: callback fired when wallet connects successfully + onConnectSuccess: (address) => { + console.log('Connected wallet:', address) + // Redirect or perform other actions + }, + + google: { clientId: '' }, + email: true +}) + +function InlinePage() { + return ( +
+

Connect Your Wallet

+ +
+ ) +} +``` + +#### Key Differences from Modal UI: + +- **No padding/margins**: The inline UI removes the default padding designed for modal display +- **Full width**: The component fills its container width +- **No modal backdrop**: Renders directly in your layout +- **Custom positioning**: You control the placement with your own CSS/layout + +#### Advanced: Using SequenceConnectInlineProvider + +For more control, you can use the lower-level `SequenceConnectInlineProvider`: + +```js +import { SequenceConnectInlineProvider } from '@0xsequence/connect' +import { WagmiProvider } from 'wagmi' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +function App() { + return ( + + + + + + + + ) +} +``` + ## Hooks ### useOpenConnectModal @@ -294,6 +366,11 @@ The settings are described in more detailed in the Sequence Web SDK documentatio } ], readOnlyNetworks: [10], + // callback fired when wallet connects successfully + onConnectSuccess: (address) => { + console.log('Wallet connected:', address) + // Perform actions like redirecting, analytics tracking, etc. + }, } diff --git a/packages/connect/src/components/Connect/Connect.tsx b/packages/connect/src/components/Connect/Connect.tsx index 498d7694d..e11448c1e 100644 --- a/packages/connect/src/components/Connect/Connect.tsx +++ b/packages/connect/src/components/Connect/Connect.tsx @@ -43,7 +43,7 @@ export const SEQUENCE_UNIVERSAL_CONNECTOR_NAME = 'Sequence' interface ConnectProps extends SequenceConnectProviderProps { emailConflictInfo?: FormattedEmailConflictInfo | null onClose: () => void - isPreview?: boolean + isInline?: boolean } export const Connect = (props: ConnectProps) => { @@ -52,7 +52,7 @@ export const Connect = (props: ConnectProps) => { const { analytics } = useAnalyticsContext() const { hideExternalConnectOptions, hideConnectedWallets, hideSocialConnectOptions } = useWalletSettings() - const { onClose, emailConflictInfo, config = {} as ConnectConfig, isPreview = false } = props + const { onClose, emailConflictInfo, config = {} as ConnectConfig, isInline = false } = props const { signIn = {} } = config const storage = useStorage() @@ -256,6 +256,11 @@ export const Connect = (props: ConnectProps) => { connect( { connector }, { + onSuccess: result => { + if (result?.accounts[0]) { + config.onConnectSuccess?.(result.accounts[0]) + } + }, onSettled: result => { setLastConnectedWallet(result?.accounts[0]) } @@ -377,14 +382,14 @@ export const Connect = (props: ConnectProps) => { : `Something went wrong. (${waasStatusData.errorResponse.msg})` return ( -
+
- + {isLoading ? `Connecting...` @@ -406,14 +411,14 @@ export const Connect = (props: ConnectProps) => { } return ( -
+
- + {isLoading ? `Connecting...` @@ -595,8 +600,8 @@ export const Connect = (props: ConnectProps) => { ) } -const TitleWrapper = ({ children, isPreview }: { children: ReactNode; isPreview: boolean }) => { - if (isPreview) { +const TitleWrapper = ({ children, isInline }: { children: ReactNode; isInline: boolean }) => { + if (isInline) { return <>{children} } diff --git a/packages/connect/src/components/SequenceConnectPreview/SequenceConnectPreview.tsx b/packages/connect/src/components/SequenceConnectInline/SequenceConnectInline.tsx similarity index 50% rename from packages/connect/src/components/SequenceConnectPreview/SequenceConnectPreview.tsx rename to packages/connect/src/components/SequenceConnectInline/SequenceConnectInline.tsx index 2cec35a72..e4621a909 100644 --- a/packages/connect/src/components/SequenceConnectPreview/SequenceConnectPreview.tsx +++ b/packages/connect/src/components/SequenceConnectInline/SequenceConnectInline.tsx @@ -3,38 +3,30 @@ import type { ReactNode } from 'react' import { WagmiProvider, type State } from 'wagmi' import type { SequenceConnectConfig } from '../../config/createConfig.js' -import { SequenceConnectPreviewProvider } from '../SequenceConnectPreviewProvider/SequenceConnectPreviewProvider.js' +import { SequenceConnectInlineProvider } from '../SequenceConnectInlineProvider/SequenceConnectInlineProvider.js' const defaultQueryClient = new QueryClient() -interface SequenceConnectPreviewProps { +export interface SequenceConnectInlineProps { config: SequenceConnectConfig queryClient?: QueryClient initialState?: State | undefined - children: ReactNode + children?: ReactNode } /** - * @internal - * Preview version of SequenceConnect component. - * This component should only be used for testing purposes. - * It provides the same functionality as SequenceConnect but only for preview purposes. + * Inline version of SequenceConnect component. + * This component renders the connect UI inline within your layout instead of in a modal. + * Ideal for embedded wallet experiences or custom layouts. */ -export const SequenceConnectPreview = (props: SequenceConnectPreviewProps) => { +export const SequenceConnectInline = (props: SequenceConnectInlineProps) => { const { config, queryClient, initialState, children } = props const { connectConfig, wagmiConfig } = config return ( - - {children} - + {children} ) diff --git a/packages/connect/src/components/SequenceConnectInline/index.ts b/packages/connect/src/components/SequenceConnectInline/index.ts new file mode 100644 index 000000000..1f458000f --- /dev/null +++ b/packages/connect/src/components/SequenceConnectInline/index.ts @@ -0,0 +1 @@ +export * from './SequenceConnectInline.js' diff --git a/packages/connect/src/components/SequenceConnectInlineProvider/SequenceConnectInlineProvider.tsx b/packages/connect/src/components/SequenceConnectInlineProvider/SequenceConnectInlineProvider.tsx new file mode 100644 index 000000000..dc60be3df --- /dev/null +++ b/packages/connect/src/components/SequenceConnectInlineProvider/SequenceConnectInlineProvider.tsx @@ -0,0 +1,387 @@ +'use client' + +import { Button, Card, Modal, ModalPrimitive, Text, ThemeProvider, type Theme } from '@0xsequence/design-system' +import { SequenceHooksProvider } from '@0xsequence/hooks' +import { ChainId } from '@0xsequence/network' +import { SequenceClient, setupAnalytics, type Analytics } from '@0xsequence/provider' +import { GoogleOAuthProvider } from '@react-oauth/google' +import { AnimatePresence } from 'motion/react' +import React, { useEffect, useState, type ReactNode } from 'react' +import { hexToString, type Hex } from 'viem' +import { useAccount, useConfig, useConnections, type Connector } from 'wagmi' + +import { DEFAULT_SESSION_EXPIRATION, LocalStorageKey, WEB_SDK_VERSION } from '../../constants/index.js' +import { AnalyticsContextProvider } from '../../contexts/Analytics.js' +import { ConnectConfigContextProvider } from '../../contexts/ConnectConfig.js' +import { ConnectModalContextProvider } from '../../contexts/ConnectModal.js' +import { SocialLinkContextProvider } from '../../contexts/SocialLink.js' +import { ThemeContextProvider } from '../../contexts/Theme.js' +import { WalletConfigContextProvider } from '../../contexts/WalletConfig.js' +import { useStorage } from '../../hooks/useStorage.js' +import { useWaasConfirmationHandler } from '../../hooks/useWaasConfirmationHandler.js' +import { useEmailConflict } from '../../hooks/useWaasEmailConflict.js' +import { styleProperties } from '../../styleProperties.js' +import { styles } from '../../styles.js' +import { + type ConnectConfig, + type DisplayedAsset, + type EthAuthSettings, + type ExtendedConnector, + type ModalPosition +} from '../../types.js' +import { isJSON } from '../../utils/helpers.js' +import { getModalPositionCss } from '../../utils/styling.js' +import { Connect } from '../Connect/Connect.js' +import { EpicAuthProvider } from '../EpicAuthProvider/index.js' +import { JsonTreeViewer } from '../JsonTreeViewer.js' +import { NetworkBadge } from '../NetworkBadge/index.js' +import { PageHeading } from '../PageHeading/index.js' +import { PoweredBySequence } from '../SequenceLogo/index.js' +import { ShadowRoot } from '../ShadowRoot/index.js' +import { SocialLink } from '../SocialLink/SocialLink.js' +import { TxnDetails } from '../TxnDetails/index.js' + +export type SequenceConnectInlineProviderProps = { + children: ReactNode + config: ConnectConfig +} + +/** + * Inline version of SequenceConnectProvider component. + * This component renders the connect UI inline within your layout instead of in a modal. + * Ideal for embedded wallet experiences or custom layouts. + */ +export const SequenceConnectInlineProvider = (props: SequenceConnectInlineProviderProps) => { + const { config, children } = props + + const { + defaultTheme = 'dark', + signIn = {}, + position = 'center', + displayedAssets: displayedAssetsSetting = [], + readOnlyNetworks, + ethAuth = {} as EthAuthSettings, + disableAnalytics = false, + hideExternalConnectOptions = false, + hideConnectedWallets = false, + hideSocialConnectOptions = false, + customCSS, + waasConfigKey = '' + } = config + + const defaultAppName = signIn.projectName || 'app' + + const { expiry = DEFAULT_SESSION_EXPIRATION, app = defaultAppName, origin, nonce } = ethAuth + + const [theme, setTheme] = useState>(defaultTheme || 'dark') + const [modalPosition, setModalPosition] = useState(position) + const [displayedAssets, setDisplayedAssets] = useState(displayedAssetsSetting) + const [analytics, setAnalytics] = useState() + const { address, isConnected } = useAccount() + const wagmiConfig = useConfig() + const storage = useStorage() + const connections = useConnections() + const waasConnector: Connector | undefined = connections.find(c => c.connector.id.includes('waas'))?.connector + + const [isWalletWidgetOpen, setIsWalletWidgetOpen] = useState(false) + + useEffect(() => { + const handleWalletModalStateChange = (event: Event) => { + const customEvent = event as CustomEvent<{ open: boolean }> + setIsWalletWidgetOpen(customEvent.detail.open) + } + + window.addEventListener('sequence:wallet-modal-state-change', handleWalletModalStateChange) + + return () => { + window.removeEventListener('sequence:wallet-modal-state-change', handleWalletModalStateChange) + } + }, []) + + const [pendingRequestConfirmation, confirmPendingRequest, rejectPendingRequest] = useWaasConfirmationHandler( + waasConnector, + !isWalletWidgetOpen + ) + + const googleWaasConnector = wagmiConfig.connectors.find( + c => c.id === 'sequence-waas' && (c as ExtendedConnector)._wallet.id === 'google-waas' + ) as ExtendedConnector | undefined + const googleClientId: string = (googleWaasConnector as any)?.params?.googleClientId || '' + + const getAnalyticsClient = (projectAccessKey: string) => { + // @ts-ignore-next-line + const sequenceAnalytics = setupAnalytics(projectAccessKey) as Analytics + + type TrackArgs = Parameters + const originalTrack = sequenceAnalytics.track.bind(sequenceAnalytics) + + sequenceAnalytics.track = (...args: TrackArgs) => { + const [event] = args + if (event && typeof event === 'object' && 'props' in event) { + event.props = { + ...event.props, + sdkType: 'sequence web sdk', + version: WEB_SDK_VERSION + } + } + return originalTrack?.(...args) + } + setAnalytics(sequenceAnalytics) + } + + useEffect(() => { + if (!isConnected) { + analytics?.reset() + + return + } + if (address) { + analytics?.identify(address.toLowerCase()) + } + }, [analytics, address, isConnected]) + + useEffect(() => { + if (!disableAnalytics) { + getAnalyticsClient(config.projectAccessKey) + } + }, []) + + useEffect(() => { + if (theme !== defaultTheme) { + setTheme(defaultTheme) + } + }, [defaultTheme]) + + useEffect(() => { + if (modalPosition !== position) { + setModalPosition(position) + } + }, [position]) + + // Write data in local storage for retrieval in connectors + useEffect(() => { + // Theme + // TODO: set the sequence theme once it is added to connect options + if (typeof theme === 'object') { + // localStorage.setItem(LocalStorageKey.Theme, JSON.stringify(theme)) + } else { + localStorage.setItem(LocalStorageKey.Theme, theme) + } + // EthAuth + // note: keep an eye out for potential race-conditions, though they shouldn't occur. + // If there are race conditions, the settings could be a function executed prior to being passed to wagmi + storage?.setItem(LocalStorageKey.EthAuthSettings, { + expiry, + app, + origin: origin || location.origin, + nonce + }) + }, [theme, ethAuth]) + + useEffect(() => { + setDisplayedAssets(displayedAssets) + }, [displayedAssetsSetting]) + + const { isEmailConflictOpen, emailConflictInfo, toggleEmailConflictModal } = useEmailConflict() + + const [isSocialLinkOpen, setIsSocialLinkOpen] = useState(false) + + return ( + + + + + {}, openConnectModalState: false }} + > + + + + +
+ + + {}} emailConflictInfo={emailConflictInfo} isInline {...props} /> + +
+ + + + {pendingRequestConfirmation && ( + { + rejectPendingRequest('') + }} + > +
+
+ + +

+ Confirm{' '} + {pendingRequestConfirmation.type === 'signMessage' ? 'signing message' : 'transaction'} +

+
+
+ + {pendingRequestConfirmation.type === 'signMessage' && pendingRequestConfirmation.message && ( +
+ + Message + + + + {isJSON(pendingRequestConfirmation.message) ? ( + + ) : ( + hexToString(pendingRequestConfirmation.message as unknown as Hex) + )} + + +
+ )} + + {pendingRequestConfirmation.type === 'signTransaction' && ( + + )} + + {pendingRequestConfirmation.chainId && ( +
+
+ + Network + +
+
+ +
+
+ )} + +
+
+
+ +
+ +
+
+
+ )} + + {isEmailConflictOpen && emailConflictInfo && ( + { + toggleEmailConflictModal(false) + }} + > +
+ + Email already in use + +
+ + Another account with this email address{' '} + ({emailConflictInfo.email}) already exists with account type{' '} + ({emailConflictInfo.type}). Please sign in again with the correct + account. + +
+
+
+
+
+ )} + + {isSocialLinkOpen && + (waasConnector ? ( + setIsSocialLinkOpen(false)}> + + + ) : ( + setIsSocialLinkOpen(false)}> + + Social link is not supported for universal wallets (works only for embedded wallets) + + + ))} +
+
+
+ {children} +
+
+
+
+
+
+
+
+ ) +} diff --git a/packages/connect/src/components/SequenceConnectInlineProvider/index.ts b/packages/connect/src/components/SequenceConnectInlineProvider/index.ts new file mode 100644 index 000000000..cf2d45c71 --- /dev/null +++ b/packages/connect/src/components/SequenceConnectInlineProvider/index.ts @@ -0,0 +1 @@ +export * from './SequenceConnectInlineProvider.js' diff --git a/packages/connect/src/components/SequenceConnectPreview/index.ts b/packages/connect/src/components/SequenceConnectPreview/index.ts deleted file mode 100644 index 23f0c99fb..000000000 --- a/packages/connect/src/components/SequenceConnectPreview/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SequenceConnectPreview.js' diff --git a/packages/connect/src/components/SequenceConnectPreviewProvider/SequenceConnectPreviewProvider.tsx b/packages/connect/src/components/SequenceConnectPreviewProvider/SequenceConnectPreviewProvider.tsx deleted file mode 100644 index 0e0897966..000000000 --- a/packages/connect/src/components/SequenceConnectPreviewProvider/SequenceConnectPreviewProvider.tsx +++ /dev/null @@ -1,87 +0,0 @@ -'use client' - -import { ThemeProvider, type Theme } from '@0xsequence/design-system' -import { GoogleOAuthProvider } from '@react-oauth/google' -import { useState, type ReactNode } from 'react' -import { useConfig } from 'wagmi' - -import { AnalyticsContextProvider } from '../../contexts/Analytics.js' -import { ConnectConfigContextProvider } from '../../contexts/ConnectConfig.js' -import { ThemeContextProvider } from '../../contexts/Theme.js' -import { WalletConfigContextProvider } from '../../contexts/WalletConfig.js' -import { useEmailConflict } from '../../hooks/useWaasEmailConflict.js' -import { type ConnectConfig, type DisplayedAsset, type ExtendedConnector, type ModalPosition } from '../../types.js' -import { Connect } from '../Connect/Connect.js' - -export type SequenceConnectProviderProps = { - children: ReactNode - config: ConnectConfig -} - -/** - * @internal - * Preview version of SequenceConnectProvider component. - * This component should only be used for testing purposes. - * It provides the same functionality as SequenceConnectProvider but only for preview purposes. - */ -export const SequenceConnectPreviewProvider = (props: SequenceConnectProviderProps) => { - const { config, children } = props - - const { - defaultTheme = 'dark', - position = 'center', - displayedAssets: displayedAssetsSetting = [], - readOnlyNetworks, - hideExternalConnectOptions = false, - hideConnectedWallets = false, - hideSocialConnectOptions = false - } = config - - const [theme, setTheme] = useState>(defaultTheme || 'dark') - const [modalPosition, setModalPosition] = useState(position) - const [displayedAssets, setDisplayedAssets] = useState(displayedAssetsSetting) - - const wagmiConfig = useConfig() - - const googleWaasConnector = wagmiConfig.connectors.find( - c => c.id === 'sequence-waas' && (c as ExtendedConnector)._wallet.id === 'google-waas' - ) as ExtendedConnector | undefined - const googleClientId: string = (googleWaasConnector as any)?.params?.googleClientId || '' - - const { emailConflictInfo } = useEmailConflict() - - return ( - - - {}, analytics: undefined }}> - - -
- - {}} emailConflictInfo={emailConflictInfo} isPreview {...props} /> - -
- {children} -
-
-
-
-
- ) -} diff --git a/packages/connect/src/components/SequenceConnectPreviewProvider/index.ts b/packages/connect/src/components/SequenceConnectPreviewProvider/index.ts deleted file mode 100644 index 985376829..000000000 --- a/packages/connect/src/components/SequenceConnectPreviewProvider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SequenceConnectPreviewProvider.js' diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts index 07ddd4850..2c1b33c3d 100644 --- a/packages/connect/src/index.ts +++ b/packages/connect/src/index.ts @@ -1,9 +1,9 @@ export { SequenceConnect } from './components/SequenceConnect/index.js' -export { SequenceConnectPreview } from './components/SequenceConnectPreview/index.js' +export { SequenceConnectInline } from './components/SequenceConnectInline/index.js' // Provider export { SequenceConnectProvider } from './components/SequenceConnectProvider/index.js' -export { SequenceConnectPreviewProvider } from './components/SequenceConnectPreviewProvider/index.js' +export { SequenceConnectInlineProvider } from './components/SequenceConnectInlineProvider/index.js' // Types export type { @@ -21,7 +21,7 @@ export type { } from './types.js' // Config -export { createConfig, type CreateConfigOptions } from './config/createConfig.js' +export { createConfig, type CreateConfigOptions, type SequenceConnectConfig } from './config/createConfig.js' export { getDefaultConnectors, getDefaultUniversalConnectors, diff --git a/packages/connect/src/types.ts b/packages/connect/src/types.ts index 1e57bc0ab..0c39cd171 100644 --- a/packages/connect/src/types.ts +++ b/packages/connect/src/types.ts @@ -85,6 +85,8 @@ export interface ConnectConfig { hideConnectedWallets?: boolean customCSS?: string embeddedWalletTitle?: string + renderInline?: boolean + onConnectSuccess?: (address: string) => void } export type StorageItem = {