Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion examples/with-react-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const config: PhantomSDKConfig = {

function App() {
return (
<PhantomProvider theme="light" config={config}>
<PhantomProvider theme="dark" config={config}>
<div style={{
minHeight: '100vh',
display: 'flex',
Expand Down
115 changes: 31 additions & 84 deletions packages/react-ui/src/PhantomProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import React, { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from "react";
import { useConnect as useBaseConnect, usePhantom, PhantomProvider as BasePhantomProvider, useIsExtensionInstalled, useIsPhantomLoginAvailable, type PhantomSDKConfig} from "@phantom/react-sdk";
import { isMobileDevice, getDeeplinkToPhantom } from "@phantom/browser-sdk";
import { getTheme, mergeTheme, type PhantomTheme } from "./themes";
import { Modal } from "./components/Modal";

export interface PhantomUIProviderProps {
children: ReactNode;
theme?: "light" | "dark" | "auto";
customTheme?: Record<string, string>;
theme?: "light" | "dark" | "auto" | PhantomTheme;
customTheme?: Partial<PhantomTheme>;
config: PhantomSDKConfig;
appIcon?: string; // URL to app icon
appName?: string; // App name to display
}

// Connection UI state
Expand All @@ -31,7 +35,7 @@ interface PhantomUIContextValue {
const PhantomUIContext = createContext<PhantomUIContextValue | null>(null);

// Internal UI Provider that consumes react-sdk context
function PhantomUIProvider({ children, theme = "light", customTheme }: Omit<PhantomUIProviderProps, 'config'>) {
function PhantomUIProvider({ children, theme = "dark", customTheme, appIcon, appName }: Omit<PhantomUIProviderProps, 'config'>) {
const baseConnect = useBaseConnect();
const { sdk, isPhantomAvailable: _isPhantomAvailable } = usePhantom();
const isExtensionInstalled = useIsExtensionInstalled();
Expand All @@ -40,6 +44,12 @@ function PhantomUIProvider({ children, theme = "light", customTheme }: Omit<Phan
// Check if this is a mobile device
const isMobile = useMemo(() => isMobileDevice(), []);

// Get the resolved theme object
const resolvedTheme = useMemo(() => {
const baseTheme = typeof theme === 'string' ? getTheme(theme) : theme;
return mergeTheme(baseTheme, customTheme);
}, [theme, customTheme]);

// Connection state
const [connectionState, setConnectionState] = useState<ConnectionUIState>({
isVisible: false,
Expand Down Expand Up @@ -185,94 +195,31 @@ function PhantomUIProvider({ children, theme = "light", customTheme }: Omit<Phan
return (
<PhantomUIContext.Provider value={contextValue}>
{children}
{/* Connection Modal - rendered conditionally based on state */}
{connectionState.isVisible && (
<div className={`phantom-ui-modal-overlay ${theme}`} style={customTheme} onClick={hideConnectionModal}>
<div className="phantom-ui-modal-content" onClick={e => e.stopPropagation()}>
<div className="phantom-ui-modal-header">
<h3>Connect to Phantom</h3>
<button className="phantom-ui-close-button" onClick={hideConnectionModal}>
×
</button>
</div>

<div className="phantom-ui-modal-body">
{connectionState.error && <div className="phantom-ui-error">{connectionState.error.message}</div>}

<div className="phantom-ui-provider-options">
{/* Mobile device with no Phantom extension - show deeplink button */}
{isMobile && !isExtensionInstalled.isInstalled && (
<button
className="phantom-ui-provider-button phantom-ui-provider-button-mobile"
onClick={connectWithDeeplink}
disabled={connectionState.isConnecting}
>
{connectionState.isConnecting && connectionState.providerType === "deeplink"
? "Opening Phantom..."
: "Open in Phantom App"}
</button>
)}

{/* Primary auth options - Phantom, Google */}
{!isMobile && (
<>
{/* Login with Phantom (embedded provider using Phantom extension) */}
{isPhantomLoginAvailable.isAvailable && (
<button
className="phantom-ui-provider-button phantom-ui-provider-button-primary"
onClick={() => connectWithAuthProvider("phantom")}
disabled={connectionState.isConnecting}
>
{connectionState.isConnecting && connectionState.providerType === "embedded"
? "Connecting..."
: "Login with Phantom"}
</button>
)}

{/* Continue with Google */}
<button
className="phantom-ui-provider-button"
onClick={() => connectWithAuthProvider("google")}
disabled={connectionState.isConnecting}
>
{connectionState.isConnecting && connectionState.providerType === "embedded"
? "Connecting..."
: "Continue with Google"}
</button>
</>
)}

{/* Extension option - smaller UI section */}
{!isMobile && isExtensionInstalled.isInstalled && (
<div className="phantom-ui-extension-section">
<div className="phantom-ui-divider">
<span>or</span>
</div>
<button
className="phantom-ui-provider-button phantom-ui-provider-button-secondary"
onClick={connectWithInjected}
disabled={connectionState.isConnecting}
>
{connectionState.isConnecting && connectionState.providerType === "injected"
? "Connecting..."
: "Continue with extension"}
</button>
</div>
)}
</div>
</div>
</div>
</div>
)}
<Modal
isVisible={connectionState.isVisible}
isConnecting={connectionState.isConnecting}
error={connectionState.error}
providerType={connectionState.providerType}
theme={resolvedTheme}
appIcon={appIcon}
appName={appName}
isMobile={isMobile}
isExtensionInstalled={isExtensionInstalled.isInstalled}
isPhantomLoginAvailable={isPhantomLoginAvailable.isAvailable}
onClose={hideConnectionModal}
onConnectWithDeeplink={connectWithDeeplink}
onConnectWithAuthProvider={connectWithAuthProvider}
onConnectWithInjected={connectWithInjected}
/>
</PhantomUIContext.Provider>
);
}

// Main exported Provider that wraps both react-sdk and react-ui providers
export function PhantomProvider({ children, theme = "light", customTheme, config }: PhantomUIProviderProps) {
export function PhantomProvider({ children, theme = "dark", customTheme, config, appIcon, appName }: PhantomUIProviderProps) {
return (
<BasePhantomProvider config={config}>
<PhantomUIProvider theme={theme} customTheme={customTheme}>
<PhantomUIProvider theme={theme} customTheme={customTheme} appIcon={appIcon} appName={appName}>
{children}
</PhantomUIProvider>
</BasePhantomProvider>
Expand Down
Loading