@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 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 url = new URL("/embed", baseUrl);
36
- url.searchParams.set("operation", operation);
37
- if (storeUrl) url.searchParams.set("storeUrl", storeUrl);
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 [isReady, setIsReady] = (0, import_react.useState)(false);
60
- const [state, setState] = (0, import_react.useState)(null);
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
- navigator.keyboard.lock(["Escape"]).then(() => {
73
- console.log("[PlayruoEmbed] Keyboard locked successfully");
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 sendCommand = (type) => {
90
- if (!iframeRef.current?.contentWindow) {
91
- console.warn(`PlayruoEmbed: Cannot send ${type} command, iframe not ready`);
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
- if (type === "try_now") {
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
- iframeRef.current.contentWindow.postMessage({ type }, "*");
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
- const handler = (event) => {
101
- const msg = event?.data ?? {};
102
- if (!msg || typeof msg.type !== "string" || !msg.type.startsWith("playruo_")) return;
103
- if (msg.type === "playruo_ready") {
104
- setIsReady(true);
105
- onReady?.();
106
- } else if (msg.type === "playruo_state_update") {
107
- setState(msg.data);
108
- onStateChange?.(msg.data);
109
- } else if (msg.type === "playruo_demo_completed") {
110
- onDemoCompleted?.(msg.data);
111
- } else if (msg.type === "playruo_purchase_request") {
112
- onPurchaseRequest?.(msg.data);
113
- } else if (msg.type === "playruo_error") {
114
- const message = msg.data?.message ?? "Unknown error";
115
- setErrorMessage(message);
116
- onError?.(message);
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
- window.addEventListener("message", handler);
120
- return () => window.removeEventListener("message", handler);
121
- }, [onReady, onStateChange, onDemoCompleted, onPurchaseRequest, onError]);
122
- const isAvailable = Boolean(isReady && state?.tryNow?.available);
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: () => sendCommand("try_now"),
127
- buyNow: () => sendCommand("buy_now"),
128
- canTryNow: Boolean(isReady && state?.tryNow?.available),
129
- canBuyNow: Boolean(isReady && state?.buyNow?.available)
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 ? state?.tryNow?.status ?? state?.buyNow?.status : void 0;
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 url = new URL("/embed", baseUrl);
7
- url.searchParams.set("operation", operation);
8
- if (storeUrl) url.searchParams.set("storeUrl", storeUrl);
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 [isReady, setIsReady] = useState(false);
31
- const [state, setState] = useState(null);
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
- navigator.keyboard.lock(["Escape"]).then(() => {
44
- console.log("[PlayruoEmbed] Keyboard locked successfully");
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 sendCommand = (type) => {
61
- if (!iframeRef.current?.contentWindow) {
62
- console.warn(`PlayruoEmbed: Cannot send ${type} command, iframe not ready`);
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
- if (type === "try_now") {
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
- iframeRef.current.contentWindow.postMessage({ type }, "*");
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
- const handler = (event) => {
72
- const msg = event?.data ?? {};
73
- if (!msg || typeof msg.type !== "string" || !msg.type.startsWith("playruo_")) return;
74
- if (msg.type === "playruo_ready") {
75
- setIsReady(true);
76
- onReady?.();
77
- } else if (msg.type === "playruo_state_update") {
78
- setState(msg.data);
79
- onStateChange?.(msg.data);
80
- } else if (msg.type === "playruo_demo_completed") {
81
- onDemoCompleted?.(msg.data);
82
- } else if (msg.type === "playruo_purchase_request") {
83
- onPurchaseRequest?.(msg.data);
84
- } else if (msg.type === "playruo_error") {
85
- const message = msg.data?.message ?? "Unknown error";
86
- setErrorMessage(message);
87
- onError?.(message);
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
- window.addEventListener("message", handler);
91
- return () => window.removeEventListener("message", handler);
92
- }, [onReady, onStateChange, onDemoCompleted, onPurchaseRequest, onError]);
93
- const isAvailable = Boolean(isReady && state?.tryNow?.available);
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: () => sendCommand("try_now"),
98
- buyNow: () => sendCommand("buy_now"),
99
- canTryNow: Boolean(isReady && state?.tryNow?.available),
100
- canBuyNow: Boolean(isReady && state?.buyNow?.available)
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 ? state?.tryNow?.status ?? state?.buyNow?.status : void 0;
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
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playruo/embed",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "React SDK to embed Playruo with postMessage API.",
5
5
  "license": "MIT",
6
6
  "type": "module",