@dubsdotapp/expo 0.2.33 → 0.2.35

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.33",
3
+ "version": "0.2.35",
4
4
  "description": "React Native SDK for the Dubs betting platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
package/src/errors.ts CHANGED
@@ -66,6 +66,14 @@ export const SOLANA_PROGRAM_ERRORS: Record<number, SolanaErrorCode> = {
66
66
  6049: { code: 'too_many_survivors', message: 'Too many survivors (max 50 per batch)' },
67
67
  };
68
68
 
69
+ /** Known wallet/MWA error patterns that don't contain Solana error codes */
70
+ const WALLET_ERROR_PATTERNS: Array<{ pattern: RegExp; error: ParsedError }> = [
71
+ { pattern: /CancellationException/i, error: { code: 'transaction_rejected', message: 'Transaction was rejected by the wallet' } },
72
+ { pattern: /declined/i, error: { code: 'user_declined', message: 'Transaction was declined' } },
73
+ { pattern: /timeout/i, error: { code: 'transaction_timeout', message: 'Transaction timed out' } },
74
+ { pattern: /not connected/i, error: { code: 'wallet_not_connected', message: 'Wallet is not connected' } },
75
+ ];
76
+
69
77
  /** Known Solana built-in instruction errors */
70
78
  const SOLANA_BUILTIN_ERRORS: Record<number, SolanaErrorCode> = {
71
79
  0: { code: 'generic_error', message: 'Generic instruction error' },
@@ -90,6 +98,11 @@ export function parseSolanaError(err: unknown): ParsedError {
90
98
  let parsed = err;
91
99
 
92
100
  if (typeof parsed === 'string') {
101
+ // Check for known wallet/MWA error patterns before attempting JSON parse
102
+ for (const { pattern, error } of WALLET_ERROR_PATTERNS) {
103
+ if (pattern.test(parsed)) return error;
104
+ }
105
+
93
106
  try {
94
107
  const jsonMatch = parsed.match(/\{.*\}/s);
95
108
  if (jsonMatch) parsed = JSON.parse(jsonMatch[0]);
@@ -10,5 +10,7 @@ export { useClaim } from './useClaim';
10
10
  export type { ClaimMutationResult } from './useClaim';
11
11
  export { useCreateCustomGame } from './useCreateCustomGame';
12
12
  export type { CreateCustomGameMutationResult } from './useCreateCustomGame';
13
+ export { useHasClaimed } from './useHasClaimed';
14
+ export type { ClaimStatus } from './useHasClaimed';
13
15
  export { useAuth } from './useAuth';
14
16
  export type { UseAuthResult } from './useAuth';
@@ -1,6 +1,7 @@
1
1
  import { useState, useCallback } from 'react';
2
2
  import { useDubs } from '../provider';
3
3
  import { signAndSendBase64Transaction } from '../utils/transaction';
4
+ import { parseSolanaError } from '../errors';
4
5
  import type { BuildClaimParams, MutationStatus } from '../types';
5
6
 
6
7
  export interface ClaimMutationResult {
@@ -64,7 +65,34 @@ export function useClaim() {
64
65
  return result;
65
66
  } catch (err) {
66
67
  console.error('[useClaim] FAILED:', err);
67
- const error = err instanceof Error ? err : new Error(String(err));
68
+ // Parse Solana program errors into human-readable messages
69
+ const raw = err instanceof Error ? err.message : String(err);
70
+ const parsed = parseSolanaError(raw);
71
+
72
+ // If the error is generic (wallet rejected / unrecognizable), check if it's
73
+ // actually an "already claimed" situation by querying the game state.
74
+ if (parsed.code === 'transaction_rejected' || parsed.code === 'transaction_failed') {
75
+ try {
76
+ const game = await client.getGame(params.gameId);
77
+ const myBet = game.bettors?.find(
78
+ (b: { wallet: string }) => b.wallet === params.playerWallet,
79
+ );
80
+ if (myBet?.amountClaimed != null && myBet.amountClaimed > 0) {
81
+ const claimedErr = new Error('Player has already claimed their winnings');
82
+ (claimedErr as any).code = 'already_claimed';
83
+ setError(claimedErr);
84
+ setStatus('error');
85
+ throw claimedErr;
86
+ }
87
+ } catch (checkErr) {
88
+ // If the post-hoc check itself identified "already claimed", rethrow it
89
+ if ((checkErr as any)?.code === 'already_claimed') throw checkErr;
90
+ // Otherwise fall through to the generic parsed error
91
+ }
92
+ }
93
+
94
+ const error = new Error(parsed.message);
95
+ (error as any).code = parsed.code;
68
96
  setError(error);
69
97
  setStatus('error');
70
98
  throw error;
@@ -0,0 +1,85 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useDubs } from '../provider';
3
+
4
+ export interface ClaimStatus {
5
+ /** Whether the current wallet has claimed this game's prize */
6
+ hasClaimed: boolean;
7
+ /** Amount claimed (null if not claimed) */
8
+ amountClaimed: number | null;
9
+ /** Claim transaction signature (null if not claimed) */
10
+ claimSignature: string | null;
11
+ /** Whether the game has been resolved */
12
+ isResolved: boolean;
13
+ /** Loading state */
14
+ loading: boolean;
15
+ /** Error if fetch failed */
16
+ error: Error | null;
17
+ /** Re-fetch claim status */
18
+ refetch: () => void;
19
+ }
20
+
21
+ /**
22
+ * Check whether the current wallet has already claimed a prize for a game.
23
+ *
24
+ * Uses the game detail endpoint which includes per-bettor claim data.
25
+ */
26
+ export function useHasClaimed(gameId: string | null): ClaimStatus {
27
+ const { client, wallet } = useDubs();
28
+ const [hasClaimed, setHasClaimed] = useState(false);
29
+ const [amountClaimed, setAmountClaimed] = useState<number | null>(null);
30
+ const [claimSignature, setClaimSignature] = useState<string | null>(null);
31
+ const [isResolved, setIsResolved] = useState(false);
32
+ const [loading, setLoading] = useState(false);
33
+ const [error, setError] = useState<Error | null>(null);
34
+ const [trigger, setTrigger] = useState(0);
35
+
36
+ const refetch = () => setTrigger(t => t + 1);
37
+
38
+ useEffect(() => {
39
+ if (!gameId || !wallet.publicKey) {
40
+ setHasClaimed(false);
41
+ setAmountClaimed(null);
42
+ setClaimSignature(null);
43
+ setIsResolved(false);
44
+ return;
45
+ }
46
+
47
+ let cancelled = false;
48
+ setLoading(true);
49
+ setError(null);
50
+
51
+ client
52
+ .getGame(gameId)
53
+ .then(game => {
54
+ if (cancelled) return;
55
+ setIsResolved(game.isResolved);
56
+
57
+ const myWallet = wallet.publicKey!.toBase58();
58
+ const myBet = game.bettors?.find(b => b.wallet === myWallet);
59
+
60
+ if (myBet?.amountClaimed != null && myBet.amountClaimed > 0) {
61
+ setHasClaimed(true);
62
+ setAmountClaimed(myBet.amountClaimed);
63
+ setClaimSignature(myBet.claimSignature ?? null);
64
+ } else {
65
+ setHasClaimed(false);
66
+ setAmountClaimed(null);
67
+ setClaimSignature(null);
68
+ }
69
+ })
70
+ .catch(err => {
71
+ if (!cancelled) {
72
+ setError(err instanceof Error ? err : new Error(String(err)));
73
+ }
74
+ })
75
+ .finally(() => {
76
+ if (!cancelled) setLoading(false);
77
+ });
78
+
79
+ return () => {
80
+ cancelled = true;
81
+ };
82
+ }, [gameId, wallet.publicKey, client, trigger]);
83
+
84
+ return { hasClaimed, amountClaimed, claimSignature, isResolved, loading, error, refetch };
85
+ }
package/src/index.ts CHANGED
@@ -82,6 +82,7 @@ export {
82
82
  useCreateCustomGame,
83
83
  useJoinGame,
84
84
  useClaim,
85
+ useHasClaimed,
85
86
  useAuth,
86
87
  } from './hooks';
87
88
  export type {
@@ -89,6 +90,7 @@ export type {
89
90
  CreateCustomGameMutationResult,
90
91
  JoinGameMutationResult,
91
92
  ClaimMutationResult,
93
+ ClaimStatus,
92
94
  UseAuthResult,
93
95
  } from './hooks';
94
96
 
package/src/types.ts CHANGED
@@ -198,6 +198,8 @@ export interface Bettor {
198
198
  avatar: string | null;
199
199
  team: 'home' | 'away' | 'draw';
200
200
  amount: number;
201
+ amountClaimed: number | null;
202
+ claimSignature: string | null;
201
203
  }
202
204
 
203
205
  export interface GameDetail {