@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dubsdotapp/expo",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "React Native SDK for the Dubs betting platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -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) unless redirectUri is provided and MWA is unavailable
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
- console.log(TAG, 'Creating PhantomDeeplinkAdapter');
75
- adapterRef.current = new PhantomDeeplinkAdapter({
109
+ adapterRef.current = getOrCreatePhantomAdapter({
76
110
  redirectUri: redirectUri!,
77
111
  appUrl,
78
112
  cluster,
79
- onSessionChange: (session) => {
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
- (adapter as PhantomDeeplinkAdapter).restoreSession(saved);
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 = {