@playruo/embed 1.2.2 → 1.2.4

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
@@ -33,8 +33,8 @@ __export(index_exports, {
33
33
  EmbedTester: () => EmbedTester,
34
34
  PlayruoEmbed: () => PlayruoEmbed,
35
35
  buildEmbedUrl: () => buildEmbedUrl,
36
- isPlayruoEmbedCommand: () => isPlayruoEmbedCommand,
37
- isPlayruoEmbedMessage: () => isPlayruoEmbedMessage
36
+ isPlayruoEmbedMessage: () => isPlayruoEmbedMessage,
37
+ isPlayruoInternalMessage: () => isPlayruoInternalMessage
38
38
  });
39
39
  module.exports = __toCommonJS(index_exports);
40
40
 
@@ -76,9 +76,7 @@ var PlayruoEmbed = ({
76
76
  const [isMobile, setIsMobile] = (0, import_react.useState)(false);
77
77
  const [isValidChromium, setIsValidChromium] = (0, import_react.useState)(true);
78
78
  const [errorMessage, setErrorMessage] = (0, import_react.useState)(null);
79
- const escapeKeyTimer = (0, import_react.useRef)(null);
80
- const escapeKeyPressed = (0, import_react.useRef)(false);
81
- const ESCAPE_HOLD_DURATION = 1500;
79
+ const [iframeState, setIframeState] = (0, import_react.useState)(null);
82
80
  const src = (0, import_react.useMemo)(() => buildEmbedUrl({ baseUrl, operation, storeUrl }), [baseUrl, operation, storeUrl]);
83
81
  (0, import_react.useEffect)(() => {
84
82
  if (typeof window === "undefined") return;
@@ -96,81 +94,22 @@ var PlayruoEmbed = ({
96
94
  }, []);
97
95
  const isCompatible = !isMobile && isValidChromium;
98
96
  const isReady = isCompatible ? isIframeLoaded : true;
99
- const currentState = (0, import_react.useMemo)(() => ({
100
- hasPlayedOnce,
101
- tryNow: {
102
- available: isCompatible,
103
- status: !isCompatible ? "Browser not compatible" : hasPlayedOnce ? "Click to replay" : "Ready"
104
- },
105
- buyNow: {
106
- available: true,
107
- status: "Ready"
108
- }
109
- }), [hasPlayedOnce, isCompatible]);
110
- const checkKeyboardLockPermission = (0, import_react.useCallback)(async () => {
111
- if (!navigator.permissions) {
112
- console.log("[PlayruoEmbed] Permissions API not supported");
113
- return false;
114
- }
115
- try {
116
- const { state } = await navigator.permissions.query({
117
- name: "keyboard-lock"
118
- });
119
- if (state === "granted" || state === "prompt") {
120
- console.log(`[PlayruoEmbed] Keyboard lock permission: ${state}`);
121
- return true;
122
- }
123
- console.log("[PlayruoEmbed] Keyboard lock permission denied");
124
- return false;
125
- } catch (error) {
126
- console.warn("[PlayruoEmbed] Error checking keyboard lock permission:", error);
127
- return false;
128
- }
129
- }, []);
130
- const requestFullscreenWithKeyboardLock = (0, import_react.useCallback)(async () => {
131
- const iframe = iframeRef.current;
132
- if (!iframe?.requestFullscreen) {
133
- console.log("[PlayruoEmbed] Fullscreen not supported on this browser");
134
- return false;
97
+ const currentState = (0, import_react.useMemo)(() => {
98
+ if (iframeState) {
99
+ return iframeState;
135
100
  }
136
- try {
137
- const fullscreenPromise = iframe.requestFullscreen();
138
- if (navigator.keyboard && window.self === window.top) {
139
- try {
140
- const permissionPromise = checkKeyboardLockPermission();
141
- const keyboardLockPromise = navigator.keyboard.lock(["Escape"]);
142
- void Promise.all([permissionPromise, keyboardLockPromise]).then(([hasPermission]) => {
143
- if (hasPermission) {
144
- console.log("[PlayruoEmbed] Keyboard locked successfully");
145
- }
146
- }).catch((err) => {
147
- const isTopLevelContextError = err?.name === "InvalidStateError" && err?.message?.includes("primary top-level browsing context");
148
- if (!isTopLevelContextError) {
149
- console.warn("[PlayruoEmbed] Keyboard lock failed:", err);
150
- }
151
- });
152
- } catch (err) {
153
- const isTopLevelContextError = err?.name === "InvalidStateError" && err?.message?.includes("primary top-level browsing context");
154
- if (!isTopLevelContextError) {
155
- console.warn("[PlayruoEmbed] Keyboard lock error:", err);
156
- }
157
- }
158
- } else if (!navigator.keyboard) {
159
- console.log("[PlayruoEmbed] Keyboard API not supported");
160
- } else {
161
- console.log("[PlayruoEmbed] Keyboard lock not available (not in top-level context)");
101
+ return {
102
+ hasPlayedOnce,
103
+ tryNow: {
104
+ available: isCompatible,
105
+ status: !isCompatible ? "Browser not compatible" : hasPlayedOnce ? "Click to resume" : "Ready"
106
+ },
107
+ buyNow: {
108
+ available: true,
109
+ status: "Ready"
162
110
  }
163
- void fullscreenPromise.then(() => {
164
- console.log("[PlayruoEmbed] Fullscreen activated successfully");
165
- }).catch((err) => {
166
- console.warn("[PlayruoEmbed] Fullscreen failed:", err);
167
- });
168
- return true;
169
- } catch (error) {
170
- console.error("[PlayruoEmbed] Error requesting fullscreen:", error);
171
- return false;
172
- }
173
- }, [iframeRef, checkKeyboardLockPermission]);
111
+ };
112
+ }, [hasPlayedOnce, isCompatible, iframeState]);
174
113
  const handleTryNow = (0, import_react.useCallback)(() => {
175
114
  if (!isCompatible) {
176
115
  console.warn("[PlayruoEmbed] Browser not compatible");
@@ -190,12 +129,10 @@ var PlayruoEmbed = ({
190
129
  } catch {
191
130
  }
192
131
  }
193
- requestFullscreenWithKeyboardLock();
194
132
  iframe.contentWindow?.postMessage({
195
133
  type: "playruo",
196
134
  action: "tryNowClick",
197
135
  fromEmbed: true,
198
- isFullscreen: true,
199
136
  isReplay: hasPlayedOnce
200
137
  }, "*");
201
138
  console.log("[PlayruoEmbed] Sent tryNowClick to game", hasPlayedOnce ? "(replay)" : "(first time)");
@@ -209,7 +146,7 @@ var PlayruoEmbed = ({
209
146
  setErrorMessage("Failed to start game");
210
147
  onError?.("Failed to start game");
211
148
  }
212
- }, [isCompatible, hasPlayedOnce, onError, iframeRef, requestFullscreenWithKeyboardLock]);
149
+ }, [isCompatible, hasPlayedOnce, onError, iframeRef]);
213
150
  const handleBuyNow = (0, import_react.useCallback)(() => {
214
151
  if (!storeUrl) {
215
152
  console.warn("[PlayruoEmbed] No store URL provided");
@@ -229,18 +166,112 @@ var PlayruoEmbed = ({
229
166
  }, [isReady, onReady]);
230
167
  (0, import_react.useEffect)(() => {
231
168
  const handleGameMessage = (event) => {
232
- const { type } = event.data || {};
169
+ const { type, data } = event.data || {};
170
+ const iframe = iframeRef.current;
171
+ if (!iframe || event.source !== iframe.contentWindow) {
172
+ return;
173
+ }
174
+ console.log("\u{1F537} [Embed] Message received from iframe:", { type, data });
175
+ if (type === "playruo_ready") {
176
+ console.log("\u{1F537} [Embed] playruo_ready received");
177
+ onReady?.();
178
+ } else if (type === "playruo_state_update" && data) {
179
+ console.log("\u{1F537} [Embed] playruo_state_update received:", data);
180
+ setIframeState(data);
181
+ if (data.hasPlayedOnce !== void 0) {
182
+ setHasPlayedOnce(data.hasPlayedOnce);
183
+ }
184
+ onStateChange?.(data);
185
+ } else if (type === "playruo_purchase_request" && data?.storeUrl) {
186
+ console.log("\u{1F537} [Embed] playruo_purchase_request received:", data);
187
+ onPurchaseRequest?.(data);
188
+ } else if (type === "playruo_demo_completed" && data) {
189
+ console.log("\u{1F537} [Embed] playruo_demo_completed received:", data);
190
+ setHasPlayedOnce(true);
191
+ onDemoCompleted?.(data);
192
+ } else if (type === "playruo_error") {
193
+ const errorMessage2 = typeof data === "string" ? data : data?.message || "Unknown error";
194
+ console.log("\u{1F537} [Embed] playruo_error received:", errorMessage2);
195
+ setErrorMessage(errorMessage2);
196
+ const errorState = {
197
+ hasPlayedOnce,
198
+ tryNow: {
199
+ available: false,
200
+ status: `Error: ${errorMessage2}`
201
+ },
202
+ buyNow: {
203
+ available: true,
204
+ status: "Ready"
205
+ }
206
+ };
207
+ setIframeState(errorState);
208
+ onStateChange?.(errorState);
209
+ onError?.(errorMessage2);
210
+ } else if (type === "playruo_session_over" && data) {
211
+ console.log("\u{1F537} [Embed] playruo_session_over received:", data);
212
+ setHasPlayedOnce(true);
213
+ setIframeState(data);
214
+ onStateChange?.(data);
215
+ } else if (type === "playruo_stream_start") {
216
+ console.log("\u{1F537} [Embed] playruo_stream_start received");
217
+ }
233
218
  if (type === "error" || type === "closeForBuy" || type === "goToStore") {
219
+ console.log("\u{1F537} [Embed] Legacy message received:", type);
234
220
  setHasPlayedOnce(true);
235
- onDemoCompleted?.({
221
+ const payload = {
236
222
  reason: "Demo session ended",
237
223
  storeUrl
238
- });
224
+ };
225
+ onDemoCompleted?.(payload);
239
226
  }
240
227
  };
241
228
  window.addEventListener("message", handleGameMessage);
242
229
  return () => window.removeEventListener("message", handleGameMessage);
243
- }, [storeUrl, onDemoCompleted]);
230
+ }, [storeUrl, onDemoCompleted, onReady, onStateChange, onPurchaseRequest, onError, iframeRef]);
231
+ const generateUUID = () => {
232
+ if (self.crypto.randomUUID) return self.crypto.randomUUID();
233
+ const bytes = new Uint8Array(16);
234
+ self.crypto.getRandomValues(bytes);
235
+ bytes[6] = bytes[6] & 15 | 64;
236
+ bytes[8] = bytes[8] & 63 | 128;
237
+ return [...bytes].map(
238
+ (b, i) => ([4, 6, 8, 10].includes(i) ? "-" : "") + b.toString(16).padStart(2, "0")
239
+ ).join("");
240
+ };
241
+ (0, import_react.useEffect)(() => {
242
+ const handleUUIDMessage = (event) => {
243
+ const iframe = iframeRef.current;
244
+ if (!iframe || event.source !== iframe.contentWindow) {
245
+ return;
246
+ }
247
+ const { type, requestId } = event.data || {};
248
+ if (type === "playruo_uuid_request") {
249
+ try {
250
+ let storedUUID = localStorage.getItem("playruo_uuid");
251
+ if (!storedUUID) {
252
+ storedUUID = generateUUID();
253
+ localStorage.setItem("playruo_uuid", storedUUID);
254
+ }
255
+ iframe.contentWindow?.postMessage(
256
+ {
257
+ type: "playruo_uuid_response",
258
+ requestId,
259
+ uuid: storedUUID
260
+ },
261
+ "*"
262
+ );
263
+ } catch (e) {
264
+ }
265
+ } else if (type === "playruo_uuid_remove") {
266
+ try {
267
+ localStorage.removeItem("playruo_uuid");
268
+ } catch (e) {
269
+ }
270
+ }
271
+ };
272
+ window.addEventListener("message", handleUUIDMessage);
273
+ return () => window.removeEventListener("message", handleUUIDMessage);
274
+ }, [iframeRef]);
244
275
  (0, import_react.useEffect)(() => {
245
276
  const handleFullscreenChange = () => {
246
277
  const doc = document;
@@ -254,10 +285,17 @@ var PlayruoEmbed = ({
254
285
  }, 200);
255
286
  }
256
287
  } else {
257
- escapeKeyPressed.current = false;
258
- if (escapeKeyTimer.current) {
259
- clearTimeout(escapeKeyTimer.current);
260
- escapeKeyTimer.current = null;
288
+ const iframe = iframeRef.current;
289
+ if (iframe) {
290
+ try {
291
+ iframe.contentWindow?.postMessage({
292
+ type: "playruo",
293
+ action: "exitFullscreen",
294
+ fromEmbed: true
295
+ }, "*");
296
+ } catch {
297
+ console.error("[PlayruoEmbed] Failed to exit fullscreen");
298
+ }
261
299
  }
262
300
  setShowGameIframe(false);
263
301
  setHasPlayedOnce(true);
@@ -278,56 +316,6 @@ var PlayruoEmbed = ({
278
316
  document.removeEventListener("MSFullscreenChange", handleFullscreenChange);
279
317
  };
280
318
  }, [storeUrl, onDemoCompleted, iframeRef]);
281
- (0, import_react.useEffect)(() => {
282
- const handleKeyDown = (event) => {
283
- if (event.key === "Escape" && !escapeKeyPressed.current) {
284
- escapeKeyPressed.current = true;
285
- event.preventDefault();
286
- escapeKeyTimer.current = setTimeout(() => {
287
- if (document.fullscreenElement) {
288
- document.exitFullscreen();
289
- }
290
- }, ESCAPE_HOLD_DURATION);
291
- }
292
- if (event.ctrlKey && event.shiftKey && event.key === "R") {
293
- event.preventDefault();
294
- if (document.fullscreenElement) {
295
- document.exitFullscreen().catch(console.warn);
296
- }
297
- setTimeout(() => {
298
- window.location.reload();
299
- }, 500);
300
- }
301
- };
302
- const handleKeyUp = (event) => {
303
- if (event.key === "Escape" && escapeKeyPressed.current) {
304
- escapeKeyPressed.current = false;
305
- if (escapeKeyTimer.current) {
306
- clearTimeout(escapeKeyTimer.current);
307
- escapeKeyTimer.current = null;
308
- }
309
- }
310
- };
311
- document.addEventListener("keydown", handleKeyDown);
312
- document.addEventListener("keyup", handleKeyUp);
313
- return () => {
314
- document.removeEventListener("keydown", handleKeyDown);
315
- document.removeEventListener("keyup", handleKeyUp);
316
- };
317
- }, []);
318
- (0, import_react.useEffect)(() => {
319
- const isVisible = showGameIframe || showIframe;
320
- const iframe = iframeRef.current;
321
- if (!iframe || !isIframeLoaded) return;
322
- try {
323
- iframe.contentWindow?.postMessage({
324
- type: "playruo",
325
- action: isVisible ? "unmute" : "mute",
326
- fromEmbed: true
327
- }, "*");
328
- } catch {
329
- }
330
- }, [showGameIframe, showIframe, isIframeLoaded, iframeRef]);
331
319
  (0, import_react.useEffect)(() => {
332
320
  const logCrossOriginStatus = () => {
333
321
  const isIsolated = self.crossOriginIsolated;
@@ -436,7 +424,6 @@ var PlayruoEmbed = ({
436
424
  ref: iframeRef,
437
425
  src,
438
426
  allow: "cross-origin-isolated; fullscreen *; autoplay *; gamepad *; clipboard-write *; web-share *; encrypted-media *; accelerometer *; gyroscope *",
439
- allowFullScreen: true,
440
427
  className: "w-full h-[70vh] rounded-lg border",
441
428
  ...{ credentialless: "true" },
442
429
  sandbox: "allow-same-origin allow-scripts allow-popups allow-forms allow-presentation allow-top-navigation allow-modals allow-pointer-lock",
@@ -829,19 +816,18 @@ var isPlayruoEmbedMessage = (data) => {
829
816
  if (!data || typeof data !== "object") return false;
830
817
  const msg = data;
831
818
  if (typeof msg.type !== "string") return false;
832
- return msg.type === "playruo_ready" || msg.type === "playruo_state_update" || msg.type === "playruo_purchase_request" || msg.type === "playruo_demo_completed" || msg.type === "playruo_error";
819
+ return msg.type === "playruo_ready" || msg.type === "playruo_state_update" || msg.type === "playruo_purchase_request" || msg.type === "playruo_demo_completed" || msg.type === "playruo_error" || msg.type === "playruo_session_over" || msg.type === "playruo_stream_start" || msg.type === "playruo_stream_stop";
833
820
  };
834
- var isPlayruoEmbedCommand = (data) => {
821
+ var isPlayruoInternalMessage = (data) => {
835
822
  if (!data || typeof data !== "object") return false;
836
- const cmd = data;
837
- if (typeof cmd.type !== "string") return false;
838
- return cmd.type === "try_now" || cmd.type === "buy_now" || cmd.type === "get_state";
823
+ const msg = data;
824
+ return msg.type === "playruo" && typeof msg.action === "string" && (msg.action === "tryNowClick" || msg.action === "exitFullscreen" || msg.action === "mute" || msg.action === "unmute") && msg.fromEmbed === true;
839
825
  };
840
826
  // Annotate the CommonJS export names for ESM import in node:
841
827
  0 && (module.exports = {
842
828
  EmbedTester,
843
829
  PlayruoEmbed,
844
830
  buildEmbedUrl,
845
- isPlayruoEmbedCommand,
846
- isPlayruoEmbedMessage
831
+ isPlayruoEmbedMessage,
832
+ isPlayruoInternalMessage
847
833
  });
package/dist/index.d.cts CHANGED
@@ -10,6 +10,7 @@ interface PlayruoEmbedState {
10
10
  buyNow: {
11
11
  available: boolean;
12
12
  status?: string;
13
+ storeUrl?: string;
13
14
  };
14
15
  }
15
16
  interface DemoCompletedPayload {
@@ -17,15 +18,6 @@ interface DemoCompletedPayload {
17
18
  storeUrl?: string;
18
19
  }
19
20
 
20
- interface Keyboard {
21
- lock: (keys: string[]) => Promise<void>;
22
- unlock: () => void;
23
- }
24
- declare global {
25
- interface Navigator {
26
- keyboard?: Keyboard;
27
- }
28
- }
29
21
  interface PlayruoEmbedActions {
30
22
  /** Start the demo session */
31
23
  tryNow: () => void;
@@ -98,25 +90,35 @@ interface PlayruoErrorMessage {
98
90
  message: string;
99
91
  } | string;
100
92
  }
101
- type PlayruoEmbedMessage = PlayruoReadyMessage | PlayruoStateUpdateMessage | PlayruoPurchaseRequestMessage | PlayruoDemoCompletedMessage | PlayruoErrorMessage;
102
- interface TryNowCommand {
103
- type: "try_now";
93
+ interface PlayruoSessionOverMessage {
94
+ type: "playruo_session_over";
95
+ data: PlayruoEmbedState;
96
+ }
97
+ interface PlayruoStreamStartMessage {
98
+ type: "playruo_stream_start";
104
99
  }
105
- interface BuyNowCommand {
106
- type: "buy_now";
100
+ interface PlayruoStreamStopMessage {
101
+ type: "playruo_stream_stop";
107
102
  }
108
- interface GetStateCommand {
109
- type: "get_state";
103
+ type PlayruoEmbedMessage = PlayruoReadyMessage | PlayruoStateUpdateMessage | PlayruoPurchaseRequestMessage | PlayruoDemoCompletedMessage | PlayruoErrorMessage | PlayruoSessionOverMessage | PlayruoStreamStartMessage | PlayruoStreamStopMessage;
104
+ interface PlayruoInternalMessage {
105
+ type: "playruo";
106
+ action: "tryNowClick" | "exitFullscreen" | "mute" | "unmute";
107
+ fromEmbed: true;
108
+ isReplay?: boolean;
109
+ parentWillHandleFullscreen?: boolean;
110
110
  }
111
- type PlayruoEmbedCommand = TryNowCommand | BuyNowCommand | GetStateCommand;
112
111
  /**
113
112
  * Type guard to check if a message event is a Playruo embed message
113
+ * Messages FROM Embed → Host via postMessage
114
114
  */
115
115
  declare const isPlayruoEmbedMessage: (data: unknown) => data is PlayruoEmbedMessage;
116
116
  /**
117
- * Type guard to check if a command is a valid Playruo embed command
117
+ * Type guard to check if a message is a Playruo internal message
118
+ * Format: { type: 'playruo', action: '...', fromEmbed: true }
119
+ * These are messages sent TO the iframe (game), not to the parent
118
120
  */
119
- declare const isPlayruoEmbedCommand: (data: unknown) => data is PlayruoEmbedCommand;
121
+ declare const isPlayruoInternalMessage: (data: unknown) => data is PlayruoInternalMessage;
120
122
 
121
123
  interface BuildUrlParams {
122
124
  baseUrl: string;
@@ -131,4 +133,4 @@ interface BuildUrlParams {
131
133
  */
132
134
  declare const buildEmbedUrl: ({ baseUrl, operation, storeUrl }: BuildUrlParams) => string;
133
135
 
134
- export { type BuyNowCommand, type DemoCompletedPayload, EmbedTester, type EmbedTesterProps, 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 };
136
+ export { type DemoCompletedPayload, EmbedTester, type EmbedTesterProps, type PlayruoDemoCompletedMessage, PlayruoEmbed, type PlayruoEmbedActions, type PlayruoEmbedMessage, type PlayruoEmbedProps, type PlayruoEmbedRenderState, type PlayruoEmbedState, type PlayruoErrorMessage, type PlayruoInternalMessage, type PlayruoPurchaseRequestMessage, type PlayruoReadyMessage, type PlayruoSessionOverMessage, type PlayruoStateUpdateMessage, type PlayruoStreamStartMessage, buildEmbedUrl, isPlayruoEmbedMessage, isPlayruoInternalMessage };
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ interface PlayruoEmbedState {
10
10
  buyNow: {
11
11
  available: boolean;
12
12
  status?: string;
13
+ storeUrl?: string;
13
14
  };
14
15
  }
15
16
  interface DemoCompletedPayload {
@@ -17,15 +18,6 @@ interface DemoCompletedPayload {
17
18
  storeUrl?: string;
18
19
  }
19
20
 
20
- interface Keyboard {
21
- lock: (keys: string[]) => Promise<void>;
22
- unlock: () => void;
23
- }
24
- declare global {
25
- interface Navigator {
26
- keyboard?: Keyboard;
27
- }
28
- }
29
21
  interface PlayruoEmbedActions {
30
22
  /** Start the demo session */
31
23
  tryNow: () => void;
@@ -98,25 +90,35 @@ interface PlayruoErrorMessage {
98
90
  message: string;
99
91
  } | string;
100
92
  }
101
- type PlayruoEmbedMessage = PlayruoReadyMessage | PlayruoStateUpdateMessage | PlayruoPurchaseRequestMessage | PlayruoDemoCompletedMessage | PlayruoErrorMessage;
102
- interface TryNowCommand {
103
- type: "try_now";
93
+ interface PlayruoSessionOverMessage {
94
+ type: "playruo_session_over";
95
+ data: PlayruoEmbedState;
96
+ }
97
+ interface PlayruoStreamStartMessage {
98
+ type: "playruo_stream_start";
104
99
  }
105
- interface BuyNowCommand {
106
- type: "buy_now";
100
+ interface PlayruoStreamStopMessage {
101
+ type: "playruo_stream_stop";
107
102
  }
108
- interface GetStateCommand {
109
- type: "get_state";
103
+ type PlayruoEmbedMessage = PlayruoReadyMessage | PlayruoStateUpdateMessage | PlayruoPurchaseRequestMessage | PlayruoDemoCompletedMessage | PlayruoErrorMessage | PlayruoSessionOverMessage | PlayruoStreamStartMessage | PlayruoStreamStopMessage;
104
+ interface PlayruoInternalMessage {
105
+ type: "playruo";
106
+ action: "tryNowClick" | "exitFullscreen" | "mute" | "unmute";
107
+ fromEmbed: true;
108
+ isReplay?: boolean;
109
+ parentWillHandleFullscreen?: boolean;
110
110
  }
111
- type PlayruoEmbedCommand = TryNowCommand | BuyNowCommand | GetStateCommand;
112
111
  /**
113
112
  * Type guard to check if a message event is a Playruo embed message
113
+ * Messages FROM Embed → Host via postMessage
114
114
  */
115
115
  declare const isPlayruoEmbedMessage: (data: unknown) => data is PlayruoEmbedMessage;
116
116
  /**
117
- * Type guard to check if a command is a valid Playruo embed command
117
+ * Type guard to check if a message is a Playruo internal message
118
+ * Format: { type: 'playruo', action: '...', fromEmbed: true }
119
+ * These are messages sent TO the iframe (game), not to the parent
118
120
  */
119
- declare const isPlayruoEmbedCommand: (data: unknown) => data is PlayruoEmbedCommand;
121
+ declare const isPlayruoInternalMessage: (data: unknown) => data is PlayruoInternalMessage;
120
122
 
121
123
  interface BuildUrlParams {
122
124
  baseUrl: string;
@@ -131,4 +133,4 @@ interface BuildUrlParams {
131
133
  */
132
134
  declare const buildEmbedUrl: ({ baseUrl, operation, storeUrl }: BuildUrlParams) => string;
133
135
 
134
- export { type BuyNowCommand, type DemoCompletedPayload, EmbedTester, type EmbedTesterProps, 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 };
136
+ export { type DemoCompletedPayload, EmbedTester, type EmbedTesterProps, type PlayruoDemoCompletedMessage, PlayruoEmbed, type PlayruoEmbedActions, type PlayruoEmbedMessage, type PlayruoEmbedProps, type PlayruoEmbedRenderState, type PlayruoEmbedState, type PlayruoErrorMessage, type PlayruoInternalMessage, type PlayruoPurchaseRequestMessage, type PlayruoReadyMessage, type PlayruoSessionOverMessage, type PlayruoStateUpdateMessage, type PlayruoStreamStartMessage, buildEmbedUrl, isPlayruoEmbedMessage, isPlayruoInternalMessage };
package/dist/index.js CHANGED
@@ -36,9 +36,7 @@ var PlayruoEmbed = ({
36
36
  const [isMobile, setIsMobile] = useState(false);
37
37
  const [isValidChromium, setIsValidChromium] = useState(true);
38
38
  const [errorMessage, setErrorMessage] = useState(null);
39
- const escapeKeyTimer = useRef(null);
40
- const escapeKeyPressed = useRef(false);
41
- const ESCAPE_HOLD_DURATION = 1500;
39
+ const [iframeState, setIframeState] = useState(null);
42
40
  const src = useMemo(() => buildEmbedUrl({ baseUrl, operation, storeUrl }), [baseUrl, operation, storeUrl]);
43
41
  useEffect(() => {
44
42
  if (typeof window === "undefined") return;
@@ -56,81 +54,22 @@ var PlayruoEmbed = ({
56
54
  }, []);
57
55
  const isCompatible = !isMobile && isValidChromium;
58
56
  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 = useCallback(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
- }, []);
90
- const requestFullscreenWithKeyboardLock = useCallback(async () => {
91
- const iframe = iframeRef.current;
92
- if (!iframe?.requestFullscreen) {
93
- console.log("[PlayruoEmbed] Fullscreen not supported on this browser");
94
- return false;
57
+ const currentState = useMemo(() => {
58
+ if (iframeState) {
59
+ return iframeState;
95
60
  }
96
- try {
97
- const fullscreenPromise = iframe.requestFullscreen();
98
- if (navigator.keyboard && window.self === window.top) {
99
- try {
100
- const permissionPromise = checkKeyboardLockPermission();
101
- const keyboardLockPromise = navigator.keyboard.lock(["Escape"]);
102
- void Promise.all([permissionPromise, keyboardLockPromise]).then(([hasPermission]) => {
103
- if (hasPermission) {
104
- console.log("[PlayruoEmbed] Keyboard locked successfully");
105
- }
106
- }).catch((err) => {
107
- const isTopLevelContextError = err?.name === "InvalidStateError" && err?.message?.includes("primary top-level browsing context");
108
- if (!isTopLevelContextError) {
109
- console.warn("[PlayruoEmbed] Keyboard lock failed:", err);
110
- }
111
- });
112
- } catch (err) {
113
- const isTopLevelContextError = err?.name === "InvalidStateError" && err?.message?.includes("primary top-level browsing context");
114
- if (!isTopLevelContextError) {
115
- console.warn("[PlayruoEmbed] Keyboard lock error:", err);
116
- }
117
- }
118
- } else if (!navigator.keyboard) {
119
- console.log("[PlayruoEmbed] Keyboard API not supported");
120
- } else {
121
- console.log("[PlayruoEmbed] Keyboard lock not available (not in top-level context)");
61
+ return {
62
+ hasPlayedOnce,
63
+ tryNow: {
64
+ available: isCompatible,
65
+ status: !isCompatible ? "Browser not compatible" : hasPlayedOnce ? "Click to resume" : "Ready"
66
+ },
67
+ buyNow: {
68
+ available: true,
69
+ status: "Ready"
122
70
  }
123
- void fullscreenPromise.then(() => {
124
- console.log("[PlayruoEmbed] Fullscreen activated successfully");
125
- }).catch((err) => {
126
- console.warn("[PlayruoEmbed] Fullscreen failed:", err);
127
- });
128
- return true;
129
- } catch (error) {
130
- console.error("[PlayruoEmbed] Error requesting fullscreen:", error);
131
- return false;
132
- }
133
- }, [iframeRef, checkKeyboardLockPermission]);
71
+ };
72
+ }, [hasPlayedOnce, isCompatible, iframeState]);
134
73
  const handleTryNow = useCallback(() => {
135
74
  if (!isCompatible) {
136
75
  console.warn("[PlayruoEmbed] Browser not compatible");
@@ -150,12 +89,10 @@ var PlayruoEmbed = ({
150
89
  } catch {
151
90
  }
152
91
  }
153
- requestFullscreenWithKeyboardLock();
154
92
  iframe.contentWindow?.postMessage({
155
93
  type: "playruo",
156
94
  action: "tryNowClick",
157
95
  fromEmbed: true,
158
- isFullscreen: true,
159
96
  isReplay: hasPlayedOnce
160
97
  }, "*");
161
98
  console.log("[PlayruoEmbed] Sent tryNowClick to game", hasPlayedOnce ? "(replay)" : "(first time)");
@@ -169,7 +106,7 @@ var PlayruoEmbed = ({
169
106
  setErrorMessage("Failed to start game");
170
107
  onError?.("Failed to start game");
171
108
  }
172
- }, [isCompatible, hasPlayedOnce, onError, iframeRef, requestFullscreenWithKeyboardLock]);
109
+ }, [isCompatible, hasPlayedOnce, onError, iframeRef]);
173
110
  const handleBuyNow = useCallback(() => {
174
111
  if (!storeUrl) {
175
112
  console.warn("[PlayruoEmbed] No store URL provided");
@@ -189,18 +126,112 @@ var PlayruoEmbed = ({
189
126
  }, [isReady, onReady]);
190
127
  useEffect(() => {
191
128
  const handleGameMessage = (event) => {
192
- const { type } = event.data || {};
129
+ const { type, data } = event.data || {};
130
+ const iframe = iframeRef.current;
131
+ if (!iframe || event.source !== iframe.contentWindow) {
132
+ return;
133
+ }
134
+ console.log("\u{1F537} [Embed] Message received from iframe:", { type, data });
135
+ if (type === "playruo_ready") {
136
+ console.log("\u{1F537} [Embed] playruo_ready received");
137
+ onReady?.();
138
+ } else if (type === "playruo_state_update" && data) {
139
+ console.log("\u{1F537} [Embed] playruo_state_update received:", data);
140
+ setIframeState(data);
141
+ if (data.hasPlayedOnce !== void 0) {
142
+ setHasPlayedOnce(data.hasPlayedOnce);
143
+ }
144
+ onStateChange?.(data);
145
+ } else if (type === "playruo_purchase_request" && data?.storeUrl) {
146
+ console.log("\u{1F537} [Embed] playruo_purchase_request received:", data);
147
+ onPurchaseRequest?.(data);
148
+ } else if (type === "playruo_demo_completed" && data) {
149
+ console.log("\u{1F537} [Embed] playruo_demo_completed received:", data);
150
+ setHasPlayedOnce(true);
151
+ onDemoCompleted?.(data);
152
+ } else if (type === "playruo_error") {
153
+ const errorMessage2 = typeof data === "string" ? data : data?.message || "Unknown error";
154
+ console.log("\u{1F537} [Embed] playruo_error received:", errorMessage2);
155
+ setErrorMessage(errorMessage2);
156
+ const errorState = {
157
+ hasPlayedOnce,
158
+ tryNow: {
159
+ available: false,
160
+ status: `Error: ${errorMessage2}`
161
+ },
162
+ buyNow: {
163
+ available: true,
164
+ status: "Ready"
165
+ }
166
+ };
167
+ setIframeState(errorState);
168
+ onStateChange?.(errorState);
169
+ onError?.(errorMessage2);
170
+ } else if (type === "playruo_session_over" && data) {
171
+ console.log("\u{1F537} [Embed] playruo_session_over received:", data);
172
+ setHasPlayedOnce(true);
173
+ setIframeState(data);
174
+ onStateChange?.(data);
175
+ } else if (type === "playruo_stream_start") {
176
+ console.log("\u{1F537} [Embed] playruo_stream_start received");
177
+ }
193
178
  if (type === "error" || type === "closeForBuy" || type === "goToStore") {
179
+ console.log("\u{1F537} [Embed] Legacy message received:", type);
194
180
  setHasPlayedOnce(true);
195
- onDemoCompleted?.({
181
+ const payload = {
196
182
  reason: "Demo session ended",
197
183
  storeUrl
198
- });
184
+ };
185
+ onDemoCompleted?.(payload);
199
186
  }
200
187
  };
201
188
  window.addEventListener("message", handleGameMessage);
202
189
  return () => window.removeEventListener("message", handleGameMessage);
203
- }, [storeUrl, onDemoCompleted]);
190
+ }, [storeUrl, onDemoCompleted, onReady, onStateChange, onPurchaseRequest, onError, iframeRef]);
191
+ const generateUUID = () => {
192
+ if (self.crypto.randomUUID) return self.crypto.randomUUID();
193
+ const bytes = new Uint8Array(16);
194
+ self.crypto.getRandomValues(bytes);
195
+ bytes[6] = bytes[6] & 15 | 64;
196
+ bytes[8] = bytes[8] & 63 | 128;
197
+ return [...bytes].map(
198
+ (b, i) => ([4, 6, 8, 10].includes(i) ? "-" : "") + b.toString(16).padStart(2, "0")
199
+ ).join("");
200
+ };
201
+ useEffect(() => {
202
+ const handleUUIDMessage = (event) => {
203
+ const iframe = iframeRef.current;
204
+ if (!iframe || event.source !== iframe.contentWindow) {
205
+ return;
206
+ }
207
+ const { type, requestId } = event.data || {};
208
+ if (type === "playruo_uuid_request") {
209
+ try {
210
+ let storedUUID = localStorage.getItem("playruo_uuid");
211
+ if (!storedUUID) {
212
+ storedUUID = generateUUID();
213
+ localStorage.setItem("playruo_uuid", storedUUID);
214
+ }
215
+ iframe.contentWindow?.postMessage(
216
+ {
217
+ type: "playruo_uuid_response",
218
+ requestId,
219
+ uuid: storedUUID
220
+ },
221
+ "*"
222
+ );
223
+ } catch (e) {
224
+ }
225
+ } else if (type === "playruo_uuid_remove") {
226
+ try {
227
+ localStorage.removeItem("playruo_uuid");
228
+ } catch (e) {
229
+ }
230
+ }
231
+ };
232
+ window.addEventListener("message", handleUUIDMessage);
233
+ return () => window.removeEventListener("message", handleUUIDMessage);
234
+ }, [iframeRef]);
204
235
  useEffect(() => {
205
236
  const handleFullscreenChange = () => {
206
237
  const doc = document;
@@ -214,10 +245,17 @@ var PlayruoEmbed = ({
214
245
  }, 200);
215
246
  }
216
247
  } else {
217
- escapeKeyPressed.current = false;
218
- if (escapeKeyTimer.current) {
219
- clearTimeout(escapeKeyTimer.current);
220
- escapeKeyTimer.current = null;
248
+ const iframe = iframeRef.current;
249
+ if (iframe) {
250
+ try {
251
+ iframe.contentWindow?.postMessage({
252
+ type: "playruo",
253
+ action: "exitFullscreen",
254
+ fromEmbed: true
255
+ }, "*");
256
+ } catch {
257
+ console.error("[PlayruoEmbed] Failed to exit fullscreen");
258
+ }
221
259
  }
222
260
  setShowGameIframe(false);
223
261
  setHasPlayedOnce(true);
@@ -238,56 +276,6 @@ var PlayruoEmbed = ({
238
276
  document.removeEventListener("MSFullscreenChange", handleFullscreenChange);
239
277
  };
240
278
  }, [storeUrl, onDemoCompleted, iframeRef]);
241
- useEffect(() => {
242
- const handleKeyDown = (event) => {
243
- if (event.key === "Escape" && !escapeKeyPressed.current) {
244
- escapeKeyPressed.current = true;
245
- event.preventDefault();
246
- escapeKeyTimer.current = setTimeout(() => {
247
- if (document.fullscreenElement) {
248
- document.exitFullscreen();
249
- }
250
- }, ESCAPE_HOLD_DURATION);
251
- }
252
- if (event.ctrlKey && event.shiftKey && event.key === "R") {
253
- event.preventDefault();
254
- if (document.fullscreenElement) {
255
- document.exitFullscreen().catch(console.warn);
256
- }
257
- setTimeout(() => {
258
- window.location.reload();
259
- }, 500);
260
- }
261
- };
262
- const handleKeyUp = (event) => {
263
- if (event.key === "Escape" && escapeKeyPressed.current) {
264
- escapeKeyPressed.current = false;
265
- if (escapeKeyTimer.current) {
266
- clearTimeout(escapeKeyTimer.current);
267
- escapeKeyTimer.current = null;
268
- }
269
- }
270
- };
271
- document.addEventListener("keydown", handleKeyDown);
272
- document.addEventListener("keyup", handleKeyUp);
273
- return () => {
274
- document.removeEventListener("keydown", handleKeyDown);
275
- document.removeEventListener("keyup", handleKeyUp);
276
- };
277
- }, []);
278
- useEffect(() => {
279
- const isVisible = showGameIframe || showIframe;
280
- const iframe = iframeRef.current;
281
- if (!iframe || !isIframeLoaded) return;
282
- try {
283
- iframe.contentWindow?.postMessage({
284
- type: "playruo",
285
- action: isVisible ? "unmute" : "mute",
286
- fromEmbed: true
287
- }, "*");
288
- } catch {
289
- }
290
- }, [showGameIframe, showIframe, isIframeLoaded, iframeRef]);
291
279
  useEffect(() => {
292
280
  const logCrossOriginStatus = () => {
293
281
  const isIsolated = self.crossOriginIsolated;
@@ -396,7 +384,6 @@ var PlayruoEmbed = ({
396
384
  ref: iframeRef,
397
385
  src,
398
386
  allow: "cross-origin-isolated; fullscreen *; autoplay *; gamepad *; clipboard-write *; web-share *; encrypted-media *; accelerometer *; gyroscope *",
399
- allowFullScreen: true,
400
387
  className: "w-full h-[70vh] rounded-lg border",
401
388
  ...{ credentialless: "true" },
402
389
  sandbox: "allow-same-origin allow-scripts allow-popups allow-forms allow-presentation allow-top-navigation allow-modals allow-pointer-lock",
@@ -789,18 +776,17 @@ var isPlayruoEmbedMessage = (data) => {
789
776
  if (!data || typeof data !== "object") return false;
790
777
  const msg = data;
791
778
  if (typeof msg.type !== "string") return false;
792
- return msg.type === "playruo_ready" || msg.type === "playruo_state_update" || msg.type === "playruo_purchase_request" || msg.type === "playruo_demo_completed" || msg.type === "playruo_error";
779
+ return msg.type === "playruo_ready" || msg.type === "playruo_state_update" || msg.type === "playruo_purchase_request" || msg.type === "playruo_demo_completed" || msg.type === "playruo_error" || msg.type === "playruo_session_over" || msg.type === "playruo_stream_start" || msg.type === "playruo_stream_stop";
793
780
  };
794
- var isPlayruoEmbedCommand = (data) => {
781
+ var isPlayruoInternalMessage = (data) => {
795
782
  if (!data || typeof data !== "object") return false;
796
- const cmd = data;
797
- if (typeof cmd.type !== "string") return false;
798
- return cmd.type === "try_now" || cmd.type === "buy_now" || cmd.type === "get_state";
783
+ const msg = data;
784
+ return msg.type === "playruo" && typeof msg.action === "string" && (msg.action === "tryNowClick" || msg.action === "exitFullscreen" || msg.action === "mute" || msg.action === "unmute") && msg.fromEmbed === true;
799
785
  };
800
786
  export {
801
787
  EmbedTester,
802
788
  PlayruoEmbed,
803
789
  buildEmbedUrl,
804
- isPlayruoEmbedCommand,
805
- isPlayruoEmbedMessage
790
+ isPlayruoEmbedMessage,
791
+ isPlayruoInternalMessage
806
792
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playruo/embed",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "React SDK to embed Playruo with postMessage API.",
5
5
  "license": "MIT",
6
6
  "type": "module",