@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 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,21 +59,54 @@ 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]);
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: state2 } = await navigator.permissions.query({
105
+ const { state } = await navigator.permissions.query({
70
106
  name: "keyboard-lock"
71
107
  });
72
- if (state2 === "granted" || state2 === "prompt") {
73
- console.log(`[PlayruoEmbed] Keyboard lock permission: ${state2}`);
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 sendCommand = (type) => {
116
- if (!iframeRef.current?.contentWindow) {
117
- 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");
118
154
  return;
119
155
  }
120
- if (type === "try_now") {
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
- iframeRef.current.contentWindow.postMessage({ type }, "*");
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\nAlternative (stricter, if not using credentialless iframes):\n Cross-Origin-Embedder-Policy: require-corp\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 }'
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
- (0, import_react.useEffect)(() => {
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: () => sendCommand("try_now"),
160
- buyNow: () => sendCommand("buy_now"),
161
- canTryNow: Boolean(isReady && state?.tryNow?.available),
162
- canBuyNow: Boolean(isReady && state?.buyNow?.available)
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 ? state?.tryNow?.status ?? state?.buyNow?.status : void 0;
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 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,21 +30,54 @@ 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]);
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: state2 } = await navigator.permissions.query({
76
+ const { state } = await navigator.permissions.query({
41
77
  name: "keyboard-lock"
42
78
  });
43
- if (state2 === "granted" || state2 === "prompt") {
44
- console.log(`[PlayruoEmbed] Keyboard lock permission: ${state2}`);
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 sendCommand = (type) => {
87
- if (!iframeRef.current?.contentWindow) {
88
- console.warn(`PlayruoEmbed: Cannot send ${type} command, iframe not ready`);
122
+ const handleTryNow = useCallback(() => {
123
+ if (!isCompatible) {
124
+ console.warn("[PlayruoEmbed] Browser not compatible");
89
125
  return;
90
126
  }
91
- if (type === "try_now") {
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
- iframeRef.current.contentWindow.postMessage({ type }, "*");
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\nAlternative (stricter, if not using credentialless iframes):\n Cross-Origin-Embedder-Policy: require-corp\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 }'
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
- useEffect(() => {
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: () => sendCommand("try_now"),
131
- buyNow: () => sendCommand("buy_now"),
132
- canTryNow: Boolean(isReady && state?.tryNow?.available),
133
- canBuyNow: Boolean(isReady && state?.buyNow?.available)
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 ? state?.tryNow?.status ?? state?.buyNow?.status : void 0;
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
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playruo/embed",
3
- "version": "1.1.5",
3
+ "version": "1.2.1",
4
4
  "description": "React SDK to embed Playruo with postMessage API.",
5
5
  "license": "MIT",
6
6
  "type": "module",