@dubsdotapp/expo 0.2.13 → 0.2.15
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 +47 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +47 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/managed-wallet.tsx +66 -47
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,24 +144,27 @@ export function ManagedWalletProvider({
|
|
|
122
144
|
|
|
123
145
|
(async () => {
|
|
124
146
|
if (usePhantom) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
(
|
|
133
|
-
if (!cancelled) {
|
|
134
|
-
console.log(TAG, 'Session restored, marking connected');
|
|
135
|
-
setConnected(true);
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
console.log(TAG, 'No saved Phantom session found');
|
|
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);
|
|
139
155
|
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Clear any stale session — Phantom sessions don't survive across app/Phantom restarts.
|
|
160
|
+
// Always require a fresh connect to establish a valid encryption channel.
|
|
161
|
+
console.log(TAG, 'Phantom path — clearing any saved session, will require fresh connect');
|
|
162
|
+
await storage.deleteItem(STORAGE_KEYS.PHANTOM_SESSION).catch(() => {});
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Nothing to restore — fall through to show connect screen
|
|
140
166
|
} catch (err) {
|
|
141
|
-
console.log(TAG, '
|
|
142
|
-
// Session expired or corrupt — user will tap Connect manually
|
|
167
|
+
console.log(TAG, 'Unexpected error during Phantom init:', err instanceof Error ? err.message : err);
|
|
143
168
|
} finally {
|
|
144
169
|
if (!cancelled) {
|
|
145
170
|
console.log(TAG, 'Phantom init complete, marking ready');
|
|
@@ -148,7 +173,6 @@ export function ManagedWalletProvider({
|
|
|
148
173
|
}
|
|
149
174
|
} else {
|
|
150
175
|
console.log(TAG, 'MWA path — dynamic-importing transact...');
|
|
151
|
-
// MWA path — dynamic-import transact
|
|
152
176
|
try {
|
|
153
177
|
const mwa = await import('@solana-mobile/mobile-wallet-adapter-protocol-web3js');
|
|
154
178
|
if (cancelled) return;
|
|
@@ -158,7 +182,6 @@ export function ManagedWalletProvider({
|
|
|
158
182
|
console.log(TAG, 'MWA not installed — transact will throw on use');
|
|
159
183
|
}
|
|
160
184
|
|
|
161
|
-
// Attempt silent reconnect from saved auth token
|
|
162
185
|
try {
|
|
163
186
|
const savedToken = await storage.getItem(STORAGE_KEYS.MWA_AUTH_TOKEN);
|
|
164
187
|
if (savedToken && !cancelled) {
|
|
@@ -208,22 +231,19 @@ export function ManagedWalletProvider({
|
|
|
208
231
|
const disconnect = useCallback(async () => {
|
|
209
232
|
console.log(TAG, 'disconnect() — clearing all state');
|
|
210
233
|
adapter.disconnect?.();
|
|
234
|
+
// Destroy and reset the singleton so a fresh adapter is created on next connect
|
|
235
|
+
if (usePhantom && phantomSingleton) {
|
|
236
|
+
console.log(TAG, 'Destroying Phantom singleton');
|
|
237
|
+
phantomSingleton.destroy();
|
|
238
|
+
phantomSingleton = null;
|
|
239
|
+
adapterRef.current = null;
|
|
240
|
+
}
|
|
211
241
|
await storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {});
|
|
212
242
|
await storage.deleteItem(STORAGE_KEYS.PHANTOM_SESSION).catch(() => {});
|
|
213
243
|
await storage.deleteItem(STORAGE_KEYS.JWT_TOKEN).catch(() => {});
|
|
214
244
|
setConnected(false);
|
|
215
245
|
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]);
|
|
246
|
+
}, [adapter, storage, usePhantom]);
|
|
227
247
|
|
|
228
248
|
// Show nothing until we've attempted silent reconnect
|
|
229
249
|
if (!isReady) {
|
|
@@ -234,7 +254,6 @@ export function ManagedWalletProvider({
|
|
|
234
254
|
// Not connected — show connect screen
|
|
235
255
|
if (!connected) {
|
|
236
256
|
if (renderConnectScreen === false) {
|
|
237
|
-
// Headless mode — render nothing
|
|
238
257
|
return null;
|
|
239
258
|
}
|
|
240
259
|
const connectProps: ConnectWalletScreenProps = {
|