@dubsdotapp/expo 0.3.9 → 0.4.0
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.d.mts +55 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +209 -122
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +159 -73
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useArcadeBridge.ts +188 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
package/src/hooks/index.ts
CHANGED
|
@@ -28,3 +28,5 @@ export { useEnterArcadePool } from './useEnterArcadePool';
|
|
|
28
28
|
export type { EnterArcadePoolMutationResult } from './useEnterArcadePool';
|
|
29
29
|
export { useArcadeCountdown } from './useArcadeCountdown';
|
|
30
30
|
export type { ArcadeCountdown } from './useArcadeCountdown';
|
|
31
|
+
export { useArcadeBridge } from './useArcadeBridge';
|
|
32
|
+
export type { UseArcadeBridgeOptions, UseArcadeBridgeResult } from './useArcadeBridge';
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useArcadeBridge
|
|
3
|
+
*
|
|
4
|
+
* Owns the full RN ↔ WebView bridge for Dubs Arcade pool integration.
|
|
5
|
+
* Replaces the manual sessionToken / gameStartTime / handleMessage / injectJavaScript
|
|
6
|
+
* wiring that previously lived inline in each game's index.tsx.
|
|
7
|
+
*
|
|
8
|
+
* PROTOCOL VERSION: dubsArcade 1.0
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* const { webviewRef, handleMessage, triggerPlay, lastResult } = useArcadeBridge({
|
|
13
|
+
* poolId,
|
|
14
|
+
* startAttempt,
|
|
15
|
+
* submitScore,
|
|
16
|
+
* canPlay,
|
|
17
|
+
* onScoreSubmitted: (result) => { refetchPool(); },
|
|
18
|
+
* onError: (err) => { console.warn(err); },
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* <WebView ref={webviewRef} onMessage={handleMessage} ... />
|
|
22
|
+
* <TouchableOpacity onPress={triggerPlay}>Play</TouchableOpacity>
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { useRef, useState, useCallback } from 'react';
|
|
26
|
+
import type { StartAttemptResult, SubmitScoreResult } from '../types';
|
|
27
|
+
|
|
28
|
+
// react-native-webview is a peer dependency of apps using this SDK;
|
|
29
|
+
// we only need its ref type for the public interface.
|
|
30
|
+
// Using `any` here avoids forcing apps that don't use WebView to install it.
|
|
31
|
+
type WebView = any;
|
|
32
|
+
|
|
33
|
+
const PROTOCOL_VERSION = '1.0';
|
|
34
|
+
|
|
35
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export interface UseArcadeBridgeOptions {
|
|
38
|
+
/** Whether the player currently has an entry with lives remaining */
|
|
39
|
+
canPlay: boolean;
|
|
40
|
+
|
|
41
|
+
/** From useArcadeGame — starts a new life on the server */
|
|
42
|
+
startAttempt: () => Promise<StartAttemptResult>;
|
|
43
|
+
|
|
44
|
+
/** From useArcadeGame — submits a score for a completed life */
|
|
45
|
+
submitScore: (
|
|
46
|
+
sessionToken: string,
|
|
47
|
+
score: number,
|
|
48
|
+
durationMs?: number,
|
|
49
|
+
) => Promise<SubmitScoreResult>;
|
|
50
|
+
|
|
51
|
+
/** Called after a score is successfully submitted */
|
|
52
|
+
onScoreSubmitted?: (result: SubmitScoreResult) => void;
|
|
53
|
+
|
|
54
|
+
/** Called on any bridge error (network, invalid message, etc.) */
|
|
55
|
+
onError?: (err: Error) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface UseArcadeBridgeResult {
|
|
59
|
+
/** Attach to <WebView ref={webviewRef}> */
|
|
60
|
+
webviewRef: React.RefObject<WebView>;
|
|
61
|
+
|
|
62
|
+
/** Pass to <WebView onMessage={handleMessage}> */
|
|
63
|
+
handleMessage: (event: any) => void;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Call when the player taps PLAY in your UI.
|
|
67
|
+
* Calls startAttempt() then injects the session token + ARCADE_START into the WebView.
|
|
68
|
+
*/
|
|
69
|
+
triggerPlay: () => Promise<void>;
|
|
70
|
+
|
|
71
|
+
/** The result of the most recent submitted score, or null */
|
|
72
|
+
lastResult: SubmitScoreResult | null;
|
|
73
|
+
|
|
74
|
+
/** True while a startAttempt or submitScore request is in flight */
|
|
75
|
+
bridgeLoading: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Hook ───────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export function useArcadeBridge({
|
|
81
|
+
canPlay,
|
|
82
|
+
startAttempt,
|
|
83
|
+
submitScore,
|
|
84
|
+
onScoreSubmitted,
|
|
85
|
+
onError,
|
|
86
|
+
}: UseArcadeBridgeOptions): UseArcadeBridgeResult {
|
|
87
|
+
const webviewRef = useRef<WebView>(null);
|
|
88
|
+
|
|
89
|
+
// Active session state — kept in refs so handleMessage callbacks stay stable
|
|
90
|
+
const sessionTokenRef = useRef<string | null>(null);
|
|
91
|
+
const gameStartTimeRef = useRef<number>(0);
|
|
92
|
+
|
|
93
|
+
const [lastResult, setLastResult] = useState<SubmitScoreResult | null>(null);
|
|
94
|
+
const [bridgeLoading, setBridgeLoading] = useState(false);
|
|
95
|
+
|
|
96
|
+
// ── Inject session token + fire ARCADE_START ──────────────────────────────
|
|
97
|
+
const injectSession = useCallback((token: string, attemptNumber: number) => {
|
|
98
|
+
webviewRef.current?.injectJavaScript(`
|
|
99
|
+
window.ARCADE_SESSION_TOKEN = ${JSON.stringify(token)};
|
|
100
|
+
window.ARCADE_ATTEMPT_NUMBER = ${attemptNumber};
|
|
101
|
+
window._ARCADE_GAME_START_TIME = Date.now();
|
|
102
|
+
window.dispatchEvent(new Event('ARCADE_START'));
|
|
103
|
+
true;
|
|
104
|
+
`);
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
// ── triggerPlay ───────────────────────────────────────────────────────────
|
|
108
|
+
const triggerPlay = useCallback(async () => {
|
|
109
|
+
if (!canPlay) return;
|
|
110
|
+
setBridgeLoading(true);
|
|
111
|
+
try {
|
|
112
|
+
const result = await startAttempt();
|
|
113
|
+
sessionTokenRef.current = result.sessionToken;
|
|
114
|
+
gameStartTimeRef.current = Date.now();
|
|
115
|
+
setLastResult(null);
|
|
116
|
+
injectSession(result.sessionToken, result.attemptNumber);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
119
|
+
onError?.(e);
|
|
120
|
+
} finally {
|
|
121
|
+
setBridgeLoading(false);
|
|
122
|
+
}
|
|
123
|
+
}, [canPlay, startAttempt, injectSession, onError]);
|
|
124
|
+
|
|
125
|
+
// ── handleMessage ─────────────────────────────────────────────────────────
|
|
126
|
+
const handleMessage = useCallback(
|
|
127
|
+
async (event: any) => {
|
|
128
|
+
let data: Record<string, any>;
|
|
129
|
+
|
|
130
|
+
// Parse + validate protocol envelope
|
|
131
|
+
try {
|
|
132
|
+
data = JSON.parse(event.nativeEvent.data);
|
|
133
|
+
} catch {
|
|
134
|
+
return; // non-JSON postMessages from the game's own code — ignore
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Only process messages from our bridge protocol
|
|
138
|
+
if (data.dubsArcade !== PROTOCOL_VERSION) return;
|
|
139
|
+
|
|
140
|
+
switch (data.type) {
|
|
141
|
+
case 'TAP_PLAY': {
|
|
142
|
+
if (canPlay) {
|
|
143
|
+
triggerPlay();
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case 'GAME_OVER': {
|
|
149
|
+
const token = sessionTokenRef.current;
|
|
150
|
+
if (!token) return; // no active session — stale message, ignore
|
|
151
|
+
|
|
152
|
+
const score: number = typeof data.score === 'number' ? data.score : 0;
|
|
153
|
+
// Use server-start time as ground truth; fall back to game's reported duration
|
|
154
|
+
const duration =
|
|
155
|
+
gameStartTimeRef.current > 0
|
|
156
|
+
? Date.now() - gameStartTimeRef.current
|
|
157
|
+
: typeof data.durationMs === 'number'
|
|
158
|
+
? data.durationMs
|
|
159
|
+
: undefined;
|
|
160
|
+
|
|
161
|
+
// Clear immediately to prevent double-submit on re-render
|
|
162
|
+
sessionTokenRef.current = null;
|
|
163
|
+
gameStartTimeRef.current = 0;
|
|
164
|
+
|
|
165
|
+
setBridgeLoading(true);
|
|
166
|
+
try {
|
|
167
|
+
const result = await submitScore(token, score, duration);
|
|
168
|
+
setLastResult(result);
|
|
169
|
+
onScoreSubmitted?.(result);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
172
|
+
onError?.(e);
|
|
173
|
+
} finally {
|
|
174
|
+
setBridgeLoading(false);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
default:
|
|
180
|
+
// Unknown message type from a future protocol version — ignore gracefully
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
[canPlay, triggerPlay, submitScore, onScoreSubmitted, onError],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return { webviewRef, handleMessage, triggerPlay, lastResult, bridgeLoading };
|
|
188
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -109,6 +109,7 @@ export {
|
|
|
109
109
|
useArcadeGame,
|
|
110
110
|
useEnterArcadePool,
|
|
111
111
|
useArcadeCountdown,
|
|
112
|
+
useArcadeBridge,
|
|
112
113
|
} from './hooks';
|
|
113
114
|
export type {
|
|
114
115
|
CreateGameMutationResult,
|
|
@@ -123,6 +124,8 @@ export type {
|
|
|
123
124
|
UseArcadeGameResult,
|
|
124
125
|
EnterArcadePoolMutationResult,
|
|
125
126
|
ArcadeCountdown,
|
|
127
|
+
UseArcadeBridgeOptions,
|
|
128
|
+
UseArcadeBridgeResult,
|
|
126
129
|
} from './hooks';
|
|
127
130
|
|
|
128
131
|
// UI
|