@playruo/embed 1.1.5 → 1.2.1
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 +204 -47
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +205 -48
- 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,21 +59,54 @@ 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]);
|
|
63
99
|
const checkKeyboardLockPermission = async () => {
|
|
64
100
|
if (!navigator.permissions) {
|
|
65
101
|
console.log("[PlayruoEmbed] Permissions API not supported");
|
|
66
102
|
return false;
|
|
67
103
|
}
|
|
68
104
|
try {
|
|
69
|
-
const { state
|
|
105
|
+
const { state } = await navigator.permissions.query({
|
|
70
106
|
name: "keyboard-lock"
|
|
71
107
|
});
|
|
72
|
-
if (
|
|
73
|
-
console.log(`[PlayruoEmbed] Keyboard lock permission: ${
|
|
108
|
+
if (state === "granted" || state === "prompt") {
|
|
109
|
+
console.log(`[PlayruoEmbed] Keyboard lock permission: ${state}`);
|
|
74
110
|
return true;
|
|
75
111
|
}
|
|
76
112
|
console.log("[PlayruoEmbed] Keyboard lock permission denied");
|
|
@@ -112,54 +148,157 @@ var PlayruoEmbed = ({
|
|
|
112
148
|
return false;
|
|
113
149
|
}
|
|
114
150
|
};
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
117
|
-
console.warn(
|
|
151
|
+
const handleTryNow = (0, import_react.useCallback)(() => {
|
|
152
|
+
if (!isCompatible) {
|
|
153
|
+
console.warn("[PlayruoEmbed] Browser not compatible");
|
|
118
154
|
return;
|
|
119
155
|
}
|
|
120
|
-
|
|
156
|
+
const iframe = iframeRef.current;
|
|
157
|
+
if (!iframe) {
|
|
158
|
+
console.warn("[PlayruoEmbed] Iframe not ready");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
setShowGameIframe(true);
|
|
162
|
+
try {
|
|
121
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");
|
|
122
181
|
}
|
|
123
|
-
|
|
124
|
-
|
|
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]);
|
|
190
|
+
(0, import_react.useEffect)(() => {
|
|
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
|
+
});
|
|
237
|
+
}
|
|
238
|
+
};
|
|
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
|
+
}, []);
|
|
125
287
|
(0, import_react.useEffect)(() => {
|
|
126
288
|
if (!self.crossOriginIsolated) {
|
|
127
289
|
console.warn(
|
|
128
|
-
'[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\
|
|
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 }'
|
|
129
291
|
);
|
|
130
292
|
}
|
|
131
293
|
}, []);
|
|
132
|
-
|
|
133
|
-
const handler = (event) => {
|
|
134
|
-
const msg = event?.data ?? {};
|
|
135
|
-
if (!msg || typeof msg.type !== "string" || !msg.type.startsWith("playruo_")) return;
|
|
136
|
-
if (msg.type === "playruo_ready") {
|
|
137
|
-
setIsReady(true);
|
|
138
|
-
onReady?.();
|
|
139
|
-
} else if (msg.type === "playruo_state_update") {
|
|
140
|
-
setState(msg.data);
|
|
141
|
-
onStateChange?.(msg.data);
|
|
142
|
-
} else if (msg.type === "playruo_demo_completed") {
|
|
143
|
-
onDemoCompleted?.(msg.data);
|
|
144
|
-
} else if (msg.type === "playruo_purchase_request") {
|
|
145
|
-
onPurchaseRequest?.(msg.data);
|
|
146
|
-
} else if (msg.type === "playruo_error") {
|
|
147
|
-
const message = msg.data?.message ?? "Unknown error";
|
|
148
|
-
setErrorMessage(message);
|
|
149
|
-
onError?.(message);
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
window.addEventListener("message", handler);
|
|
153
|
-
return () => window.removeEventListener("message", handler);
|
|
154
|
-
}, [onReady, onStateChange, onDemoCompleted, onPurchaseRequest, onError]);
|
|
155
|
-
const isAvailable = Boolean(isReady && state?.tryNow?.available);
|
|
294
|
+
const isAvailable = Boolean(isReady && isCompatible);
|
|
156
295
|
const renderContent = () => {
|
|
157
296
|
if (!renderState) return null;
|
|
158
297
|
const actions = {
|
|
159
|
-
tryNow:
|
|
160
|
-
buyNow:
|
|
161
|
-
canTryNow: Boolean(isReady &&
|
|
162
|
-
canBuyNow: Boolean(isReady
|
|
298
|
+
tryNow: handleTryNow,
|
|
299
|
+
buyNow: handleBuyNow,
|
|
300
|
+
canTryNow: Boolean(isReady && isCompatible),
|
|
301
|
+
canBuyNow: Boolean(isReady)
|
|
163
302
|
};
|
|
164
303
|
if (errorMessage) {
|
|
165
304
|
return renderState({ status: "error", message: errorMessage, actions });
|
|
@@ -167,7 +306,7 @@ var PlayruoEmbed = ({
|
|
|
167
306
|
if (!isReady) {
|
|
168
307
|
return renderState({ status: "loading", actions });
|
|
169
308
|
}
|
|
170
|
-
const message = !isAvailable ?
|
|
309
|
+
const message = !isAvailable ? currentState.tryNow.status : void 0;
|
|
171
310
|
return renderState({
|
|
172
311
|
status: isAvailable ? "available" : "unavailable",
|
|
173
312
|
message,
|
|
@@ -176,22 +315,40 @@ var PlayruoEmbed = ({
|
|
|
176
315
|
};
|
|
177
316
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, children: [
|
|
178
317
|
renderContent(),
|
|
179
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
318
|
+
isCompatible && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
180
319
|
"iframe",
|
|
181
320
|
{
|
|
182
321
|
ref: iframeRef,
|
|
183
322
|
src,
|
|
184
|
-
allow: "fullscreen *; autoplay *; gamepad *; clipboard-write *; web-share *; encrypted-media *; accelerometer *; gyroscope *",
|
|
323
|
+
allow: "cross-origin-isolated; fullscreen *; autoplay *; gamepad *; clipboard-write *; web-share *; encrypted-media *; accelerometer *; gyroscope *",
|
|
324
|
+
allowFullScreen: true,
|
|
185
325
|
className: "w-full h-[70vh] rounded-lg border",
|
|
186
326
|
...{ credentialless: "true" },
|
|
327
|
+
sandbox: "allow-same-origin allow-scripts allow-popups allow-forms allow-presentation allow-top-navigation allow-modals allow-pointer-lock",
|
|
187
328
|
title: "Playruo",
|
|
329
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
188
330
|
style: {
|
|
189
331
|
position: showIframe ? void 0 : "absolute",
|
|
190
332
|
top: showIframe ? void 0 : -9999,
|
|
191
333
|
left: showIframe ? void 0 : -9999,
|
|
192
334
|
zIndex: showIframe ? void 0 : -10,
|
|
193
|
-
opacity: showIframe ? 1 : 0,
|
|
335
|
+
opacity: showIframe && showGameIframe ? 1 : 0,
|
|
336
|
+
visibility: showIframe && showGameIframe ? "visible" : "hidden",
|
|
194
337
|
pointerEvents: showIframe ? "auto" : "none"
|
|
338
|
+
},
|
|
339
|
+
onLoad: () => {
|
|
340
|
+
setIsIframeLoaded(true);
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
if (iframeRef.current) {
|
|
343
|
+
iframeRef.current.focus();
|
|
344
|
+
iframeRef.current.contentWindow?.focus();
|
|
345
|
+
}
|
|
346
|
+
}, 500);
|
|
347
|
+
},
|
|
348
|
+
onError: () => {
|
|
349
|
+
console.error("[PlayruoEmbed] Iframe failed to load");
|
|
350
|
+
setErrorMessage("Failed to load game");
|
|
351
|
+
onError?.("Failed to load game");
|
|
195
352
|
}
|
|
196
353
|
}
|
|
197
354
|
)
|
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,21 +30,54 @@ 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]);
|
|
34
70
|
const checkKeyboardLockPermission = async () => {
|
|
35
71
|
if (!navigator.permissions) {
|
|
36
72
|
console.log("[PlayruoEmbed] Permissions API not supported");
|
|
37
73
|
return false;
|
|
38
74
|
}
|
|
39
75
|
try {
|
|
40
|
-
const { state
|
|
76
|
+
const { state } = await navigator.permissions.query({
|
|
41
77
|
name: "keyboard-lock"
|
|
42
78
|
});
|
|
43
|
-
if (
|
|
44
|
-
console.log(`[PlayruoEmbed] Keyboard lock permission: ${
|
|
79
|
+
if (state === "granted" || state === "prompt") {
|
|
80
|
+
console.log(`[PlayruoEmbed] Keyboard lock permission: ${state}`);
|
|
45
81
|
return true;
|
|
46
82
|
}
|
|
47
83
|
console.log("[PlayruoEmbed] Keyboard lock permission denied");
|
|
@@ -83,54 +119,157 @@ var PlayruoEmbed = ({
|
|
|
83
119
|
return false;
|
|
84
120
|
}
|
|
85
121
|
};
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
88
|
-
console.warn(
|
|
122
|
+
const handleTryNow = useCallback(() => {
|
|
123
|
+
if (!isCompatible) {
|
|
124
|
+
console.warn("[PlayruoEmbed] Browser not compatible");
|
|
89
125
|
return;
|
|
90
126
|
}
|
|
91
|
-
|
|
127
|
+
const iframe = iframeRef.current;
|
|
128
|
+
if (!iframe) {
|
|
129
|
+
console.warn("[PlayruoEmbed] Iframe not ready");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
setShowGameIframe(true);
|
|
133
|
+
try {
|
|
92
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");
|
|
93
152
|
}
|
|
94
|
-
|
|
95
|
-
|
|
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]);
|
|
161
|
+
useEffect(() => {
|
|
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
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
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
|
+
}, []);
|
|
96
258
|
useEffect(() => {
|
|
97
259
|
if (!self.crossOriginIsolated) {
|
|
98
260
|
console.warn(
|
|
99
|
-
'[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\
|
|
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 }'
|
|
100
262
|
);
|
|
101
263
|
}
|
|
102
264
|
}, []);
|
|
103
|
-
|
|
104
|
-
const handler = (event) => {
|
|
105
|
-
const msg = event?.data ?? {};
|
|
106
|
-
if (!msg || typeof msg.type !== "string" || !msg.type.startsWith("playruo_")) return;
|
|
107
|
-
if (msg.type === "playruo_ready") {
|
|
108
|
-
setIsReady(true);
|
|
109
|
-
onReady?.();
|
|
110
|
-
} else if (msg.type === "playruo_state_update") {
|
|
111
|
-
setState(msg.data);
|
|
112
|
-
onStateChange?.(msg.data);
|
|
113
|
-
} else if (msg.type === "playruo_demo_completed") {
|
|
114
|
-
onDemoCompleted?.(msg.data);
|
|
115
|
-
} else if (msg.type === "playruo_purchase_request") {
|
|
116
|
-
onPurchaseRequest?.(msg.data);
|
|
117
|
-
} else if (msg.type === "playruo_error") {
|
|
118
|
-
const message = msg.data?.message ?? "Unknown error";
|
|
119
|
-
setErrorMessage(message);
|
|
120
|
-
onError?.(message);
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
window.addEventListener("message", handler);
|
|
124
|
-
return () => window.removeEventListener("message", handler);
|
|
125
|
-
}, [onReady, onStateChange, onDemoCompleted, onPurchaseRequest, onError]);
|
|
126
|
-
const isAvailable = Boolean(isReady && state?.tryNow?.available);
|
|
265
|
+
const isAvailable = Boolean(isReady && isCompatible);
|
|
127
266
|
const renderContent = () => {
|
|
128
267
|
if (!renderState) return null;
|
|
129
268
|
const actions = {
|
|
130
|
-
tryNow:
|
|
131
|
-
buyNow:
|
|
132
|
-
canTryNow: Boolean(isReady &&
|
|
133
|
-
canBuyNow: Boolean(isReady
|
|
269
|
+
tryNow: handleTryNow,
|
|
270
|
+
buyNow: handleBuyNow,
|
|
271
|
+
canTryNow: Boolean(isReady && isCompatible),
|
|
272
|
+
canBuyNow: Boolean(isReady)
|
|
134
273
|
};
|
|
135
274
|
if (errorMessage) {
|
|
136
275
|
return renderState({ status: "error", message: errorMessage, actions });
|
|
@@ -138,7 +277,7 @@ var PlayruoEmbed = ({
|
|
|
138
277
|
if (!isReady) {
|
|
139
278
|
return renderState({ status: "loading", actions });
|
|
140
279
|
}
|
|
141
|
-
const message = !isAvailable ?
|
|
280
|
+
const message = !isAvailable ? currentState.tryNow.status : void 0;
|
|
142
281
|
return renderState({
|
|
143
282
|
status: isAvailable ? "available" : "unavailable",
|
|
144
283
|
message,
|
|
@@ -147,22 +286,40 @@ var PlayruoEmbed = ({
|
|
|
147
286
|
};
|
|
148
287
|
return /* @__PURE__ */ jsxs("div", { className, children: [
|
|
149
288
|
renderContent(),
|
|
150
|
-
/* @__PURE__ */ jsx(
|
|
289
|
+
isCompatible && /* @__PURE__ */ jsx(
|
|
151
290
|
"iframe",
|
|
152
291
|
{
|
|
153
292
|
ref: iframeRef,
|
|
154
293
|
src,
|
|
155
|
-
allow: "fullscreen *; autoplay *; gamepad *; clipboard-write *; web-share *; encrypted-media *; accelerometer *; gyroscope *",
|
|
294
|
+
allow: "cross-origin-isolated; fullscreen *; autoplay *; gamepad *; clipboard-write *; web-share *; encrypted-media *; accelerometer *; gyroscope *",
|
|
295
|
+
allowFullScreen: true,
|
|
156
296
|
className: "w-full h-[70vh] rounded-lg border",
|
|
157
297
|
...{ credentialless: "true" },
|
|
298
|
+
sandbox: "allow-same-origin allow-scripts allow-popups allow-forms allow-presentation allow-top-navigation allow-modals allow-pointer-lock",
|
|
158
299
|
title: "Playruo",
|
|
300
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
159
301
|
style: {
|
|
160
302
|
position: showIframe ? void 0 : "absolute",
|
|
161
303
|
top: showIframe ? void 0 : -9999,
|
|
162
304
|
left: showIframe ? void 0 : -9999,
|
|
163
305
|
zIndex: showIframe ? void 0 : -10,
|
|
164
|
-
opacity: showIframe ? 1 : 0,
|
|
306
|
+
opacity: showIframe && showGameIframe ? 1 : 0,
|
|
307
|
+
visibility: showIframe && showGameIframe ? "visible" : "hidden",
|
|
165
308
|
pointerEvents: showIframe ? "auto" : "none"
|
|
309
|
+
},
|
|
310
|
+
onLoad: () => {
|
|
311
|
+
setIsIframeLoaded(true);
|
|
312
|
+
setTimeout(() => {
|
|
313
|
+
if (iframeRef.current) {
|
|
314
|
+
iframeRef.current.focus();
|
|
315
|
+
iframeRef.current.contentWindow?.focus();
|
|
316
|
+
}
|
|
317
|
+
}, 500);
|
|
318
|
+
},
|
|
319
|
+
onError: () => {
|
|
320
|
+
console.error("[PlayruoEmbed] Iframe failed to load");
|
|
321
|
+
setErrorMessage("Failed to load game");
|
|
322
|
+
onError?.("Failed to load game");
|
|
166
323
|
}
|
|
167
324
|
}
|
|
168
325
|
)
|