@dubsdotapp/expo 0.2.19 → 0.2.21

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.19",
3
+ "version": "0.2.21",
4
4
  "description": "React Native SDK for the Dubs betting platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -31,11 +31,15 @@
31
31
  "@solana/web3.js": "^1.90.0",
32
32
  "react": ">=18.0.0",
33
33
  "react-native": ">=0.72.0",
34
- "expo-secure-store": ">=13.0.0"
34
+ "expo-secure-store": ">=13.0.0",
35
+ "expo-device": ">=6.0.0"
35
36
  },
36
37
  "peerDependenciesMeta": {
37
38
  "expo-secure-store": {
38
39
  "optional": true
40
+ },
41
+ "expo-device": {
42
+ "optional": true
39
43
  }
40
44
  },
41
45
  "optionalDependencies": {
@@ -45,6 +49,7 @@
45
49
  "@solana/web3.js": "^1.95.0",
46
50
  "@types/react": "^18.2.0",
47
51
  "react": "^18.2.0",
52
+ "expo-device": "^7.0.0",
48
53
  "react-native": "^0.73.0",
49
54
  "tsup": "^8.0.0",
50
55
  "typescript": "^5.3.0"
@@ -2,6 +2,7 @@ import { useState, useCallback, useRef, useContext } from 'react';
2
2
  import bs58 from 'bs58';
3
3
  import { useDubs } from '../provider';
4
4
  import { AuthContext } from '../auth-context';
5
+ import { getDeviceInfo } from '../utils/device';
5
6
  import type { AuthStatus, DubsUser } from '../types';
6
7
 
7
8
  export interface UseAuthResult {
@@ -52,11 +53,12 @@ export function useAuth(): UseAuthResult {
52
53
  const [token, setToken] = useState<string | null>(null);
53
54
  const [error, setError] = useState<Error | null>(null);
54
55
 
55
- // Stash nonce+signature between authenticate → register (single-sign flow)
56
+ // Stash nonce+signature+deviceInfo between authenticate → register (single-sign flow)
56
57
  const pendingAuth = useRef<{
57
58
  walletAddress: string;
58
59
  nonce: string;
59
60
  signature: string;
61
+ deviceInfo?: import('../utils/device').DeviceInfo;
60
62
  } | null>(null);
61
63
 
62
64
  const reset = useCallback(() => {
@@ -82,6 +84,10 @@ export function useAuth(): UseAuthResult {
82
84
 
83
85
  const walletAddress = wallet.publicKey.toBase58();
84
86
 
87
+ // 0. Collect device info
88
+ const deviceInfo = await getDeviceInfo();
89
+ console.log('[useAuth] Device info:', JSON.stringify(deviceInfo, null, 2));
90
+
85
91
  // 1. Get nonce
86
92
  const { nonce, message } = await client.getNonce(walletAddress);
87
93
 
@@ -94,11 +100,11 @@ export function useAuth(): UseAuthResult {
94
100
 
95
101
  // 3. Verify with server
96
102
  setStatus('verifying');
97
- const result = await client.authenticate({ walletAddress, signature, nonce });
103
+ const result = await client.authenticate({ walletAddress, signature, nonce, deviceInfo });
98
104
 
99
105
  if (result.needsRegistration) {
100
106
  // Stash credentials for register() — nonce is NOT consumed
101
- pendingAuth.current = { walletAddress, nonce, signature };
107
+ pendingAuth.current = { walletAddress, nonce, signature, deviceInfo };
102
108
  setStatus('needsRegistration');
103
109
  return;
104
110
  }
@@ -130,6 +136,7 @@ export function useAuth(): UseAuthResult {
130
136
  username,
131
137
  referralCode,
132
138
  avatarUrl,
139
+ deviceInfo: pending.deviceInfo,
133
140
  });
134
141
 
135
142
  pendingAuth.current = null;
@@ -10,7 +10,7 @@ export interface ClaimMutationResult {
10
10
  }
11
11
 
12
12
  export function useClaim() {
13
- const { client, wallet } = useDubs();
13
+ const { client, wallet, connection } = useDubs();
14
14
  const [status, setStatus] = useState<MutationStatus>('idle');
15
15
  const [error, setError] = useState<Error | null>(null);
16
16
  const [data, setData] = useState<ClaimMutationResult | null>(null);
@@ -38,6 +38,7 @@ export function useClaim() {
38
38
  const signature = await signAndSendBase64Transaction(
39
39
  claimResult.transaction,
40
40
  wallet,
41
+ connection,
41
42
  );
42
43
  console.log('[useClaim] Step 2 done. Signature:', signature);
43
44
 
@@ -12,7 +12,7 @@ export interface CreateCustomGameMutationResult {
12
12
  }
13
13
 
14
14
  export function useCreateCustomGame() {
15
- const { client, wallet } = useDubs();
15
+ const { client, wallet, connection } = useDubs();
16
16
  const [status, setStatus] = useState<MutationStatus>('idle');
17
17
  const [error, setError] = useState<Error | null>(null);
18
18
  const [data, setData] = useState<CreateCustomGameMutationResult | null>(null);
@@ -40,6 +40,7 @@ export function useCreateCustomGame() {
40
40
  const signature = await signAndSendBase64Transaction(
41
41
  createResult.transaction,
42
42
  wallet,
43
+ connection,
43
44
  );
44
45
  console.log('[useCreateCustomGame] Step 2 done. Signature:', signature);
45
46
 
@@ -11,7 +11,7 @@ export interface CreateGameMutationResult {
11
11
  }
12
12
 
13
13
  export function useCreateGame() {
14
- const { client, wallet } = useDubs();
14
+ const { client, wallet, connection } = useDubs();
15
15
  const [status, setStatus] = useState<MutationStatus>('idle');
16
16
  const [error, setError] = useState<Error | null>(null);
17
17
  const [data, setData] = useState<CreateGameMutationResult | null>(null);
@@ -39,6 +39,7 @@ export function useCreateGame() {
39
39
  const signature = await signAndSendBase64Transaction(
40
40
  createResult.transaction,
41
41
  wallet,
42
+ connection,
42
43
  );
43
44
  console.log('[useCreateGame] Step 2 done. Signature:', signature);
44
45
 
@@ -11,7 +11,7 @@ export interface JoinGameMutationResult {
11
11
  }
12
12
 
13
13
  export function useJoinGame() {
14
- const { client, wallet } = useDubs();
14
+ const { client, wallet, connection } = useDubs();
15
15
  const [status, setStatus] = useState<MutationStatus>('idle');
16
16
  const [error, setError] = useState<Error | null>(null);
17
17
  const [data, setData] = useState<JoinGameMutationResult | null>(null);
@@ -39,6 +39,7 @@ export function useJoinGame() {
39
39
  const signature = await signAndSendBase64Transaction(
40
40
  joinResult.transaction,
41
41
  wallet,
42
+ connection,
42
43
  );
43
44
  console.log('[useJoinGame] Step 2 done. Signature:', signature);
44
45
 
package/src/index.ts CHANGED
@@ -107,3 +107,5 @@ export type {
107
107
 
108
108
  // Utils
109
109
  export { signAndSendBase64Transaction } from './utils/transaction';
110
+ export { getDeviceInfo } from './utils/device';
111
+ export type { DeviceInfo } from './utils/device';
@@ -156,15 +156,22 @@ export function ManagedWalletProvider({
156
156
  return;
157
157
  }
158
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
159
  try {
165
- // Nothing to restore — fall through to show connect screen
160
+ const savedSession = await storage.getItem(STORAGE_KEYS.PHANTOM_SESSION);
161
+ if (savedSession && !cancelled) {
162
+ const session: PhantomSession = JSON.parse(savedSession);
163
+ console.log(TAG, 'Found saved Phantom session, restoring for wallet:', session.walletPublicKey);
164
+ phantom.restoreSession(session);
165
+ if (!cancelled) {
166
+ console.log(TAG, 'Phantom reconnected from saved session');
167
+ setConnected(true);
168
+ }
169
+ } else {
170
+ console.log(TAG, 'No saved Phantom session');
171
+ }
166
172
  } catch (err) {
167
- console.log(TAG, 'Unexpected error during Phantom init:', err instanceof Error ? err.message : err);
173
+ console.log(TAG, 'Phantom session restore failed:', err instanceof Error ? err.message : err);
174
+ await storage.deleteItem(STORAGE_KEYS.PHANTOM_SESSION).catch(() => {});
168
175
  } finally {
169
176
  if (!cancelled) {
170
177
  console.log(TAG, 'Phantom init complete, marking ready');
package/src/types.ts CHANGED
@@ -295,10 +295,13 @@ export interface NonceResult {
295
295
  message: string;
296
296
  }
297
297
 
298
+ export type { DeviceInfo } from './utils/device';
299
+
298
300
  export interface AuthenticateParams {
299
301
  walletAddress: string;
300
302
  signature: string;
301
303
  nonce: string;
304
+ deviceInfo?: import('./utils/device').DeviceInfo;
302
305
  }
303
306
 
304
307
  export interface AuthenticateResult {
@@ -314,6 +317,7 @@ export interface RegisterParams {
314
317
  username: string;
315
318
  referralCode?: string;
316
319
  avatarUrl?: string;
320
+ deviceInfo?: import('./utils/device').DeviceInfo;
317
321
  }
318
322
 
319
323
  export interface RegisterResult {
@@ -0,0 +1,55 @@
1
+ import { Platform } from 'react-native';
2
+
3
+ export interface DeviceInfo {
4
+ platform: string;
5
+ modelName: string | null;
6
+ brand: string | null;
7
+ manufacturer: string | null;
8
+ osName: string | null;
9
+ osVersion: string | null;
10
+ deviceType: number | null;
11
+ deviceName: string | null;
12
+ totalMemory: number | null;
13
+ modelId: string | null;
14
+ designName: string | null;
15
+ productName: string | null;
16
+ isDevice: boolean | null;
17
+ }
18
+
19
+ export async function getDeviceInfo(): Promise<DeviceInfo> {
20
+ try {
21
+ const Device = require('expo-device');
22
+ return {
23
+ platform: Platform.OS,
24
+ modelName: Device.modelName,
25
+ brand: Device.brand,
26
+ manufacturer: Device.manufacturer,
27
+ osName: Device.osName,
28
+ osVersion: Device.osVersion,
29
+ deviceType: Device.deviceType,
30
+ deviceName: Device.deviceName,
31
+ totalMemory: Device.totalMemory,
32
+ modelId: Device.modelId,
33
+ designName: Device.designName,
34
+ productName: Device.productName,
35
+ isDevice: Device.isDevice,
36
+ };
37
+ } catch {
38
+ // expo-device not installed — return minimal info
39
+ return {
40
+ platform: Platform.OS,
41
+ modelName: null,
42
+ brand: null,
43
+ manufacturer: null,
44
+ osName: null,
45
+ osVersion: null,
46
+ deviceType: null,
47
+ deviceName: null,
48
+ totalMemory: null,
49
+ modelId: null,
50
+ designName: null,
51
+ productName: null,
52
+ isDevice: null,
53
+ };
54
+ }
55
+ }
@@ -1,13 +1,16 @@
1
- import { Transaction } from '@solana/web3.js';
1
+ import { Transaction, Connection } from '@solana/web3.js';
2
2
  import type { WalletAdapter } from '../wallet/types';
3
3
 
4
4
  /**
5
5
  * Deserialize a base64-encoded transaction, sign via wallet adapter, send to Solana.
6
+ * Prefers signAndSendTransaction if available (MWA), otherwise falls back to
7
+ * signTransaction + sendRawTransaction via RPC (Phantom deeplinks).
6
8
  * Returns the transaction signature.
7
9
  */
8
10
  export async function signAndSendBase64Transaction(
9
11
  base64Tx: string,
10
12
  wallet: WalletAdapter,
13
+ connection: Connection,
11
14
  ): Promise<string> {
12
15
  if (!wallet.publicKey) throw new Error('Wallet not connected');
13
16
 
@@ -18,9 +21,13 @@ export async function signAndSendBase64Transaction(
18
21
  }
19
22
  const transaction = Transaction.from(bytes);
20
23
 
24
+ // Prefer signAndSendTransaction if wallet supports it (e.g. MWA)
21
25
  if (wallet.signAndSendTransaction) {
22
26
  return wallet.signAndSendTransaction(transaction);
23
27
  }
24
28
 
25
- throw new Error('Wallet does not support signAndSendTransaction');
29
+ // Fallback: sign via wallet, then send via RPC
30
+ const signed = await wallet.signTransaction(transaction);
31
+ const signature = await connection.sendRawTransaction(signed.serialize());
32
+ return signature;
26
33
  }
@@ -237,46 +237,6 @@ export class PhantomDeeplinkAdapter implements WalletAdapter {
237
237
  return Transaction.from(bs58.decode(data.transaction));
238
238
  }
239
239
 
240
- async signAndSendTransaction(transaction: Transaction): Promise<string> {
241
- this.assertConnected();
242
- console.log(TAG, 'signAndSendTransaction() — serializing transaction');
243
-
244
- const serializedTx = bs58.encode(
245
- transaction.serialize({ requireAllSignatures: false, verifySignatures: false }),
246
- );
247
- console.log(TAG, 'Transaction serialized, length:', serializedTx.length);
248
-
249
- const { nonce, ciphertext } = encryptPayload(
250
- { transaction: serializedTx, session: this._sessionToken },
251
- this._sharedSecret!,
252
- );
253
-
254
- const requestId = nextRequestId();
255
- const redirectLink = this.config.redirectUri;
256
- console.log(TAG, `signAndSendTransaction() requestId=${requestId}`);
257
-
258
- const params = new URLSearchParams({
259
- dapp_encryption_public_key: bs58.encode(this._dappKeyPair!.publicKey),
260
- nonce,
261
- payload: ciphertext,
262
- redirect_link: redirectLink,
263
- });
264
-
265
- const url = `https://phantom.app/ul/v1/signAndSendTransaction?${params.toString()}`;
266
- console.log(TAG, 'Opening Phantom signAndSendTransaction deeplink...');
267
- const response = await this.handler.send(url, requestId, this.timeout);
268
- console.log(TAG, 'Received signAndSendTransaction response');
269
-
270
- const data = decryptPayload(
271
- response.params.data,
272
- response.params.nonce,
273
- this._sharedSecret!,
274
- );
275
- console.log(TAG, 'Transaction sent! Signature:', data.signature);
276
-
277
- return data.signature as string;
278
- }
279
-
280
240
  async signMessage(message: Uint8Array): Promise<Uint8Array> {
281
241
  this.assertConnected();
282
242
  console.log(TAG, 'signMessage() — message length:', message.length);