forked from solana-foundation/templates
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathphantom.ts
More file actions
241 lines (201 loc) · 7.84 KB
/
phantom.ts
File metadata and controls
241 lines (201 loc) · 7.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import { BrowserSDK, AddressType, ConnectResult, WalletAddress } from '@phantom/browser-sdk'
let sdk: BrowserSDK | null = null
let connectedAddress: string | null = null
export function initializeSDK(): BrowserSDK {
const appId = import.meta.env.VITE_PHANTOM_APP_ID
const redirectUrl = import.meta.env.VITE_REDIRECT_URL
if (!appId || appId === 'your-app-id-here') {
throw new Error(
'Missing Phantom App ID. Set VITE_PHANTOM_APP_ID in .env file. ' +
'Get your App ID at https://phantom.com/portal',
)
}
if (!redirectUrl) {
throw new Error('Missing redirect URL. Set VITE_REDIRECT_URL in .env file.')
}
sdk = new BrowserSDK({
providers: ['google', 'apple'],
addressTypes: [AddressType.solana],
appId: appId,
authOptions: {
redirectUrl: redirectUrl,
},
})
console.log('Phantom SDK initialized')
return sdk
}
// Get the SDK instance (must be initialized first)
export function getSDK(): BrowserSDK {
if (!sdk) {
throw new Error('SDK not initialized. Call initializeSDK() first.')
}
return sdk
}
// Trigger Phantom Connect authentication with selected social provider
export async function connect(provider: 'google' | 'apple' = 'google'): Promise<string> {
try {
const sdkInstance = getSDK()
console.log(`Starting authentication with ${provider}...`)
// Connect with social provider (triggers OAuth flow)
// Note: This will redirect the user to OAuth provider, so the promise
// may not resolve in the current page session
const result: ConnectResult = await sdkInstance.connect({ provider })
// Extract Solana address from result
if (result?.addresses && Array.isArray(result.addresses)) {
const solanaAddr = result.addresses.find((addr: WalletAddress) => addr.addressType === AddressType.solana)
const address = solanaAddr?.address
if (address && typeof address === 'string') {
connectedAddress = address
console.log('Connected:', address)
return address
}
}
// During OAuth flow, user gets redirected so no address is returned yet
// This is expected behavior, not an error - just throw to be caught
throw new Error('REDIRECT_IN_PROGRESS')
} catch (error) {
console.error('Connection error:', error)
// If this is a redirect-in-progress (expected OAuth flow), pass it through
if (error instanceof Error && error.message === 'REDIRECT_IN_PROGRESS') {
throw error
}
// For actual errors, provide helpful message
if (error instanceof Error) {
throw new Error(`Connection failed: ${error.message}`)
}
throw new Error('Failed to connect with Phantom. Please try again.')
}
}
// Disconnect from Phantom and clear session
export async function disconnect(): Promise<void> {
try {
const sdkInstance = getSDK()
await sdkInstance.disconnect()
connectedAddress = null
console.log('Logged out')
} catch (error) {
console.error('Logout failed:', error)
throw new Error('Failed to log out. Please try again.')
}
}
// Get the cached connected address
export function getAddress(): string | null {
return connectedAddress
}
// Check if user has an active session
export function isConnected(): boolean {
try {
const sdkInstance = getSDK()
return sdkInstance.isConnected()
} catch (error) {
return false
}
}
// Check for existing session on page load (auto-reconnect)
export async function checkExistingSession(): Promise<string | null> {
try {
const sdkInstance = getSDK()
// First check if this is an OAuth callback redirect
const urlParams = new URLSearchParams(window.location.search)
const isCallback =
urlParams.has('response_type') || urlParams.has('wallet_id') || urlParams.has('code') || urlParams.has('state')
if (isCallback) {
console.log('Processing OAuth callback...')
// Wait a bit for SDK to initialize the callback
await new Promise((resolve) => setTimeout(resolve, 1500))
// Call connect to complete the OAuth handshake and get session
try {
const result: ConnectResult = await sdkInstance.connect({ provider: 'phantom' })
// Extract Solana address from result
if (result?.addresses && Array.isArray(result.addresses)) {
const solanaAddr = result.addresses.find((addr: WalletAddress) => addr.addressType === AddressType.solana)
const address = solanaAddr?.address
if (address && typeof address === 'string') {
connectedAddress = address
console.log('OAuth callback completed, address:', address)
// Clean the URL
window.history.replaceState({}, document.title, window.location.pathname)
return address
}
}
} catch (err) {
console.error('OAuth callback failed:', err)
// Clean URL even on error
window.history.replaceState({}, document.title, window.location.pathname)
return null
}
}
// Not a callback - check if SDK reports an existing session
if (!sdkInstance.isConnected()) {
console.log('No active session')
return null
}
// If connected, retrieve the session data from SDK
console.log('Active session detected, retrieving address...')
try {
const result: ConnectResult = await sdkInstance.connect({ provider: 'phantom' })
// Extract Solana address from session
if (result?.addresses && Array.isArray(result.addresses)) {
const solanaAddr = result.addresses.find((addr: WalletAddress) => addr.addressType === AddressType.solana)
const address = solanaAddr?.address
if (address && typeof address === 'string') {
connectedAddress = address
console.log('Session restored:', address)
return address
}
}
} catch (err) {
console.error('Failed to retrieve session data:', err)
}
return null
} catch (error) {
console.error('Session check failed:', error)
return null
}
}
// Handle OAuth callback after Phantom redirects back to app
export async function handleAuthCallback(): Promise<string | null> {
try {
// Check for OAuth callback parameters in URL
const urlParams = new URLSearchParams(window.location.search)
const hasAuthParams =
urlParams.has('response_type') || urlParams.has('wallet_id') || urlParams.has('code') || urlParams.has('state')
if (!hasAuthParams) {
return null
}
console.log('Processing OAuth callback...')
const sdkInstance = getSDK()
// Wait for SDK to process the OAuth callback
await new Promise((resolve) => setTimeout(resolve, 1500))
try {
// Retrieve session data without showing UI
const result: ConnectResult = await sdkInstance.connect({ provider: 'phantom' })
// Extract Solana address from session
if (result?.addresses && Array.isArray(result.addresses)) {
const solanaAddr = result.addresses.find((addr: WalletAddress) => addr.addressType === AddressType.solana)
const address = solanaAddr?.address
if (address && typeof address === 'string') {
connectedAddress = address
console.log('OAuth session established')
// Clean URL parameters
window.history.replaceState({}, document.title, window.location.pathname)
return address
}
}
} catch (err) {
console.log('Session retrieval failed')
}
// Fallback: check if we have cached connection
if (isConnected() && connectedAddress) {
window.history.replaceState({}, document.title, window.location.pathname)
return connectedAddress
}
// Clean URL even if callback failed
window.history.replaceState({}, document.title, window.location.pathname)
return null
} catch (error) {
console.error('OAuth callback error:', error)
window.history.replaceState({}, document.title, window.location.pathname)
return null
}
}