@playruo/embed 1.1.4 → 1.2.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.cjs +231 -42
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +232 -43
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -32,9 +32,12 @@ var import_react = require("react");
|
|
|
32
32
|
|
|
33
33
|
// src/url.ts
|
|
34
34
|
var buildEmbedUrl = ({ baseUrl, operation = "iframe", storeUrl }) => {
|
|
35
|
-
const
|
|
36
|
-
url
|
|
37
|
-
|
|
35
|
+
const operationPath = operation.startsWith("/") ? operation.slice(1) : operation;
|
|
36
|
+
const url = new URL(`/${operationPath}`, baseUrl);
|
|
37
|
+
url.searchParams.set("iframe", "true");
|
|
38
|
+
if (storeUrl) {
|
|
39
|
+
url.searchParams.set("storeUrl", storeUrl);
|
|
40
|
+
}
|
|
38
41
|
return url.toString();
|
|
39
42
|
};
|
|
40
43
|
|
|
@@ -56,10 +59,63 @@ var PlayruoEmbed = ({
|
|
|
56
59
|
}) => {
|
|
57
60
|
const internalIframeRef = (0, import_react.useRef)(null);
|
|
58
61
|
const iframeRef = externalIframeRef || internalIframeRef;
|
|
59
|
-
const [
|
|
60
|
-
const [
|
|
62
|
+
const [isIframeLoaded, setIsIframeLoaded] = (0, import_react.useState)(false);
|
|
63
|
+
const [hasPlayedOnce, setHasPlayedOnce] = (0, import_react.useState)(false);
|
|
64
|
+
const [showGameIframe, setShowGameIframe] = (0, import_react.useState)(false);
|
|
65
|
+
const [isMobile, setIsMobile] = (0, import_react.useState)(false);
|
|
66
|
+
const [isValidChromium, setIsValidChromium] = (0, import_react.useState)(true);
|
|
61
67
|
const [errorMessage, setErrorMessage] = (0, import_react.useState)(null);
|
|
68
|
+
const escapeKeyTimer = (0, import_react.useRef)(null);
|
|
69
|
+
const escapeKeyPressed = (0, import_react.useRef)(false);
|
|
70
|
+
const ESCAPE_HOLD_DURATION = 1500;
|
|
62
71
|
const src = (0, import_react.useMemo)(() => buildEmbedUrl({ baseUrl, operation, storeUrl }), [baseUrl, operation, storeUrl]);
|
|
72
|
+
(0, import_react.useEffect)(() => {
|
|
73
|
+
if (typeof window === "undefined") return;
|
|
74
|
+
const detectMobile = () => {
|
|
75
|
+
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
|
76
|
+
return /android|iPad|iPhone|iPod/i.test(userAgent);
|
|
77
|
+
};
|
|
78
|
+
const detectChromium = () => {
|
|
79
|
+
const hasChrome = !!window.chrome;
|
|
80
|
+
const isChromium = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
|
81
|
+
return hasChrome || isChromium;
|
|
82
|
+
};
|
|
83
|
+
setIsMobile(detectMobile());
|
|
84
|
+
setIsValidChromium(detectChromium());
|
|
85
|
+
}, []);
|
|
86
|
+
const isCompatible = !isMobile && isValidChromium;
|
|
87
|
+
const isReady = isCompatible ? isIframeLoaded : true;
|
|
88
|
+
const currentState = (0, import_react.useMemo)(() => ({
|
|
89
|
+
hasPlayedOnce,
|
|
90
|
+
tryNow: {
|
|
91
|
+
available: isCompatible,
|
|
92
|
+
status: !isCompatible ? "Browser not compatible" : hasPlayedOnce ? "Click to replay" : "Ready"
|
|
93
|
+
},
|
|
94
|
+
buyNow: {
|
|
95
|
+
available: true,
|
|
96
|
+
status: "Ready"
|
|
97
|
+
}
|
|
98
|
+
}), [hasPlayedOnce, isCompatible]);
|
|
99
|
+
const checkKeyboardLockPermission = async () => {
|
|
100
|
+
if (!navigator.permissions) {
|
|
101
|
+
console.log("[PlayruoEmbed] Permissions API not supported");
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const { state } = await navigator.permissions.query({
|
|
106
|
+
name: "keyboard-lock"
|
|
107
|
+
});
|
|
108
|
+
if (state === "granted" || state === "prompt") {
|
|
109
|
+
console.log(`[PlayruoEmbed] Keyboard lock permission: ${state}`);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
console.log("[PlayruoEmbed] Keyboard lock permission denied");
|
|
113
|
+
return false;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.warn("[PlayruoEmbed] Error checking keyboard lock permission:", error);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
63
119
|
const requestFullscreenWithKeyboardLock = async () => {
|
|
64
120
|
const iframe = iframeRef.current;
|
|
65
121
|
if (!iframe?.requestFullscreen) {
|
|
@@ -69,11 +125,17 @@ var PlayruoEmbed = ({
|
|
|
69
125
|
try {
|
|
70
126
|
const fullscreenPromise = iframe.requestFullscreen();
|
|
71
127
|
if (navigator.keyboard) {
|
|
72
|
-
|
|
73
|
-
|
|
128
|
+
const permissionPromise = checkKeyboardLockPermission();
|
|
129
|
+
const keyboardLockPromise = navigator.keyboard.lock(["Escape"]);
|
|
130
|
+
void Promise.all([permissionPromise, keyboardLockPromise]).then(([hasPermission]) => {
|
|
131
|
+
if (hasPermission) {
|
|
132
|
+
console.log("[PlayruoEmbed] Keyboard locked successfully");
|
|
133
|
+
}
|
|
74
134
|
}).catch((err) => {
|
|
75
135
|
console.warn("[PlayruoEmbed] Keyboard lock failed:", err);
|
|
76
136
|
});
|
|
137
|
+
} else {
|
|
138
|
+
console.log("[PlayruoEmbed] Keyboard API not supported");
|
|
77
139
|
}
|
|
78
140
|
void fullscreenPromise.then(() => {
|
|
79
141
|
console.log("[PlayruoEmbed] Fullscreen activated successfully");
|
|
@@ -86,47 +148,157 @@ var PlayruoEmbed = ({
|
|
|
86
148
|
return false;
|
|
87
149
|
}
|
|
88
150
|
};
|
|
89
|
-
const
|
|
90
|
-
if (!
|
|
91
|
-
console.warn(
|
|
151
|
+
const handleTryNow = (0, import_react.useCallback)(() => {
|
|
152
|
+
if (!isCompatible) {
|
|
153
|
+
console.warn("[PlayruoEmbed] Browser not compatible");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const iframe = iframeRef.current;
|
|
157
|
+
if (!iframe) {
|
|
158
|
+
console.warn("[PlayruoEmbed] Iframe not ready");
|
|
92
159
|
return;
|
|
93
160
|
}
|
|
94
|
-
|
|
161
|
+
setShowGameIframe(true);
|
|
162
|
+
try {
|
|
95
163
|
requestFullscreenWithKeyboardLock();
|
|
164
|
+
iframe.contentWindow?.postMessage({
|
|
165
|
+
type: "playruo",
|
|
166
|
+
action: "tryNowClick",
|
|
167
|
+
fromEmbed: true,
|
|
168
|
+
isFullscreen: true,
|
|
169
|
+
isReplay: hasPlayedOnce
|
|
170
|
+
}, "*");
|
|
171
|
+
console.log("[PlayruoEmbed] Sent tryNowClick to game", hasPlayedOnce ? "(replay)" : "(first time)");
|
|
172
|
+
iframe.focus();
|
|
173
|
+
requestAnimationFrame(() => {
|
|
174
|
+
iframe.contentWindow?.focus();
|
|
175
|
+
});
|
|
176
|
+
setHasPlayedOnce(true);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error("[PlayruoEmbed] Failed to start game:", error);
|
|
179
|
+
setErrorMessage("Failed to start game");
|
|
180
|
+
onError?.("Failed to start game");
|
|
96
181
|
}
|
|
97
|
-
|
|
98
|
-
|
|
182
|
+
}, [isCompatible, hasPlayedOnce, onError, iframeRef]);
|
|
183
|
+
const handleBuyNow = (0, import_react.useCallback)(() => {
|
|
184
|
+
if (!storeUrl) {
|
|
185
|
+
console.warn("[PlayruoEmbed] No store URL provided");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
onPurchaseRequest?.({ storeUrl });
|
|
189
|
+
}, [storeUrl, onPurchaseRequest]);
|
|
99
190
|
(0, import_react.useEffect)(() => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
191
|
+
if (isReady) {
|
|
192
|
+
onStateChange?.(currentState);
|
|
193
|
+
}
|
|
194
|
+
}, [isReady, currentState, onStateChange]);
|
|
195
|
+
(0, import_react.useEffect)(() => {
|
|
196
|
+
if (isReady) {
|
|
197
|
+
onReady?.();
|
|
198
|
+
}
|
|
199
|
+
}, [isReady, onReady]);
|
|
200
|
+
(0, import_react.useEffect)(() => {
|
|
201
|
+
const handleGameMessage = (event) => {
|
|
202
|
+
const { type } = event.data || {};
|
|
203
|
+
if (type === "error" || type === "closeForBuy" || type === "goToStore") {
|
|
204
|
+
setHasPlayedOnce(true);
|
|
205
|
+
onDemoCompleted?.({
|
|
206
|
+
reason: "Demo session ended",
|
|
207
|
+
storeUrl
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
window.addEventListener("message", handleGameMessage);
|
|
212
|
+
return () => window.removeEventListener("message", handleGameMessage);
|
|
213
|
+
}, [storeUrl, onDemoCompleted]);
|
|
214
|
+
(0, import_react.useEffect)(() => {
|
|
215
|
+
const handleFullscreenChange = () => {
|
|
216
|
+
const doc = document;
|
|
217
|
+
const isFullscreen = !!(doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement);
|
|
218
|
+
if (isFullscreen) {
|
|
219
|
+
const iframe = iframeRef.current;
|
|
220
|
+
if (iframe) {
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
iframe.focus();
|
|
223
|
+
iframe.contentWindow?.focus();
|
|
224
|
+
}, 200);
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
escapeKeyPressed.current = false;
|
|
228
|
+
if (escapeKeyTimer.current) {
|
|
229
|
+
clearTimeout(escapeKeyTimer.current);
|
|
230
|
+
escapeKeyTimer.current = null;
|
|
231
|
+
}
|
|
232
|
+
setHasPlayedOnce(true);
|
|
233
|
+
onDemoCompleted?.({
|
|
234
|
+
reason: "Demo session ended",
|
|
235
|
+
storeUrl
|
|
236
|
+
});
|
|
117
237
|
}
|
|
118
238
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
239
|
+
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
240
|
+
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
|
|
241
|
+
document.addEventListener("mozfullscreenchange", handleFullscreenChange);
|
|
242
|
+
document.addEventListener("MSFullscreenChange", handleFullscreenChange);
|
|
243
|
+
return () => {
|
|
244
|
+
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
245
|
+
document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
|
|
246
|
+
document.removeEventListener("mozfullscreenchange", handleFullscreenChange);
|
|
247
|
+
document.removeEventListener("MSFullscreenChange", handleFullscreenChange);
|
|
248
|
+
};
|
|
249
|
+
}, [storeUrl, onDemoCompleted, iframeRef]);
|
|
250
|
+
(0, import_react.useEffect)(() => {
|
|
251
|
+
const handleKeyDown = (event) => {
|
|
252
|
+
if (event.key === "Escape" && !escapeKeyPressed.current) {
|
|
253
|
+
escapeKeyPressed.current = true;
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
escapeKeyTimer.current = setTimeout(() => {
|
|
256
|
+
if (document.fullscreenElement) {
|
|
257
|
+
document.exitFullscreen();
|
|
258
|
+
}
|
|
259
|
+
}, ESCAPE_HOLD_DURATION);
|
|
260
|
+
}
|
|
261
|
+
if (event.ctrlKey && event.shiftKey && event.key === "R") {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
if (document.fullscreenElement) {
|
|
264
|
+
document.exitFullscreen().catch(console.warn);
|
|
265
|
+
}
|
|
266
|
+
setTimeout(() => {
|
|
267
|
+
window.location.reload();
|
|
268
|
+
}, 500);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const handleKeyUp = (event) => {
|
|
272
|
+
if (event.key === "Escape" && escapeKeyPressed.current) {
|
|
273
|
+
escapeKeyPressed.current = false;
|
|
274
|
+
if (escapeKeyTimer.current) {
|
|
275
|
+
clearTimeout(escapeKeyTimer.current);
|
|
276
|
+
escapeKeyTimer.current = null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
281
|
+
document.addEventListener("keyup", handleKeyUp);
|
|
282
|
+
return () => {
|
|
283
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
284
|
+
document.removeEventListener("keyup", handleKeyUp);
|
|
285
|
+
};
|
|
286
|
+
}, []);
|
|
287
|
+
(0, import_react.useEffect)(() => {
|
|
288
|
+
if (!self.crossOriginIsolated) {
|
|
289
|
+
console.warn(
|
|
290
|
+
'[PlayruoEmbed] \u26A0\uFE0F Not in a cross-origin isolated context.\nSharedArrayBuffer and high-precision timers may not work.\n\nAdd these headers to your server:\n Cross-Origin-Opener-Policy: same-origin\n Cross-Origin-Embedder-Policy: credentialless\n\nFor Next.js, add to next.config.js:\n async headers() {\n return [{\n source: "/:path*",\n headers: [\n { key: "Cross-Origin-Opener-Policy", value: "same-origin" },\n { key: "Cross-Origin-Embedder-Policy", value: "credentialless" }\n ]\n }];\n }'
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}, []);
|
|
294
|
+
const isAvailable = Boolean(isReady && isCompatible);
|
|
123
295
|
const renderContent = () => {
|
|
124
296
|
if (!renderState) return null;
|
|
125
297
|
const actions = {
|
|
126
|
-
tryNow:
|
|
127
|
-
buyNow:
|
|
128
|
-
canTryNow: Boolean(isReady &&
|
|
129
|
-
canBuyNow: Boolean(isReady
|
|
298
|
+
tryNow: handleTryNow,
|
|
299
|
+
buyNow: handleBuyNow,
|
|
300
|
+
canTryNow: Boolean(isReady && isCompatible),
|
|
301
|
+
canBuyNow: Boolean(isReady)
|
|
130
302
|
};
|
|
131
303
|
if (errorMessage) {
|
|
132
304
|
return renderState({ status: "error", message: errorMessage, actions });
|
|
@@ -134,7 +306,7 @@ var PlayruoEmbed = ({
|
|
|
134
306
|
if (!isReady) {
|
|
135
307
|
return renderState({ status: "loading", actions });
|
|
136
308
|
}
|
|
137
|
-
const message = !isAvailable ?
|
|
309
|
+
const message = !isAvailable ? currentState.tryNow.status : void 0;
|
|
138
310
|
return renderState({
|
|
139
311
|
status: isAvailable ? "available" : "unavailable",
|
|
140
312
|
message,
|
|
@@ -143,21 +315,38 @@ var PlayruoEmbed = ({
|
|
|
143
315
|
};
|
|
144
316
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, children: [
|
|
145
317
|
renderContent(),
|
|
146
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
318
|
+
isCompatible && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
147
319
|
"iframe",
|
|
148
320
|
{
|
|
149
321
|
ref: iframeRef,
|
|
150
322
|
src,
|
|
151
|
-
allow: "fullscreen *; autoplay *; gamepad *",
|
|
323
|
+
allow: "fullscreen *; autoplay *; gamepad *; clipboard-write *; web-share *; encrypted-media *; accelerometer *; gyroscope *",
|
|
152
324
|
className: "w-full h-[70vh] rounded-lg border",
|
|
325
|
+
...{ credentialless: "true" },
|
|
326
|
+
sandbox: "allow-same-origin allow-scripts allow-popups allow-forms allow-presentation allow-top-navigation allow-modals allow-pointer-lock",
|
|
153
327
|
title: "Playruo",
|
|
154
328
|
style: {
|
|
155
329
|
position: showIframe ? void 0 : "absolute",
|
|
156
330
|
top: showIframe ? void 0 : -9999,
|
|
157
331
|
left: showIframe ? void 0 : -9999,
|
|
158
332
|
zIndex: showIframe ? void 0 : -10,
|
|
159
|
-
opacity: showIframe ? 1 : 0,
|
|
333
|
+
opacity: showIframe && showGameIframe ? 1 : 0,
|
|
334
|
+
visibility: showIframe && showGameIframe ? "visible" : "hidden",
|
|
160
335
|
pointerEvents: showIframe ? "auto" : "none"
|
|
336
|
+
},
|
|
337
|
+
onLoad: () => {
|
|
338
|
+
setIsIframeLoaded(true);
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
if (iframeRef.current) {
|
|
341
|
+
iframeRef.current.focus();
|
|
342
|
+
iframeRef.current.contentWindow?.focus();
|
|
343
|
+
}
|
|
344
|
+
}, 500);
|
|
345
|
+
},
|
|
346
|
+
onError: () => {
|
|
347
|
+
console.error("[PlayruoEmbed] Iframe failed to load");
|
|
348
|
+
setErrorMessage("Failed to load game");
|
|
349
|
+
onError?.("Failed to load game");
|
|
161
350
|
}
|
|
162
351
|
}
|
|
163
352
|
)
|
package/dist/index.d.cts
CHANGED
|
@@ -113,6 +113,12 @@ interface BuildUrlParams {
|
|
|
113
113
|
operation?: string;
|
|
114
114
|
storeUrl?: string;
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Build direct game URL (no intermediate /embed page)
|
|
118
|
+
* @example
|
|
119
|
+
* buildEmbedUrl({ baseUrl: "https://playruo.live", operation: "my-game" })
|
|
120
|
+
* // => "https://playruo.live/my-game?iframe=true"
|
|
121
|
+
*/
|
|
116
122
|
declare const buildEmbedUrl: ({ baseUrl, operation, storeUrl }: BuildUrlParams) => string;
|
|
117
123
|
|
|
118
124
|
export { type BuyNowCommand, type DemoCompletedPayload, type GetStateCommand, type PlayruoDemoCompletedMessage, PlayruoEmbed, type PlayruoEmbedActions, type PlayruoEmbedCommand, type PlayruoEmbedMessage, type PlayruoEmbedProps, type PlayruoEmbedRenderState, type PlayruoEmbedState, type PlayruoErrorMessage, type PlayruoPurchaseRequestMessage, type PlayruoReadyMessage, type PlayruoStateUpdateMessage, type TryNowCommand, buildEmbedUrl, isPlayruoEmbedCommand, isPlayruoEmbedMessage };
|
package/dist/index.d.ts
CHANGED
|
@@ -113,6 +113,12 @@ interface BuildUrlParams {
|
|
|
113
113
|
operation?: string;
|
|
114
114
|
storeUrl?: string;
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Build direct game URL (no intermediate /embed page)
|
|
118
|
+
* @example
|
|
119
|
+
* buildEmbedUrl({ baseUrl: "https://playruo.live", operation: "my-game" })
|
|
120
|
+
* // => "https://playruo.live/my-game?iframe=true"
|
|
121
|
+
*/
|
|
116
122
|
declare const buildEmbedUrl: ({ baseUrl, operation, storeUrl }: BuildUrlParams) => string;
|
|
117
123
|
|
|
118
124
|
export { type BuyNowCommand, type DemoCompletedPayload, type GetStateCommand, type PlayruoDemoCompletedMessage, PlayruoEmbed, type PlayruoEmbedActions, type PlayruoEmbedCommand, type PlayruoEmbedMessage, type PlayruoEmbedProps, type PlayruoEmbedRenderState, type PlayruoEmbedState, type PlayruoErrorMessage, type PlayruoPurchaseRequestMessage, type PlayruoReadyMessage, type PlayruoStateUpdateMessage, type TryNowCommand, buildEmbedUrl, isPlayruoEmbedCommand, isPlayruoEmbedMessage };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// src/react/playruo-embed.tsx
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
|
3
3
|
|
|
4
4
|
// src/url.ts
|
|
5
5
|
var buildEmbedUrl = ({ baseUrl, operation = "iframe", storeUrl }) => {
|
|
6
|
-
const
|
|
7
|
-
url
|
|
8
|
-
|
|
6
|
+
const operationPath = operation.startsWith("/") ? operation.slice(1) : operation;
|
|
7
|
+
const url = new URL(`/${operationPath}`, baseUrl);
|
|
8
|
+
url.searchParams.set("iframe", "true");
|
|
9
|
+
if (storeUrl) {
|
|
10
|
+
url.searchParams.set("storeUrl", storeUrl);
|
|
11
|
+
}
|
|
9
12
|
return url.toString();
|
|
10
13
|
};
|
|
11
14
|
|
|
@@ -27,10 +30,63 @@ var PlayruoEmbed = ({
|
|
|
27
30
|
}) => {
|
|
28
31
|
const internalIframeRef = useRef(null);
|
|
29
32
|
const iframeRef = externalIframeRef || internalIframeRef;
|
|
30
|
-
const [
|
|
31
|
-
const [
|
|
33
|
+
const [isIframeLoaded, setIsIframeLoaded] = useState(false);
|
|
34
|
+
const [hasPlayedOnce, setHasPlayedOnce] = useState(false);
|
|
35
|
+
const [showGameIframe, setShowGameIframe] = useState(false);
|
|
36
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
37
|
+
const [isValidChromium, setIsValidChromium] = useState(true);
|
|
32
38
|
const [errorMessage, setErrorMessage] = useState(null);
|
|
39
|
+
const escapeKeyTimer = useRef(null);
|
|
40
|
+
const escapeKeyPressed = useRef(false);
|
|
41
|
+
const ESCAPE_HOLD_DURATION = 1500;
|
|
33
42
|
const src = useMemo(() => buildEmbedUrl({ baseUrl, operation, storeUrl }), [baseUrl, operation, storeUrl]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (typeof window === "undefined") return;
|
|
45
|
+
const detectMobile = () => {
|
|
46
|
+
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
|
47
|
+
return /android|iPad|iPhone|iPod/i.test(userAgent);
|
|
48
|
+
};
|
|
49
|
+
const detectChromium = () => {
|
|
50
|
+
const hasChrome = !!window.chrome;
|
|
51
|
+
const isChromium = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
|
52
|
+
return hasChrome || isChromium;
|
|
53
|
+
};
|
|
54
|
+
setIsMobile(detectMobile());
|
|
55
|
+
setIsValidChromium(detectChromium());
|
|
56
|
+
}, []);
|
|
57
|
+
const isCompatible = !isMobile && isValidChromium;
|
|
58
|
+
const isReady = isCompatible ? isIframeLoaded : true;
|
|
59
|
+
const currentState = useMemo(() => ({
|
|
60
|
+
hasPlayedOnce,
|
|
61
|
+
tryNow: {
|
|
62
|
+
available: isCompatible,
|
|
63
|
+
status: !isCompatible ? "Browser not compatible" : hasPlayedOnce ? "Click to replay" : "Ready"
|
|
64
|
+
},
|
|
65
|
+
buyNow: {
|
|
66
|
+
available: true,
|
|
67
|
+
status: "Ready"
|
|
68
|
+
}
|
|
69
|
+
}), [hasPlayedOnce, isCompatible]);
|
|
70
|
+
const checkKeyboardLockPermission = async () => {
|
|
71
|
+
if (!navigator.permissions) {
|
|
72
|
+
console.log("[PlayruoEmbed] Permissions API not supported");
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const { state } = await navigator.permissions.query({
|
|
77
|
+
name: "keyboard-lock"
|
|
78
|
+
});
|
|
79
|
+
if (state === "granted" || state === "prompt") {
|
|
80
|
+
console.log(`[PlayruoEmbed] Keyboard lock permission: ${state}`);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
console.log("[PlayruoEmbed] Keyboard lock permission denied");
|
|
84
|
+
return false;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.warn("[PlayruoEmbed] Error checking keyboard lock permission:", error);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
34
90
|
const requestFullscreenWithKeyboardLock = async () => {
|
|
35
91
|
const iframe = iframeRef.current;
|
|
36
92
|
if (!iframe?.requestFullscreen) {
|
|
@@ -40,11 +96,17 @@ var PlayruoEmbed = ({
|
|
|
40
96
|
try {
|
|
41
97
|
const fullscreenPromise = iframe.requestFullscreen();
|
|
42
98
|
if (navigator.keyboard) {
|
|
43
|
-
|
|
44
|
-
|
|
99
|
+
const permissionPromise = checkKeyboardLockPermission();
|
|
100
|
+
const keyboardLockPromise = navigator.keyboard.lock(["Escape"]);
|
|
101
|
+
void Promise.all([permissionPromise, keyboardLockPromise]).then(([hasPermission]) => {
|
|
102
|
+
if (hasPermission) {
|
|
103
|
+
console.log("[PlayruoEmbed] Keyboard locked successfully");
|
|
104
|
+
}
|
|
45
105
|
}).catch((err) => {
|
|
46
106
|
console.warn("[PlayruoEmbed] Keyboard lock failed:", err);
|
|
47
107
|
});
|
|
108
|
+
} else {
|
|
109
|
+
console.log("[PlayruoEmbed] Keyboard API not supported");
|
|
48
110
|
}
|
|
49
111
|
void fullscreenPromise.then(() => {
|
|
50
112
|
console.log("[PlayruoEmbed] Fullscreen activated successfully");
|
|
@@ -57,47 +119,157 @@ var PlayruoEmbed = ({
|
|
|
57
119
|
return false;
|
|
58
120
|
}
|
|
59
121
|
};
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
-
console.warn(
|
|
122
|
+
const handleTryNow = useCallback(() => {
|
|
123
|
+
if (!isCompatible) {
|
|
124
|
+
console.warn("[PlayruoEmbed] Browser not compatible");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const iframe = iframeRef.current;
|
|
128
|
+
if (!iframe) {
|
|
129
|
+
console.warn("[PlayruoEmbed] Iframe not ready");
|
|
63
130
|
return;
|
|
64
131
|
}
|
|
65
|
-
|
|
132
|
+
setShowGameIframe(true);
|
|
133
|
+
try {
|
|
66
134
|
requestFullscreenWithKeyboardLock();
|
|
135
|
+
iframe.contentWindow?.postMessage({
|
|
136
|
+
type: "playruo",
|
|
137
|
+
action: "tryNowClick",
|
|
138
|
+
fromEmbed: true,
|
|
139
|
+
isFullscreen: true,
|
|
140
|
+
isReplay: hasPlayedOnce
|
|
141
|
+
}, "*");
|
|
142
|
+
console.log("[PlayruoEmbed] Sent tryNowClick to game", hasPlayedOnce ? "(replay)" : "(first time)");
|
|
143
|
+
iframe.focus();
|
|
144
|
+
requestAnimationFrame(() => {
|
|
145
|
+
iframe.contentWindow?.focus();
|
|
146
|
+
});
|
|
147
|
+
setHasPlayedOnce(true);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error("[PlayruoEmbed] Failed to start game:", error);
|
|
150
|
+
setErrorMessage("Failed to start game");
|
|
151
|
+
onError?.("Failed to start game");
|
|
67
152
|
}
|
|
68
|
-
|
|
69
|
-
|
|
153
|
+
}, [isCompatible, hasPlayedOnce, onError, iframeRef]);
|
|
154
|
+
const handleBuyNow = useCallback(() => {
|
|
155
|
+
if (!storeUrl) {
|
|
156
|
+
console.warn("[PlayruoEmbed] No store URL provided");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
onPurchaseRequest?.({ storeUrl });
|
|
160
|
+
}, [storeUrl, onPurchaseRequest]);
|
|
70
161
|
useEffect(() => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
162
|
+
if (isReady) {
|
|
163
|
+
onStateChange?.(currentState);
|
|
164
|
+
}
|
|
165
|
+
}, [isReady, currentState, onStateChange]);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (isReady) {
|
|
168
|
+
onReady?.();
|
|
169
|
+
}
|
|
170
|
+
}, [isReady, onReady]);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const handleGameMessage = (event) => {
|
|
173
|
+
const { type } = event.data || {};
|
|
174
|
+
if (type === "error" || type === "closeForBuy" || type === "goToStore") {
|
|
175
|
+
setHasPlayedOnce(true);
|
|
176
|
+
onDemoCompleted?.({
|
|
177
|
+
reason: "Demo session ended",
|
|
178
|
+
storeUrl
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
window.addEventListener("message", handleGameMessage);
|
|
183
|
+
return () => window.removeEventListener("message", handleGameMessage);
|
|
184
|
+
}, [storeUrl, onDemoCompleted]);
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
const handleFullscreenChange = () => {
|
|
187
|
+
const doc = document;
|
|
188
|
+
const isFullscreen = !!(doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement);
|
|
189
|
+
if (isFullscreen) {
|
|
190
|
+
const iframe = iframeRef.current;
|
|
191
|
+
if (iframe) {
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
iframe.focus();
|
|
194
|
+
iframe.contentWindow?.focus();
|
|
195
|
+
}, 200);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
escapeKeyPressed.current = false;
|
|
199
|
+
if (escapeKeyTimer.current) {
|
|
200
|
+
clearTimeout(escapeKeyTimer.current);
|
|
201
|
+
escapeKeyTimer.current = null;
|
|
202
|
+
}
|
|
203
|
+
setHasPlayedOnce(true);
|
|
204
|
+
onDemoCompleted?.({
|
|
205
|
+
reason: "Demo session ended",
|
|
206
|
+
storeUrl
|
|
207
|
+
});
|
|
88
208
|
}
|
|
89
209
|
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
210
|
+
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
211
|
+
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
|
|
212
|
+
document.addEventListener("mozfullscreenchange", handleFullscreenChange);
|
|
213
|
+
document.addEventListener("MSFullscreenChange", handleFullscreenChange);
|
|
214
|
+
return () => {
|
|
215
|
+
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
216
|
+
document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
|
|
217
|
+
document.removeEventListener("mozfullscreenchange", handleFullscreenChange);
|
|
218
|
+
document.removeEventListener("MSFullscreenChange", handleFullscreenChange);
|
|
219
|
+
};
|
|
220
|
+
}, [storeUrl, onDemoCompleted, iframeRef]);
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
const handleKeyDown = (event) => {
|
|
223
|
+
if (event.key === "Escape" && !escapeKeyPressed.current) {
|
|
224
|
+
escapeKeyPressed.current = true;
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
escapeKeyTimer.current = setTimeout(() => {
|
|
227
|
+
if (document.fullscreenElement) {
|
|
228
|
+
document.exitFullscreen();
|
|
229
|
+
}
|
|
230
|
+
}, ESCAPE_HOLD_DURATION);
|
|
231
|
+
}
|
|
232
|
+
if (event.ctrlKey && event.shiftKey && event.key === "R") {
|
|
233
|
+
event.preventDefault();
|
|
234
|
+
if (document.fullscreenElement) {
|
|
235
|
+
document.exitFullscreen().catch(console.warn);
|
|
236
|
+
}
|
|
237
|
+
setTimeout(() => {
|
|
238
|
+
window.location.reload();
|
|
239
|
+
}, 500);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const handleKeyUp = (event) => {
|
|
243
|
+
if (event.key === "Escape" && escapeKeyPressed.current) {
|
|
244
|
+
escapeKeyPressed.current = false;
|
|
245
|
+
if (escapeKeyTimer.current) {
|
|
246
|
+
clearTimeout(escapeKeyTimer.current);
|
|
247
|
+
escapeKeyTimer.current = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
252
|
+
document.addEventListener("keyup", handleKeyUp);
|
|
253
|
+
return () => {
|
|
254
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
255
|
+
document.removeEventListener("keyup", handleKeyUp);
|
|
256
|
+
};
|
|
257
|
+
}, []);
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
if (!self.crossOriginIsolated) {
|
|
260
|
+
console.warn(
|
|
261
|
+
'[PlayruoEmbed] \u26A0\uFE0F Not in a cross-origin isolated context.\nSharedArrayBuffer and high-precision timers may not work.\n\nAdd these headers to your server:\n Cross-Origin-Opener-Policy: same-origin\n Cross-Origin-Embedder-Policy: credentialless\n\nFor Next.js, add to next.config.js:\n async headers() {\n return [{\n source: "/:path*",\n headers: [\n { key: "Cross-Origin-Opener-Policy", value: "same-origin" },\n { key: "Cross-Origin-Embedder-Policy", value: "credentialless" }\n ]\n }];\n }'
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}, []);
|
|
265
|
+
const isAvailable = Boolean(isReady && isCompatible);
|
|
94
266
|
const renderContent = () => {
|
|
95
267
|
if (!renderState) return null;
|
|
96
268
|
const actions = {
|
|
97
|
-
tryNow:
|
|
98
|
-
buyNow:
|
|
99
|
-
canTryNow: Boolean(isReady &&
|
|
100
|
-
canBuyNow: Boolean(isReady
|
|
269
|
+
tryNow: handleTryNow,
|
|
270
|
+
buyNow: handleBuyNow,
|
|
271
|
+
canTryNow: Boolean(isReady && isCompatible),
|
|
272
|
+
canBuyNow: Boolean(isReady)
|
|
101
273
|
};
|
|
102
274
|
if (errorMessage) {
|
|
103
275
|
return renderState({ status: "error", message: errorMessage, actions });
|
|
@@ -105,7 +277,7 @@ var PlayruoEmbed = ({
|
|
|
105
277
|
if (!isReady) {
|
|
106
278
|
return renderState({ status: "loading", actions });
|
|
107
279
|
}
|
|
108
|
-
const message = !isAvailable ?
|
|
280
|
+
const message = !isAvailable ? currentState.tryNow.status : void 0;
|
|
109
281
|
return renderState({
|
|
110
282
|
status: isAvailable ? "available" : "unavailable",
|
|
111
283
|
message,
|
|
@@ -114,21 +286,38 @@ var PlayruoEmbed = ({
|
|
|
114
286
|
};
|
|
115
287
|
return /* @__PURE__ */ jsxs("div", { className, children: [
|
|
116
288
|
renderContent(),
|
|
117
|
-
/* @__PURE__ */ jsx(
|
|
289
|
+
isCompatible && /* @__PURE__ */ jsx(
|
|
118
290
|
"iframe",
|
|
119
291
|
{
|
|
120
292
|
ref: iframeRef,
|
|
121
293
|
src,
|
|
122
|
-
allow: "fullscreen *; autoplay *; gamepad *",
|
|
294
|
+
allow: "fullscreen *; autoplay *; gamepad *; clipboard-write *; web-share *; encrypted-media *; accelerometer *; gyroscope *",
|
|
123
295
|
className: "w-full h-[70vh] rounded-lg border",
|
|
296
|
+
...{ credentialless: "true" },
|
|
297
|
+
sandbox: "allow-same-origin allow-scripts allow-popups allow-forms allow-presentation allow-top-navigation allow-modals allow-pointer-lock",
|
|
124
298
|
title: "Playruo",
|
|
125
299
|
style: {
|
|
126
300
|
position: showIframe ? void 0 : "absolute",
|
|
127
301
|
top: showIframe ? void 0 : -9999,
|
|
128
302
|
left: showIframe ? void 0 : -9999,
|
|
129
303
|
zIndex: showIframe ? void 0 : -10,
|
|
130
|
-
opacity: showIframe ? 1 : 0,
|
|
304
|
+
opacity: showIframe && showGameIframe ? 1 : 0,
|
|
305
|
+
visibility: showIframe && showGameIframe ? "visible" : "hidden",
|
|
131
306
|
pointerEvents: showIframe ? "auto" : "none"
|
|
307
|
+
},
|
|
308
|
+
onLoad: () => {
|
|
309
|
+
setIsIframeLoaded(true);
|
|
310
|
+
setTimeout(() => {
|
|
311
|
+
if (iframeRef.current) {
|
|
312
|
+
iframeRef.current.focus();
|
|
313
|
+
iframeRef.current.contentWindow?.focus();
|
|
314
|
+
}
|
|
315
|
+
}, 500);
|
|
316
|
+
},
|
|
317
|
+
onError: () => {
|
|
318
|
+
console.error("[PlayruoEmbed] Iframe failed to load");
|
|
319
|
+
setErrorMessage("Failed to load game");
|
|
320
|
+
onError?.("Failed to load game");
|
|
132
321
|
}
|
|
133
322
|
}
|
|
134
323
|
)
|