Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
27 changes: 26 additions & 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 config={config}>
<div
style={{
minHeight: "100vh",
Expand Down Expand Up @@ -75,6 +75,31 @@ function App() {
mobile app via phantom.app/ul
</p>
</div>

<ConnectExample />

<div
style={{
marginTop: "3rem",
padding: "1.5rem",
background: "white",
borderRadius: "12px",
border: "1px solid #e5e7eb",
textAlign: "center",
}}
>
<h3 style={{ margin: "0 0 1rem 0", color: "#1f2937" }}>📱 Mobile Testing</h3>
<p
style={{
color: "#6b7280",
margin: "0",
lineHeight: "1.5",
}}
>
On mobile devices, you'll see an additional "Open in Phantom App" button that will redirect to the Phantom
mobile app via phantom.app/ul
</p>
</div>
</div>
</PhantomProvider>
);
Expand Down
7 changes: 5 additions & 2 deletions packages/browser-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ interface ExtendedInjectedProviderConfig extends InjectedProviderConfig {
embeddedWalletType?: never;
}

type AuthProviderType = EmbeddedProviderAuthType | "injected";

type AuthOptions = {
provider: EmbeddedProviderAuthType | "injected";
provider: AuthProviderType;
customAuthData?: Record<string, any>;
};

type ConnectResult = Omit<EmbeddedConnectResult, "authProvider"> & {
authProvider?: EmbeddedProviderAuthType | "injected" | undefined;
authProvider?: AuthProviderType | undefined;
};

// Re-export types from core for convenience
Expand All @@ -67,6 +69,7 @@ export type {
SignMessageResult,
SignedTransaction,
AuthOptions,
AuthProviderType,
DebugCallback,
DebugLevel,
};
Expand Down
12 changes: 5 additions & 7 deletions packages/react-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,7 @@ export * from "./types";

// Re-export useful types and utilities from browser-sdk
export { NetworkId, AddressType, DebugLevel, debug } from "@phantom/browser-sdk";
export type {
DebugMessage,
AutoConfirmEnableParams,
AutoConfirmResult,
AutoConfirmSupportedChainsResult,
} from "@phantom/browser-sdk";

// Re-export event types for typed event handlers
export type {
EmbeddedProviderEvent,
ConnectEventData,
Expand All @@ -26,6 +19,11 @@ export type {
DisconnectEventData,
EmbeddedProviderEventMap,
EventCallback,
DebugMessage,
AutoConfirmEnableParams,
AutoConfirmResult,
AutoConfirmSupportedChainsResult,
AuthOptions,
} from "@phantom/browser-sdk";

// Re-export chain interfaces
Expand Down
89 changes: 47 additions & 42 deletions packages/react-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,57 +141,62 @@ function ConnectButton() {

## Theming

Customize the modal appearance using CSS variables:

```css
:root {
/* Modal */
--phantom-ui-modal-bg: #ffffff;
--phantom-ui-modal-overlay: rgba(0, 0, 0, 0.5);
--phantom-ui-modal-border-radius: 12px;

/* Buttons */
--phantom-ui-button-bg: #ab9ff2;
--phantom-ui-button-hover-bg: #9c8dff;
--phantom-ui-button-text: #ffffff;
--phantom-ui-button-border-radius: 8px;

/* Text */
--phantom-ui-text-primary: #212529;
--phantom-ui-text-secondary: #6c757d;

/* Spacing */
--phantom-ui-spacing-sm: 8px;
--phantom-ui-spacing-md: 16px;
--phantom-ui-spacing-lg: 24px;
}
```
Customize the modal appearance by passing a theme object to the `PhantomProvider`. The package includes two built-in themes: `darkTheme` (default) and `lightTheme`.

### Dark Theme
### Using Built-in Themes

```css
[data-theme="dark"] {
--phantom-ui-modal-bg: #2d2d2d;
--phantom-ui-text-primary: #ffffff;
--phantom-ui-text-secondary: #b3b3b3;
}
```
```tsx
import { PhantomProvider, darkTheme, lightTheme } from "@phantom/react-ui";

Apply themes via the `theme` prop or CSS:
// Use dark theme (default)
<PhantomProvider config={config} theme={darkTheme}>
<App />
</PhantomProvider>

```tsx
<PhantomProvider theme="dark">
// Use light theme
<PhantomProvider config={config} theme={lightTheme}>
<App />
</PhantomProvider>
```

### Custom Theme

// Or via CSS
<div data-theme="dark">
<PhantomProvider>
<App />
</PhantomProvider>
</div>
You can pass a partial theme object to customize specific properties:

```tsx
import { PhantomProvider } from "@phantom/react-ui";

const customTheme = {
background: "#1a1a1a",
text: "#ffffff",
secondary: "#98979C",
brand: "#ab9ff2",
error: "#ff4444",
success: "#00ff00",
borderRadius: "16px",
overlay: "rgba(0, 0, 0, 0.8)",
};

<PhantomProvider config={config} theme={customTheme}>
<App />
</PhantomProvider>;
```

### Theme Properties

| Property | Type | Description |
| -------------- | -------- | --------------------------------------------------------- |
| `background` | `string` | Background color for modal |
| `text` | `string` | Primary text color |
| `secondary` | `string` | Secondary color for text, borders, dividers (must be hex) |
| `brand` | `string` | Brand/primary action color |
| `error` | `string` | Error state color |
| `success` | `string` | Success state color |
| `borderRadius` | `string` | Border radius for buttons and modal |
| `overlay` | `string` | Overlay background color (with opacity) |

**Note:** The `secondary` color must be a hex color value (e.g., `#98979C`) as it's used to derive auxiliary colors with opacity.

## Migration from @phantom/react-sdk

Migration is simple - just add the UI provider and import `useConnect` from `@phantom/react-ui`:
Expand Down
119 changes: 32 additions & 87 deletions packages/react-ui/src/PhantomProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import {
useIsPhantomLoginAvailable,
type PhantomSDKConfig,
} from "@phantom/react-sdk";
import { isMobileDevice, getDeeplinkToPhantom } from "@phantom/browser-sdk";
import { isMobileDevice, getDeeplinkToPhantom, type AuthProviderType } from "@phantom/browser-sdk";
import { darkTheme, mergeTheme, type PhantomTheme } from "./themes";
import { Modal } from "./components/Modal";

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

// Connection UI state
Expand All @@ -29,7 +32,7 @@ interface PhantomUIContextValue {
connectionState: ConnectionUIState;
showConnectionModal: () => void;
hideConnectionModal: () => void;
connectWithAuthProvider: (provider: "google" | "apple" | "phantom") => Promise<void>;
connectWithAuthProvider: (provider: AuthProviderType) => Promise<void>;
connectWithInjected: () => Promise<void>;
connectWithDeeplink: () => void;
isMobile: boolean;
Expand All @@ -38,7 +41,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 = darkTheme, appIcon, appName }: Omit<PhantomUIProviderProps, "config">) {
const baseConnect = useBaseConnect();
const { sdk, isPhantomAvailable: _isPhantomAvailable } = usePhantom();
const isExtensionInstalled = useIsExtensionInstalled();
Expand All @@ -47,6 +50,11 @@ 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(() => {
return mergeTheme(theme);
}, [theme]);

// Connection state
const [connectionState, setConnectionState] = useState<ConnectionUIState>({
isVisible: false,
Expand Down Expand Up @@ -76,7 +84,7 @@ function PhantomUIProvider({ children, theme = "light", customTheme }: Omit<Phan

// Connect with specific auth provider
const connectWithAuthProvider = useCallback(
async (provider: "google" | "apple" | "phantom") => {
async (provider: AuthProviderType) => {
try {
setConnectionState(prev => ({
...prev,
Expand Down Expand Up @@ -191,94 +199,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 = darkTheme, config, appIcon, appName }: PhantomUIProviderProps) {
return (
<BasePhantomProvider config={config}>
<PhantomUIProvider theme={theme} customTheme={customTheme}>
<PhantomUIProvider theme={theme} appIcon={appIcon} appName={appName}>
{children}
</PhantomUIProvider>
</BasePhantomProvider>
Expand Down
Loading