@dubsdotapp/expo 0.2.13 → 0.2.14
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.js +44 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +44 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/managed-wallet.tsx +60 -34
package/package.json
CHANGED
package/src/managed-wallet.tsx
CHANGED
|
@@ -11,6 +11,41 @@ import { STORAGE_KEYS } from './storage';
|
|
|
11
11
|
|
|
12
12
|
const TAG = '[Dubs:ManagedWallet]';
|
|
13
13
|
|
|
14
|
+
// ── Module-level Phantom adapter singleton ──
|
|
15
|
+
// Persists across React remounts (e.g. when the app backgrounds to open Phantom).
|
|
16
|
+
// This prevents the Linking listener from being torn down mid-flow.
|
|
17
|
+
let phantomSingleton: PhantomDeeplinkAdapter | null = null;
|
|
18
|
+
|
|
19
|
+
function getOrCreatePhantomAdapter(config: {
|
|
20
|
+
redirectUri: string;
|
|
21
|
+
appUrl?: string;
|
|
22
|
+
cluster: string;
|
|
23
|
+
storage: TokenStorage;
|
|
24
|
+
}): PhantomDeeplinkAdapter {
|
|
25
|
+
if (!phantomSingleton) {
|
|
26
|
+
console.log(TAG, 'Creating PhantomDeeplinkAdapter (singleton)');
|
|
27
|
+
phantomSingleton = new PhantomDeeplinkAdapter({
|
|
28
|
+
redirectUri: config.redirectUri,
|
|
29
|
+
appUrl: config.appUrl,
|
|
30
|
+
cluster: config.cluster,
|
|
31
|
+
onSessionChange: (session) => {
|
|
32
|
+
if (session) {
|
|
33
|
+
console.log(TAG, 'Phantom session changed — saving to storage, wallet:', session.walletPublicKey);
|
|
34
|
+
config.storage.setItem(STORAGE_KEYS.PHANTOM_SESSION, JSON.stringify(session)).catch((err) => {
|
|
35
|
+
console.log(TAG, 'Failed to save Phantom session:', err);
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
console.log(TAG, 'Phantom session cleared — removing from storage');
|
|
39
|
+
config.storage.deleteItem(STORAGE_KEYS.PHANTOM_SESSION).catch((err) => {
|
|
40
|
+
console.log(TAG, 'Failed to delete Phantom session:', err);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return phantomSingleton;
|
|
47
|
+
}
|
|
48
|
+
|
|
14
49
|
// ── Disconnect Context (internal) ──
|
|
15
50
|
|
|
16
51
|
type DisconnectFn = () => Promise<void>;
|
|
@@ -60,7 +95,7 @@ export function ManagedWalletProvider({
|
|
|
60
95
|
|
|
61
96
|
// Determine which adapter to use:
|
|
62
97
|
// - iOS always uses Phantom deeplinks
|
|
63
|
-
// - Android uses MWA (default)
|
|
98
|
+
// - Android uses MWA (default)
|
|
64
99
|
const usePhantom = Platform.OS === 'ios' && !!redirectUri;
|
|
65
100
|
|
|
66
101
|
console.log(TAG, `Platform: ${Platform.OS}, redirectUri: ${redirectUri ? 'provided' : 'not set'}, usePhantom: ${usePhantom}`);
|
|
@@ -68,27 +103,14 @@ export function ManagedWalletProvider({
|
|
|
68
103
|
const adapterRef = useRef<WalletAdapter | null>(null);
|
|
69
104
|
const transactRef = useRef<any>(null);
|
|
70
105
|
|
|
71
|
-
// Lazily create adapter
|
|
106
|
+
// Lazily create adapter — Phantom uses a module-level singleton to survive remounts
|
|
72
107
|
if (!adapterRef.current) {
|
|
73
108
|
if (usePhantom) {
|
|
74
|
-
|
|
75
|
-
adapterRef.current = new PhantomDeeplinkAdapter({
|
|
109
|
+
adapterRef.current = getOrCreatePhantomAdapter({
|
|
76
110
|
redirectUri: redirectUri!,
|
|
77
111
|
appUrl,
|
|
78
112
|
cluster,
|
|
79
|
-
|
|
80
|
-
if (session) {
|
|
81
|
-
console.log(TAG, 'Phantom session changed — saving to storage, wallet:', session.walletPublicKey);
|
|
82
|
-
storage.setItem(STORAGE_KEYS.PHANTOM_SESSION, JSON.stringify(session)).catch((err) => {
|
|
83
|
-
console.log(TAG, 'Failed to save Phantom session:', err);
|
|
84
|
-
});
|
|
85
|
-
} else {
|
|
86
|
-
console.log(TAG, 'Phantom session cleared — removing from storage');
|
|
87
|
-
storage.deleteItem(STORAGE_KEYS.PHANTOM_SESSION).catch((err) => {
|
|
88
|
-
console.log(TAG, 'Failed to delete Phantom session:', err);
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
},
|
|
113
|
+
storage,
|
|
92
114
|
});
|
|
93
115
|
} else {
|
|
94
116
|
console.log(TAG, 'Creating MwaWalletAdapter');
|
|
@@ -122,14 +144,25 @@ export function ManagedWalletProvider({
|
|
|
122
144
|
|
|
123
145
|
(async () => {
|
|
124
146
|
if (usePhantom) {
|
|
147
|
+
const phantom = adapter as PhantomDeeplinkAdapter;
|
|
148
|
+
|
|
149
|
+
// If the singleton is already connected (e.g. after returning from Phantom), skip restore
|
|
150
|
+
if (phantom.connected) {
|
|
151
|
+
console.log(TAG, 'Phantom adapter already connected, skipping restore');
|
|
152
|
+
if (!cancelled) {
|
|
153
|
+
setConnected(true);
|
|
154
|
+
setIsReady(true);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
125
159
|
console.log(TAG, 'Phantom path — checking for saved session...');
|
|
126
|
-
// Attempt to restore a saved Phantom session
|
|
127
160
|
try {
|
|
128
161
|
const savedJson = await storage.getItem(STORAGE_KEYS.PHANTOM_SESSION);
|
|
129
162
|
if (savedJson && !cancelled) {
|
|
130
163
|
console.log(TAG, 'Found saved Phantom session, restoring...');
|
|
131
164
|
const saved: PhantomSession = JSON.parse(savedJson);
|
|
132
|
-
|
|
165
|
+
phantom.restoreSession(saved);
|
|
133
166
|
if (!cancelled) {
|
|
134
167
|
console.log(TAG, 'Session restored, marking connected');
|
|
135
168
|
setConnected(true);
|
|
@@ -139,7 +172,6 @@ export function ManagedWalletProvider({
|
|
|
139
172
|
}
|
|
140
173
|
} catch (err) {
|
|
141
174
|
console.log(TAG, 'Failed to restore Phantom session:', err instanceof Error ? err.message : err);
|
|
142
|
-
// Session expired or corrupt — user will tap Connect manually
|
|
143
175
|
} finally {
|
|
144
176
|
if (!cancelled) {
|
|
145
177
|
console.log(TAG, 'Phantom init complete, marking ready');
|
|
@@ -148,7 +180,6 @@ export function ManagedWalletProvider({
|
|
|
148
180
|
}
|
|
149
181
|
} else {
|
|
150
182
|
console.log(TAG, 'MWA path — dynamic-importing transact...');
|
|
151
|
-
// MWA path — dynamic-import transact
|
|
152
183
|
try {
|
|
153
184
|
const mwa = await import('@solana-mobile/mobile-wallet-adapter-protocol-web3js');
|
|
154
185
|
if (cancelled) return;
|
|
@@ -158,7 +189,6 @@ export function ManagedWalletProvider({
|
|
|
158
189
|
console.log(TAG, 'MWA not installed — transact will throw on use');
|
|
159
190
|
}
|
|
160
191
|
|
|
161
|
-
// Attempt silent reconnect from saved auth token
|
|
162
192
|
try {
|
|
163
193
|
const savedToken = await storage.getItem(STORAGE_KEYS.MWA_AUTH_TOKEN);
|
|
164
194
|
if (savedToken && !cancelled) {
|
|
@@ -208,22 +238,19 @@ export function ManagedWalletProvider({
|
|
|
208
238
|
const disconnect = useCallback(async () => {
|
|
209
239
|
console.log(TAG, 'disconnect() — clearing all state');
|
|
210
240
|
adapter.disconnect?.();
|
|
241
|
+
// Destroy and reset the singleton so a fresh adapter is created on next connect
|
|
242
|
+
if (usePhantom && phantomSingleton) {
|
|
243
|
+
console.log(TAG, 'Destroying Phantom singleton');
|
|
244
|
+
phantomSingleton.destroy();
|
|
245
|
+
phantomSingleton = null;
|
|
246
|
+
adapterRef.current = null;
|
|
247
|
+
}
|
|
211
248
|
await storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {});
|
|
212
249
|
await storage.deleteItem(STORAGE_KEYS.PHANTOM_SESSION).catch(() => {});
|
|
213
250
|
await storage.deleteItem(STORAGE_KEYS.JWT_TOKEN).catch(() => {});
|
|
214
251
|
setConnected(false);
|
|
215
252
|
console.log(TAG, 'disconnect() — done');
|
|
216
|
-
}, [adapter, storage]);
|
|
217
|
-
|
|
218
|
-
// Cleanup deeplink handler on unmount
|
|
219
|
-
useEffect(() => {
|
|
220
|
-
return () => {
|
|
221
|
-
if (usePhantom && adapter && 'destroy' in adapter) {
|
|
222
|
-
console.log(TAG, 'Unmounting — destroying Phantom adapter');
|
|
223
|
-
(adapter as PhantomDeeplinkAdapter).destroy();
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
}, [adapter, usePhantom]);
|
|
253
|
+
}, [adapter, storage, usePhantom]);
|
|
227
254
|
|
|
228
255
|
// Show nothing until we've attempted silent reconnect
|
|
229
256
|
if (!isReady) {
|
|
@@ -234,7 +261,6 @@ export function ManagedWalletProvider({
|
|
|
234
261
|
// Not connected — show connect screen
|
|
235
262
|
if (!connected) {
|
|
236
263
|
if (renderConnectScreen === false) {
|
|
237
|
-
// Headless mode — render nothing
|
|
238
264
|
return null;
|
|
239
265
|
}
|
|
240
266
|
const connectProps: ConnectWalletScreenProps = {
|