|
1 | | -import React, { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from "react"; |
| 1 | +import React, { useState, useCallback, useMemo, type ReactNode } from "react"; |
2 | 2 | import { |
3 | 3 | useConnect as useBaseConnect, |
4 | 4 | usePhantom, |
5 | 5 | PhantomProvider as BasePhantomProvider, |
6 | | - useIsExtensionInstalled, |
7 | | - useIsPhantomLoginAvailable, |
8 | 6 | type PhantomSDKConfig, |
9 | 7 | } 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"; |
11 | 12 |
|
12 | 13 | export interface PhantomUIProviderProps { |
13 | 14 | children: ReactNode; |
14 | | - theme?: "light" | "dark" | "auto"; |
15 | | - customTheme?: Record<string, string>; |
| 15 | + theme?: Partial<PhantomTheme>; |
16 | 16 | config: PhantomSDKConfig; |
| 17 | + appIcon?: string; // URL to app icon |
| 18 | + appName?: string; // App name to display |
17 | 19 | } |
18 | 20 |
|
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 | | - |
40 | 21 | // 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">) { |
42 | 23 | const baseConnect = useBaseConnect(); |
43 | 24 | const { sdk } = usePhantom(); |
44 | | - const isExtensionInstalled = useIsExtensionInstalled(); |
45 | | - const isPhantomLoginAvailable = useIsPhantomLoginAvailable(); |
46 | 25 |
|
47 | 26 | // Check if this is a mobile device |
48 | 27 | const isMobile = useMemo(() => isMobileDevice(), []); |
49 | 28 |
|
| 29 | + // Get the resolved theme object |
| 30 | + const resolvedTheme = useMemo(() => { |
| 31 | + return mergeTheme(theme); |
| 32 | + }, [theme]); |
| 33 | + |
50 | 34 | // Connection state |
51 | 35 | const [connectionState, setConnectionState] = useState<ConnectionUIState>({ |
52 | 36 | isVisible: false, |
@@ -76,13 +60,13 @@ function PhantomUIProvider({ children, theme = "light", customTheme }: Omit<Phan |
76 | 60 |
|
77 | 61 | // Connect with specific auth provider |
78 | 62 | const connectWithAuthProvider = useCallback( |
79 | | - async (provider: "google" | "apple" | "phantom") => { |
| 63 | + async (provider: AuthProviderType) => { |
80 | 64 | try { |
81 | 65 | setConnectionState(prev => ({ |
82 | 66 | ...prev, |
83 | 67 | isConnecting: true, |
84 | 68 | error: null, |
85 | | - providerType: "embedded", // Always embedded when using modal |
| 69 | + providerType: provider, |
86 | 70 | })); |
87 | 71 |
|
88 | 72 | await baseConnect.connect({ provider }); |
@@ -186,109 +170,24 @@ function PhantomUIProvider({ children, theme = "light", customTheme }: Omit<Phan |
186 | 170 | connectWithInjected, |
187 | 171 | connectWithDeeplink, |
188 | 172 | isMobile, |
| 173 | + theme: resolvedTheme, |
189 | 174 | }; |
190 | 175 |
|
191 | 176 | return ( |
192 | 177 | <PhantomUIContext.Provider value={contextValue}> |
193 | 178 | {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} /> |
273 | 180 | </PhantomUIContext.Provider> |
274 | 181 | ); |
275 | 182 | } |
276 | 183 |
|
277 | 184 | // 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) { |
279 | 186 | return ( |
280 | 187 | <BasePhantomProvider config={config}> |
281 | | - <PhantomUIProvider theme={theme} customTheme={customTheme}> |
| 188 | + <PhantomUIProvider theme={theme} appIcon={appIcon} appName={appName}> |
282 | 189 | {children} |
283 | 190 | </PhantomUIProvider> |
284 | 191 | </BasePhantomProvider> |
285 | 192 | ); |
286 | 193 | } |
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