Skip to content

Commit 341a1b2

Browse files
authored
Merge pull request #303 from phantom/chore/update-connect-ui
chore: Minors improvement ui
2 parents 05060dd + bf5ed03 commit 341a1b2

File tree

19 files changed

+725
-973
lines changed

19 files changed

+725
-973
lines changed

examples/with-react-ui/src/App.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const config: PhantomSDKConfig = {
99

1010
function App() {
1111
return (
12-
<PhantomProvider theme="light" config={config}>
12+
<PhantomProvider config={config}>
1313
<div
1414
style={{
1515
minHeight: "100vh",
@@ -75,6 +75,31 @@ function App() {
7575
mobile app via phantom.app/ul
7676
</p>
7777
</div>
78+
79+
<ConnectExample />
80+
81+
<div
82+
style={{
83+
marginTop: "3rem",
84+
padding: "1.5rem",
85+
background: "white",
86+
borderRadius: "12px",
87+
border: "1px solid #e5e7eb",
88+
textAlign: "center",
89+
}}
90+
>
91+
<h3 style={{ margin: "0 0 1rem 0", color: "#1f2937" }}>📱 Mobile Testing</h3>
92+
<p
93+
style={{
94+
color: "#6b7280",
95+
margin: "0",
96+
lineHeight: "1.5",
97+
}}
98+
>
99+
On mobile devices, you'll see an additional "Open in Phantom App" button that will redirect to the Phantom
100+
mobile app via phantom.app/ul
101+
</p>
102+
</div>
78103
</div>
79104
</PhantomProvider>
80105
);

packages/browser-sdk/src/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,15 @@ interface ExtendedInjectedProviderConfig extends InjectedProviderConfig {
4949
embeddedWalletType?: never;
5050
}
5151

52+
type AuthProviderType = EmbeddedProviderAuthType | "injected";
53+
5254
type AuthOptions = {
53-
provider: EmbeddedProviderAuthType | "injected";
55+
provider: AuthProviderType;
5456
customAuthData?: Record<string, any>;
5557
};
5658

5759
type ConnectResult = Omit<EmbeddedConnectResult, "authProvider"> & {
58-
authProvider?: EmbeddedProviderAuthType | "injected" | undefined;
60+
authProvider?: AuthProviderType | undefined;
5961
};
6062

6163
// Re-export types from core for convenience
@@ -67,6 +69,7 @@ export type {
6769
SignMessageResult,
6870
SignedTransaction,
6971
AuthOptions,
72+
AuthProviderType,
7073
DebugCallback,
7174
DebugLevel,
7275
};

packages/react-sdk/src/index.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,7 @@ export * from "./types";
1010

1111
// Re-export useful types and utilities from browser-sdk
1212
export { NetworkId, AddressType, DebugLevel, debug } from "@phantom/browser-sdk";
13-
export type {
14-
DebugMessage,
15-
AutoConfirmEnableParams,
16-
AutoConfirmResult,
17-
AutoConfirmSupportedChainsResult,
18-
} from "@phantom/browser-sdk";
1913

20-
// Re-export event types for typed event handlers
2114
export type {
2215
EmbeddedProviderEvent,
2316
ConnectEventData,
@@ -26,6 +19,11 @@ export type {
2619
DisconnectEventData,
2720
EmbeddedProviderEventMap,
2821
EventCallback,
22+
DebugMessage,
23+
AutoConfirmEnableParams,
24+
AutoConfirmResult,
25+
AutoConfirmSupportedChainsResult,
26+
AuthOptions,
2927
} from "@phantom/browser-sdk";
3028

3129
// Re-export chain interfaces

packages/react-ui/README.md

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -160,57 +160,62 @@ function ConnectButton() {
160160

161161
## Theming
162162

163-
Customize the modal appearance using CSS variables:
164-
165-
```css
166-
:root {
167-
/* Modal */
168-
--phantom-ui-modal-bg: #ffffff;
169-
--phantom-ui-modal-overlay: rgba(0, 0, 0, 0.5);
170-
--phantom-ui-modal-border-radius: 12px;
171-
172-
/* Buttons */
173-
--phantom-ui-button-bg: #ab9ff2;
174-
--phantom-ui-button-hover-bg: #9c8dff;
175-
--phantom-ui-button-text: #ffffff;
176-
--phantom-ui-button-border-radius: 8px;
177-
178-
/* Text */
179-
--phantom-ui-text-primary: #212529;
180-
--phantom-ui-text-secondary: #6c757d;
181-
182-
/* Spacing */
183-
--phantom-ui-spacing-sm: 8px;
184-
--phantom-ui-spacing-md: 16px;
185-
--phantom-ui-spacing-lg: 24px;
186-
}
187-
```
163+
Customize the modal appearance by passing a theme object to the `PhantomProvider`. The package includes two built-in themes: `darkTheme` (default) and `lightTheme`.
188164

189-
### Dark Theme
165+
### Using Built-in Themes
190166

191-
```css
192-
[data-theme="dark"] {
193-
--phantom-ui-modal-bg: #2d2d2d;
194-
--phantom-ui-text-primary: #ffffff;
195-
--phantom-ui-text-secondary: #b3b3b3;
196-
}
197-
```
167+
```tsx
168+
import { PhantomProvider, darkTheme, lightTheme } from "@phantom/react-ui";
198169

199-
Apply themes via the `theme` prop or CSS:
170+
// Use dark theme (default)
171+
<PhantomProvider config={config} theme={darkTheme}>
172+
<App />
173+
</PhantomProvider>
200174

201-
```tsx
202-
<PhantomProvider theme="dark">
175+
// Use light theme
176+
<PhantomProvider config={config} theme={lightTheme}>
203177
<App />
204178
</PhantomProvider>
179+
```
180+
181+
### Custom Theme
205182

206-
// Or via CSS
207-
<div data-theme="dark">
208-
<PhantomProvider>
209-
<App />
210-
</PhantomProvider>
211-
</div>
183+
You can pass a partial theme object to customize specific properties:
184+
185+
```tsx
186+
import { PhantomProvider } from "@phantom/react-ui";
187+
188+
const customTheme = {
189+
background: "#1a1a1a",
190+
text: "#ffffff",
191+
secondary: "#98979C",
192+
brand: "#ab9ff2",
193+
error: "#ff4444",
194+
success: "#00ff00",
195+
borderRadius: "16px",
196+
overlay: "rgba(0, 0, 0, 0.8)",
197+
};
198+
199+
<PhantomProvider config={config} theme={customTheme}>
200+
<App />
201+
</PhantomProvider>;
212202
```
213203

204+
### Theme Properties
205+
206+
| Property | Type | Description |
207+
| -------------- | -------- | --------------------------------------------------------- |
208+
| `background` | `string` | Background color for modal |
209+
| `text` | `string` | Primary text color |
210+
| `secondary` | `string` | Secondary color for text, borders, dividers (must be hex) |
211+
| `brand` | `string` | Brand/primary action color |
212+
| `error` | `string` | Error state color |
213+
| `success` | `string` | Success state color |
214+
| `borderRadius` | `string` | Border radius for buttons and modal |
215+
| `overlay` | `string` | Overlay background color (with opacity) |
216+
217+
**Note:** The `secondary` color must be a hex color value (e.g., `#98979C`) as it's used to derive auxiliary colors with opacity.
218+
214219
## Migration from @phantom/react-sdk
215220

216221
Migration is simple - just add the UI provider and import `useConnect` from `@phantom/react-ui`:
Lines changed: 20 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,36 @@
1-
import React, { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from "react";
1+
import React, { useState, useCallback, useMemo, type ReactNode } from "react";
22
import {
33
useConnect as useBaseConnect,
44
usePhantom,
55
PhantomProvider as BasePhantomProvider,
6-
useIsExtensionInstalled,
7-
useIsPhantomLoginAvailable,
86
type PhantomSDKConfig,
97
} from "@phantom/react-sdk";
10-
import { isMobileDevice, getDeeplinkToPhantom } from "@phantom/browser-sdk";
8+
import { isMobileDevice, getDeeplinkToPhantom, type AuthProviderType } from "@phantom/browser-sdk";
9+
import { darkTheme, mergeTheme, type PhantomTheme } from "./themes";
10+
import { Modal } from "./components/Modal";
11+
import { PhantomUIContext, type PhantomUIContextValue, type ConnectionUIState } from "./context";
1112

1213
export interface PhantomUIProviderProps {
1314
children: ReactNode;
14-
theme?: "light" | "dark" | "auto";
15-
customTheme?: Record<string, string>;
15+
theme?: Partial<PhantomTheme>;
1616
config: PhantomSDKConfig;
17+
appIcon?: string; // URL to app icon
18+
appName?: string; // App name to display
1719
}
1820

19-
// Connection UI state
20-
interface ConnectionUIState {
21-
isVisible: boolean;
22-
isConnecting: boolean;
23-
error: Error | null;
24-
providerType: "injected" | "embedded" | "deeplink" | null;
25-
}
26-
27-
interface PhantomUIContextValue {
28-
// Connection state
29-
connectionState: ConnectionUIState;
30-
showConnectionModal: () => void;
31-
hideConnectionModal: () => void;
32-
connectWithAuthProvider: (provider: "google" | "apple" | "phantom") => Promise<void>;
33-
connectWithInjected: () => Promise<void>;
34-
connectWithDeeplink: () => void;
35-
isMobile: boolean;
36-
}
37-
38-
const PhantomUIContext = createContext<PhantomUIContextValue | null>(null);
39-
4021
// Internal UI Provider that consumes react-sdk context
41-
function PhantomUIProvider({ children, theme = "light", customTheme }: Omit<PhantomUIProviderProps, "config">) {
22+
function PhantomUIProvider({ children, theme = darkTheme, appIcon, appName }: Omit<PhantomUIProviderProps, "config">) {
4223
const baseConnect = useBaseConnect();
4324
const { sdk } = usePhantom();
44-
const isExtensionInstalled = useIsExtensionInstalled();
45-
const isPhantomLoginAvailable = useIsPhantomLoginAvailable();
4625

4726
// Check if this is a mobile device
4827
const isMobile = useMemo(() => isMobileDevice(), []);
4928

29+
// Get the resolved theme object
30+
const resolvedTheme = useMemo(() => {
31+
return mergeTheme(theme);
32+
}, [theme]);
33+
5034
// Connection state
5135
const [connectionState, setConnectionState] = useState<ConnectionUIState>({
5236
isVisible: false,
@@ -76,13 +60,13 @@ function PhantomUIProvider({ children, theme = "light", customTheme }: Omit<Phan
7660

7761
// Connect with specific auth provider
7862
const connectWithAuthProvider = useCallback(
79-
async (provider: "google" | "apple" | "phantom") => {
63+
async (provider: AuthProviderType) => {
8064
try {
8165
setConnectionState(prev => ({
8266
...prev,
8367
isConnecting: true,
8468
error: null,
85-
providerType: "embedded", // Always embedded when using modal
69+
providerType: provider,
8670
}));
8771

8872
await baseConnect.connect({ provider });
@@ -186,109 +170,24 @@ function PhantomUIProvider({ children, theme = "light", customTheme }: Omit<Phan
186170
connectWithInjected,
187171
connectWithDeeplink,
188172
isMobile,
173+
theme: resolvedTheme,
189174
};
190175

191176
return (
192177
<PhantomUIContext.Provider value={contextValue}>
193178
{children}
194-
{/* Connection Modal - rendered conditionally based on state */}
195-
{connectionState.isVisible && (
196-
<div className={`phantom-ui-modal-overlay ${theme}`} style={customTheme} onClick={hideConnectionModal}>
197-
<div className="phantom-ui-modal-content" onClick={e => e.stopPropagation()}>
198-
<div className="phantom-ui-modal-header">
199-
<h3>Connect to Phantom</h3>
200-
<button className="phantom-ui-close-button" onClick={hideConnectionModal}>
201-
×
202-
</button>
203-
</div>
204-
205-
<div className="phantom-ui-modal-body">
206-
{connectionState.error && <div className="phantom-ui-error">{connectionState.error.message}</div>}
207-
208-
<div className="phantom-ui-provider-options">
209-
{/* Mobile device with no Phantom extension - show deeplink button */}
210-
{isMobile && !isExtensionInstalled.isInstalled && (
211-
<button
212-
className="phantom-ui-provider-button phantom-ui-provider-button-mobile"
213-
onClick={connectWithDeeplink}
214-
disabled={connectionState.isConnecting}
215-
>
216-
{connectionState.isConnecting && connectionState.providerType === "deeplink"
217-
? "Opening Phantom..."
218-
: "Open in Phantom App"}
219-
</button>
220-
)}
221-
222-
{/* Primary auth options - Phantom, Google */}
223-
{!isMobile && (
224-
<>
225-
{/* Login with Phantom (embedded provider using Phantom extension) */}
226-
{isPhantomLoginAvailable.isAvailable && (
227-
<button
228-
className="phantom-ui-provider-button phantom-ui-provider-button-primary"
229-
onClick={() => connectWithAuthProvider("phantom")}
230-
disabled={connectionState.isConnecting}
231-
>
232-
{connectionState.isConnecting && connectionState.providerType === "embedded"
233-
? "Connecting..."
234-
: "Login with Phantom"}
235-
</button>
236-
)}
237-
238-
{/* Continue with Google */}
239-
<button
240-
className="phantom-ui-provider-button"
241-
onClick={() => connectWithAuthProvider("google")}
242-
disabled={connectionState.isConnecting}
243-
>
244-
{connectionState.isConnecting && connectionState.providerType === "embedded"
245-
? "Connecting..."
246-
: "Continue with Google"}
247-
</button>
248-
</>
249-
)}
250-
251-
{/* Extension option - smaller UI section */}
252-
{!isMobile && isExtensionInstalled.isInstalled && (
253-
<div className="phantom-ui-extension-section">
254-
<div className="phantom-ui-divider">
255-
<span>or</span>
256-
</div>
257-
<button
258-
className="phantom-ui-provider-button phantom-ui-provider-button-secondary"
259-
onClick={connectWithInjected}
260-
disabled={connectionState.isConnecting}
261-
>
262-
{connectionState.isConnecting && connectionState.providerType === "injected"
263-
? "Connecting..."
264-
: "Continue with extension"}
265-
</button>
266-
</div>
267-
)}
268-
</div>
269-
</div>
270-
</div>
271-
</div>
272-
)}
179+
<Modal appIcon={appIcon} appName={appName} />
273180
</PhantomUIContext.Provider>
274181
);
275182
}
276183

277184
// Main exported Provider that wraps both react-sdk and react-ui providers
278-
export function PhantomProvider({ children, theme = "light", customTheme, config }: PhantomUIProviderProps) {
185+
export function PhantomProvider({ children, theme = darkTheme, config, appIcon, appName }: PhantomUIProviderProps) {
279186
return (
280187
<BasePhantomProvider config={config}>
281-
<PhantomUIProvider theme={theme} customTheme={customTheme}>
188+
<PhantomUIProvider theme={theme} appIcon={appIcon} appName={appName}>
282189
{children}
283190
</PhantomUIProvider>
284191
</BasePhantomProvider>
285192
);
286193
}
287-
288-
export function usePhantomUI(): PhantomUIContextValue {
289-
const context = useContext(PhantomUIContext);
290-
if (!context) {
291-
throw new Error("usePhantomUI must be used within a PhantomProvider");
292-
}
293-
return context;
294-
}

0 commit comments

Comments
 (0)