@dubsdotapp/expo 0.2.22 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +176 -69
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +160 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/managed-wallet.tsx +24 -6
- package/src/storage.ts +1 -0
- package/src/wallet/phantom-deeplink/deeplink-handler.ts +10 -1
- package/src/wallet/phantom-deeplink/phantom-deeplink-adapter.ts +115 -0
package/package.json
CHANGED
package/src/managed-wallet.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import { MwaWalletAdapter } from './wallet/mwa-adapter';
|
|
|
4
4
|
import { PhantomDeeplinkAdapter } from './wallet/phantom-deeplink';
|
|
5
5
|
import type { PhantomSession } from './wallet/phantom-deeplink';
|
|
6
6
|
import type { WalletAdapter } from './wallet/types';
|
|
7
|
-
import { isSolanaSeeker } from './utils/device';
|
|
8
7
|
import { ConnectWalletScreen } from './ui/ConnectWalletScreen';
|
|
9
8
|
import type { ConnectWalletScreenProps } from './ui/ConnectWalletScreen';
|
|
10
9
|
import type { TokenStorage } from './storage';
|
|
@@ -29,6 +28,7 @@ function getOrCreatePhantomAdapter(config: {
|
|
|
29
28
|
redirectUri: config.redirectUri,
|
|
30
29
|
appUrl: config.appUrl,
|
|
31
30
|
cluster: config.cluster,
|
|
31
|
+
storage: config.storage,
|
|
32
32
|
onSessionChange: (session) => {
|
|
33
33
|
if (session) {
|
|
34
34
|
console.log(TAG, 'Phantom session changed — saving to storage, wallet:', session.walletPublicKey);
|
|
@@ -95,12 +95,11 @@ export function ManagedWalletProvider({
|
|
|
95
95
|
const [error, setError] = useState<string | null>(null);
|
|
96
96
|
|
|
97
97
|
// Determine which adapter to use:
|
|
98
|
-
// -
|
|
99
|
-
// -
|
|
100
|
-
const
|
|
101
|
-
const usePhantom = !seeker && !!redirectUri;
|
|
98
|
+
// - Android (all devices) → MWA (Phantom + other wallets support Mobile Wallet Adapter natively)
|
|
99
|
+
// - iOS → Phantom deeplinks (MWA not available on iOS)
|
|
100
|
+
const usePhantom = Platform.OS === 'ios' && !!redirectUri;
|
|
102
101
|
|
|
103
|
-
console.log(TAG, `Platform: ${Platform.OS},
|
|
102
|
+
console.log(TAG, `Platform: ${Platform.OS}, redirectUri: ${redirectUri ? 'provided' : 'not set'}, usePhantom: ${usePhantom}`);
|
|
104
103
|
|
|
105
104
|
const adapterRef = useRef<WalletAdapter | null>(null);
|
|
106
105
|
const transactRef = useRef<any>(null);
|
|
@@ -158,6 +157,24 @@ export function ManagedWalletProvider({
|
|
|
158
157
|
return;
|
|
159
158
|
}
|
|
160
159
|
|
|
160
|
+
// Check for cold-start recovery (Android killed the app during connect)
|
|
161
|
+
const coldStartUrl = phantom.consumeColdStartUrl();
|
|
162
|
+
if (coldStartUrl) {
|
|
163
|
+
try {
|
|
164
|
+
console.log(TAG, 'Cold-start URL detected, attempting recovery');
|
|
165
|
+
await phantom.completeConnectFromColdStart(coldStartUrl);
|
|
166
|
+
if (!cancelled) {
|
|
167
|
+
console.log(TAG, 'Cold-start recovery succeeded');
|
|
168
|
+
setConnected(true);
|
|
169
|
+
setIsReady(true);
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.log(TAG, 'Cold-start recovery failed:', err instanceof Error ? err.message : err);
|
|
174
|
+
// Fall through to normal session restore
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
161
178
|
try {
|
|
162
179
|
const savedSession = await storage.getItem(STORAGE_KEYS.PHANTOM_SESSION);
|
|
163
180
|
if (savedSession && !cancelled) {
|
|
@@ -250,6 +267,7 @@ export function ManagedWalletProvider({
|
|
|
250
267
|
await storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {});
|
|
251
268
|
await storage.deleteItem(STORAGE_KEYS.PHANTOM_SESSION).catch(() => {});
|
|
252
269
|
await storage.deleteItem(STORAGE_KEYS.JWT_TOKEN).catch(() => {});
|
|
270
|
+
await storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {});
|
|
253
271
|
setConnected(false);
|
|
254
272
|
console.log(TAG, 'disconnect() — done');
|
|
255
273
|
}, [adapter, storage, usePhantom]);
|
package/src/storage.ts
CHANGED
|
@@ -23,6 +23,7 @@ export class DeeplinkHandler {
|
|
|
23
23
|
private readonly redirectBase: string;
|
|
24
24
|
private readonly pending = new Map<string, PendingRequest>();
|
|
25
25
|
private subscription: ReturnType<typeof Linking.addEventListener> | null = null;
|
|
26
|
+
private _coldStartUrl: string | null = null;
|
|
26
27
|
|
|
27
28
|
constructor(redirectBase: string) {
|
|
28
29
|
// Strip trailing slashes for consistent matching
|
|
@@ -163,10 +164,18 @@ export class DeeplinkHandler {
|
|
|
163
164
|
this.pending.delete(id);
|
|
164
165
|
req.resolve({ params });
|
|
165
166
|
} else {
|
|
166
|
-
console.log(TAG, 'No pending requests to resolve —
|
|
167
|
+
console.log(TAG, 'No pending requests to resolve — stashing as cold-start URL');
|
|
168
|
+
this._coldStartUrl = url;
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
/** Return and clear the stashed cold-start URL (if any). */
|
|
173
|
+
getColdStartUrl(): string | null {
|
|
174
|
+
const url = this._coldStartUrl;
|
|
175
|
+
this._coldStartUrl = null;
|
|
176
|
+
return url;
|
|
177
|
+
}
|
|
178
|
+
|
|
170
179
|
/** Extract the request ID from the dubs_rid query parameter. */
|
|
171
180
|
private extractRequestId(url: string): string | null {
|
|
172
181
|
try {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { PublicKey, Transaction } from '@solana/web3.js';
|
|
2
2
|
import bs58 from 'bs58';
|
|
3
3
|
import type { WalletAdapter } from '../types';
|
|
4
|
+
import type { TokenStorage } from '../../storage';
|
|
5
|
+
import { STORAGE_KEYS } from '../../storage';
|
|
4
6
|
import {
|
|
5
7
|
generateKeyPair,
|
|
6
8
|
deriveSharedSecret,
|
|
@@ -38,6 +40,8 @@ export interface PhantomDeeplinkAdapterConfig {
|
|
|
38
40
|
timeout?: number;
|
|
39
41
|
/** Called when the Phantom session changes (save/clear for persistence) */
|
|
40
42
|
onSessionChange?: (session: PhantomSession | null) => void;
|
|
43
|
+
/** Storage for persisting in-flight connect state (cold-start recovery on Android) */
|
|
44
|
+
storage?: TokenStorage;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
let requestCounter = 0;
|
|
@@ -147,6 +151,20 @@ export class PhantomDeeplinkAdapter implements WalletAdapter {
|
|
|
147
151
|
app_url: appUrl,
|
|
148
152
|
});
|
|
149
153
|
|
|
154
|
+
// Persist in-flight state so we can recover if the OS kills the app (Android cold-start)
|
|
155
|
+
if (this.config.storage) {
|
|
156
|
+
console.log(TAG, 'Saving in-flight connect state to storage');
|
|
157
|
+
await this.config.storage.setItem(
|
|
158
|
+
STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT,
|
|
159
|
+
JSON.stringify({
|
|
160
|
+
dappPublicKey: dappPubBase58,
|
|
161
|
+
dappSecretKey: bs58.encode(this._dappKeyPair.secretKey),
|
|
162
|
+
requestId,
|
|
163
|
+
createdAt: Date.now(),
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
150
168
|
const url = `https://phantom.app/ul/v1/connect?${params.toString()}`;
|
|
151
169
|
console.log(TAG, 'Opening Phantom connect deeplink...');
|
|
152
170
|
const response = await this.handler.send(url, requestId, this.timeout);
|
|
@@ -183,6 +201,9 @@ export class PhantomDeeplinkAdapter implements WalletAdapter {
|
|
|
183
201
|
|
|
184
202
|
// Notify consumer of new session
|
|
185
203
|
this.config.onSessionChange?.(this.getSession());
|
|
204
|
+
|
|
205
|
+
// Clear in-flight state now that connect succeeded
|
|
206
|
+
this.config.storage?.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {});
|
|
186
207
|
}
|
|
187
208
|
|
|
188
209
|
disconnect(): void {
|
|
@@ -197,6 +218,100 @@ export class PhantomDeeplinkAdapter implements WalletAdapter {
|
|
|
197
218
|
console.log(TAG, 'Disconnected');
|
|
198
219
|
}
|
|
199
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Return and clear the stashed cold-start URL from the deeplink handler.
|
|
223
|
+
* Used by ManagedWalletProvider to detect an in-flight connect on cold start.
|
|
224
|
+
*/
|
|
225
|
+
consumeColdStartUrl(): string | null {
|
|
226
|
+
return this.handler.getColdStartUrl();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Complete a connect flow that was interrupted by the OS killing the app (Android).
|
|
231
|
+
* Loads the saved in-flight keypair from storage, decrypts the Phantom response
|
|
232
|
+
* from the cold-start URL, and restores the session.
|
|
233
|
+
*/
|
|
234
|
+
async completeConnectFromColdStart(url: string): Promise<void> {
|
|
235
|
+
if (!this.config.storage) {
|
|
236
|
+
throw new Error('Cannot recover cold-start connect: no storage configured');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log(TAG, 'completeConnectFromColdStart() — attempting recovery');
|
|
240
|
+
|
|
241
|
+
const raw = await this.config.storage.getItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT);
|
|
242
|
+
if (!raw) {
|
|
243
|
+
throw new Error('No in-flight connect state found in storage');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const inFlight = JSON.parse(raw) as {
|
|
247
|
+
dappPublicKey: string;
|
|
248
|
+
dappSecretKey: string;
|
|
249
|
+
requestId: string;
|
|
250
|
+
createdAt: number;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Check expiry (120s)
|
|
254
|
+
const age = Date.now() - inFlight.createdAt;
|
|
255
|
+
if (age > 120_000) {
|
|
256
|
+
console.log(TAG, `In-flight state expired (${Math.round(age / 1000)}s old), clearing`);
|
|
257
|
+
await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {});
|
|
258
|
+
throw new Error('In-flight connect state expired');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(TAG, `In-flight state age: ${Math.round(age / 1000)}s, requestId: ${inFlight.requestId}`);
|
|
262
|
+
|
|
263
|
+
// Restore keypair from storage
|
|
264
|
+
this._dappKeyPair = {
|
|
265
|
+
publicKey: bs58.decode(inFlight.dappPublicKey),
|
|
266
|
+
secretKey: bs58.decode(inFlight.dappSecretKey),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Parse the Phantom callback URL
|
|
270
|
+
const parsed = new URL(url);
|
|
271
|
+
const params: Record<string, string> = {};
|
|
272
|
+
parsed.searchParams.forEach((value, key) => {
|
|
273
|
+
params[key] = value;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Check for Phantom error
|
|
277
|
+
if (params.errorCode) {
|
|
278
|
+
const errorMessage = params.errorMessage
|
|
279
|
+
? decodeURIComponent(params.errorMessage)
|
|
280
|
+
: `Phantom error code: ${params.errorCode}`;
|
|
281
|
+
await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {});
|
|
282
|
+
throw new Error(errorMessage);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const phantomPubBase58 = params.phantom_encryption_public_key;
|
|
286
|
+
if (!phantomPubBase58) {
|
|
287
|
+
await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {});
|
|
288
|
+
throw new Error('Phantom did not return an encryption public key');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log(TAG, 'Cold-start: Phantom public key:', phantomPubBase58);
|
|
292
|
+
|
|
293
|
+
this._phantomPublicKey = bs58.decode(phantomPubBase58);
|
|
294
|
+
this._sharedSecret = deriveSharedSecret(
|
|
295
|
+
this._dappKeyPair.secretKey,
|
|
296
|
+
this._phantomPublicKey,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const data = decryptPayload(params.data, params.nonce, this._sharedSecret);
|
|
300
|
+
console.log(TAG, 'Cold-start: Decrypted connect data — public_key:', data.public_key);
|
|
301
|
+
|
|
302
|
+
this._sessionToken = data.session;
|
|
303
|
+
this._publicKey = new PublicKey(data.public_key);
|
|
304
|
+
this._connected = true;
|
|
305
|
+
|
|
306
|
+
console.log(TAG, 'Cold-start recovery complete! Wallet:', this._publicKey.toBase58());
|
|
307
|
+
|
|
308
|
+
// Notify consumer of new session
|
|
309
|
+
this.config.onSessionChange?.(this.getSession());
|
|
310
|
+
|
|
311
|
+
// Clear in-flight state
|
|
312
|
+
await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {});
|
|
313
|
+
}
|
|
314
|
+
|
|
200
315
|
async signTransaction(transaction: Transaction): Promise<Transaction> {
|
|
201
316
|
this.assertConnected();
|
|
202
317
|
console.log(TAG, 'signTransaction() — serializing transaction');
|