@playruo/embed 1.3.1 → 1.3.3

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/README.md CHANGED
@@ -1,3 +1,27 @@
1
1
  # @playruo/embed
2
2
 
3
3
  React SDK to embed Playruo via iframe + postMessage.
4
+
5
+ ## Windowed launch
6
+
7
+ `PlayruoEmbed` requests browser fullscreen on the iframe when the user starts a
8
+ demo. This remains the default behavior.
9
+
10
+ Set `enableWindowedLaunch` to `true` to skip the parent fullscreen request and
11
+ start the experience inside the current iframe bounds:
12
+
13
+ ```tsx
14
+ <PlayruoEmbed
15
+ operation="iframe"
16
+ enableWindowedLaunch
17
+ />
18
+ ```
19
+
20
+ ## Event forwarding
21
+
22
+ When the embedded Playruo iframe sends `{ type: "playruo_event", data }`, the
23
+ React package forwards the payload through `onEvent(data)`.
24
+
25
+ If the package host is itself inside another iframe, the same message is also
26
+ forwarded one level up with `window.parent.postMessage({ type:
27
+ "playruo_event", data }, "*")`.
package/dist/index.cjs CHANGED
@@ -74,14 +74,19 @@ var PlayruoEmbed = ({
74
74
  onReady,
75
75
  onStateChange,
76
76
  onDemoCompleted,
77
+ onEvent,
77
78
  onPurchaseRequest,
78
79
  onError,
80
+ enableWindowedLaunch = false,
81
+ forwardEventsToParent = true,
79
82
  renderState,
80
83
  iframeRef: externalIframeRef
81
84
  }) => {
82
85
  const internalIframeRef = (0, import_react.useRef)(null);
83
86
  const iframeRef = externalIframeRef || internalIframeRef;
84
87
  const wasEmbedFullscreenRef = (0, import_react.useRef)(false);
88
+ const hasNotifiedCompletionRef = (0, import_react.useRef)(false);
89
+ const suppressNextFullscreenExitCleanupRef = (0, import_react.useRef)(false);
85
90
  const [isIframeLoaded, setIsIframeLoaded] = (0, import_react.useState)(false);
86
91
  const [hasPlayedOnce, setHasPlayedOnce] = (0, import_react.useState)(false);
87
92
  const [showGameIframe, setShowGameIframe] = (0, import_react.useState)(false);
@@ -151,6 +156,149 @@ var PlayruoEmbed = ({
151
156
  console.warn("[PlayruoEmbed] Failed to release Escape key lock:", error);
152
157
  }
153
158
  }, [getKeyboardApi]);
159
+ const getFullscreenElement = (0, import_react.useCallback)(() => {
160
+ const doc = document;
161
+ return doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement || null;
162
+ }, []);
163
+ const notifyDemoCompletedOnce = (0, import_react.useCallback)(
164
+ (payload) => {
165
+ if (hasNotifiedCompletionRef.current) {
166
+ return;
167
+ }
168
+ hasNotifiedCompletionRef.current = true;
169
+ onDemoCompleted?.(
170
+ payload ?? {
171
+ reason: "Demo session ended",
172
+ storeUrl
173
+ }
174
+ );
175
+ },
176
+ [onDemoCompleted, storeUrl]
177
+ );
178
+ const postExitFullscreenToIframe = (0, import_react.useCallback)(() => {
179
+ const iframe = iframeRef.current;
180
+ if (!iframe) {
181
+ return;
182
+ }
183
+ try {
184
+ iframe.contentWindow?.postMessage({
185
+ type: "playruo",
186
+ action: "exitFullscreen",
187
+ fromEmbed: true,
188
+ isFullscreen: false,
189
+ parentWillHandleFullscreen: true
190
+ }, "*");
191
+ } catch {
192
+ console.error("[PlayruoEmbed] Failed to exit fullscreen");
193
+ }
194
+ }, [iframeRef]);
195
+ const postExitFullscreenAckToIframe = (0, import_react.useCallback)(() => {
196
+ const iframe = iframeRef.current;
197
+ if (!iframe) {
198
+ return;
199
+ }
200
+ try {
201
+ iframe.contentWindow?.postMessage({ type: "ACK_EXIT_FULLSCREEN" }, "*");
202
+ } catch {
203
+ console.error("[PlayruoEmbed] Failed to acknowledge fullscreen exit");
204
+ }
205
+ }, [iframeRef]);
206
+ const exitEmbedFullscreen = (0, import_react.useCallback)(
207
+ async ({
208
+ reason = "cleanup",
209
+ suppressFullscreenExitCleanup = true
210
+ } = {}) => {
211
+ const fullscreenElement = getFullscreenElement();
212
+ const doc = document;
213
+ const exitFullscreen = doc.exitFullscreen?.bind(doc) ?? doc.webkitExitFullscreen?.bind(doc) ?? doc.mozCancelFullScreen?.bind(doc) ?? doc.msExitFullscreen?.bind(doc);
214
+ const shouldAttemptExit = Boolean(fullscreenElement || wasEmbedFullscreenRef.current);
215
+ if (!shouldAttemptExit || !exitFullscreen) {
216
+ console.log("[PlayruoEmbed] Parent fullscreen exit skipped", {
217
+ hasFullscreenElement: Boolean(fullscreenElement),
218
+ hasExitFullscreen: Boolean(exitFullscreen),
219
+ reason,
220
+ wasEmbedFullscreen: wasEmbedFullscreenRef.current
221
+ });
222
+ return false;
223
+ }
224
+ console.log("[PlayruoEmbed] Parent fullscreen exit requested", {
225
+ fullscreenElement: fullscreenElement?.tagName,
226
+ reason,
227
+ wasEmbedFullscreen: wasEmbedFullscreenRef.current
228
+ });
229
+ const shouldSuppressFullscreenExitCleanup = Boolean(
230
+ suppressFullscreenExitCleanup && fullscreenElement
231
+ );
232
+ if (shouldSuppressFullscreenExitCleanup) {
233
+ suppressNextFullscreenExitCleanupRef.current = true;
234
+ wasEmbedFullscreenRef.current = false;
235
+ }
236
+ try {
237
+ await exitFullscreen();
238
+ if (shouldSuppressFullscreenExitCleanup) {
239
+ window.setTimeout(() => {
240
+ suppressNextFullscreenExitCleanupRef.current = false;
241
+ }, 1e3);
242
+ }
243
+ return true;
244
+ } catch (error) {
245
+ if (shouldSuppressFullscreenExitCleanup) {
246
+ suppressNextFullscreenExitCleanupRef.current = false;
247
+ wasEmbedFullscreenRef.current = Boolean(getFullscreenElement());
248
+ }
249
+ console.warn("[PlayruoEmbed] Failed to leave fullscreen:", error);
250
+ return false;
251
+ }
252
+ },
253
+ [getFullscreenElement]
254
+ );
255
+ const cleanupEmbedSession = (0, import_react.useCallback)(
256
+ ({
257
+ payload,
258
+ notifyCompletion = true,
259
+ requestFullscreenExit = true,
260
+ notifyIframe = false,
261
+ markPlayed = true
262
+ } = {}) => {
263
+ setShowGameIframe(false);
264
+ if (markPlayed) {
265
+ setHasPlayedOnce(true);
266
+ }
267
+ releaseEscapeKeyLock();
268
+ if (notifyIframe) {
269
+ postExitFullscreenToIframe();
270
+ }
271
+ if (requestFullscreenExit) {
272
+ void exitEmbedFullscreen();
273
+ }
274
+ if (notifyCompletion) {
275
+ notifyDemoCompletedOnce(payload);
276
+ }
277
+ },
278
+ [
279
+ exitEmbedFullscreen,
280
+ notifyDemoCompletedOnce,
281
+ postExitFullscreenToIframe,
282
+ releaseEscapeKeyLock
283
+ ]
284
+ );
285
+ const markStreamActive = (0, import_react.useCallback)(() => {
286
+ hasNotifiedCompletionRef.current = false;
287
+ setShowGameIframe(true);
288
+ }, []);
289
+ const forwardEventToParent = (0, import_react.useCallback)(
290
+ (message) => {
291
+ if (!forwardEventsToParent || typeof window === "undefined" || window.parent === window) {
292
+ return;
293
+ }
294
+ try {
295
+ window.parent.postMessage(message, "*");
296
+ } catch (error) {
297
+ console.warn("[PlayruoEmbed] Failed to forward event to parent:", error);
298
+ }
299
+ },
300
+ [forwardEventsToParent]
301
+ );
154
302
  const lockEscapeKey = (0, import_react.useCallback)(async () => {
155
303
  const keyboard = getKeyboardApi();
156
304
  if (!keyboard?.lock) {
@@ -187,19 +335,27 @@ var PlayruoEmbed = ({
187
335
  console.warn("[PlayruoEmbed] Iframe not ready");
188
336
  return;
189
337
  }
338
+ hasNotifiedCompletionRef.current = false;
190
339
  setShowGameIframe(true);
191
340
  try {
192
- try {
193
- await requestImmersiveMode(iframe);
194
- } catch (fsError) {
195
- console.warn("[PlayruoEmbed] Fullscreen denied, continuing without:", fsError);
341
+ let isParentFullscreen = false;
342
+ if (enableWindowedLaunch) {
343
+ releaseEscapeKeyLock();
344
+ console.log("[PlayruoEmbed] Windowed launch enabled, skipping fullscreen request");
345
+ } else {
346
+ try {
347
+ await requestImmersiveMode(iframe);
348
+ isParentFullscreen = true;
349
+ } catch (fsError) {
350
+ console.warn("[PlayruoEmbed] Fullscreen denied, continuing without:", fsError);
351
+ }
196
352
  }
197
353
  iframe.contentWindow?.postMessage({
198
354
  type: "playruo",
199
355
  action: "tryNowClick",
200
356
  fromEmbed: true,
201
- isFullscreen: true,
202
- parentWillHandleFullscreen: true,
357
+ isFullscreen: isParentFullscreen,
358
+ parentWillHandleFullscreen: isParentFullscreen,
203
359
  isReplay: hasPlayedOnce
204
360
  }, "*");
205
361
  console.log("[PlayruoEmbed] Sent tryNowClick to game", hasPlayedOnce ? "(replay)" : "(first time)");
@@ -210,10 +366,19 @@ var PlayruoEmbed = ({
210
366
  setHasPlayedOnce(true);
211
367
  } catch (error) {
212
368
  console.error("[PlayruoEmbed] Failed to start game:", error);
369
+ setShowGameIframe(false);
213
370
  setErrorMessage("Failed to start game");
214
371
  onError?.("Failed to start game");
215
372
  }
216
- }, [isCompatible, hasPlayedOnce, onError, iframeRef, requestImmersiveMode]);
373
+ }, [
374
+ enableWindowedLaunch,
375
+ hasPlayedOnce,
376
+ iframeRef,
377
+ isCompatible,
378
+ onError,
379
+ releaseEscapeKeyLock,
380
+ requestImmersiveMode
381
+ ]);
217
382
  const handleBuyNow = (0, import_react.useCallback)(() => {
218
383
  if (!storeUrl) {
219
384
  console.warn("[PlayruoEmbed] No store URL provided");
@@ -233,7 +398,8 @@ var PlayruoEmbed = ({
233
398
  }, [isReady, onReady]);
234
399
  (0, import_react.useEffect)(() => {
235
400
  const handleGameMessage = (event) => {
236
- const { type, data } = event.data || {};
401
+ const message = event.data || {};
402
+ const { type, data } = message;
237
403
  const iframe = iframeRef.current;
238
404
  if (!iframe || event.source !== iframe.contentWindow) {
239
405
  return;
@@ -254,8 +420,7 @@ var PlayruoEmbed = ({
254
420
  onPurchaseRequest?.(data);
255
421
  } else if (type === "playruo_demo_completed" && data) {
256
422
  console.log("\u{1F537} [Embed] playruo_demo_completed received:", data);
257
- setHasPlayedOnce(true);
258
- onDemoCompleted?.(data);
423
+ cleanupEmbedSession({ payload: data });
259
424
  } else if (type === "playruo_error") {
260
425
  const errorMessage2 = typeof data === "string" ? data : data?.message || "Unknown error";
261
426
  console.log("\u{1F537} [Embed] playruo_error received:", errorMessage2);
@@ -276,25 +441,65 @@ var PlayruoEmbed = ({
276
441
  onError?.(errorMessage2);
277
442
  } else if (type === "playruo_session_over" && data) {
278
443
  console.log("\u{1F537} [Embed] playruo_session_over received:", data);
279
- setHasPlayedOnce(true);
444
+ void exitEmbedFullscreen({ reason: "playruo_session_over" });
280
445
  setIframeState(data);
281
446
  onStateChange?.(data);
447
+ cleanupEmbedSession({ requestFullscreenExit: false });
282
448
  } else if (type === "playruo_stream_start") {
283
449
  console.log("\u{1F537} [Embed] playruo_stream_start received");
450
+ markStreamActive();
451
+ } else if (type === "playruo_stream_stop") {
452
+ console.log("\u{1F537} [Embed] playruo_stream_stop received");
453
+ void exitEmbedFullscreen({ reason: "playruo_stream_stop" });
454
+ cleanupEmbedSession({
455
+ markPlayed: false,
456
+ notifyCompletion: false,
457
+ requestFullscreenExit: false
458
+ });
459
+ } else if (type === "REQUEST_EXIT_FULLSCREEN") {
460
+ console.log("\u{1F537} [Embed] REQUEST_EXIT_FULLSCREEN received");
461
+ void exitEmbedFullscreen({
462
+ reason: "REQUEST_EXIT_FULLSCREEN",
463
+ suppressFullscreenExitCleanup: false
464
+ }).finally(postExitFullscreenAckToIframe);
465
+ } else if (type === "playruo_event" && data && typeof data === "object") {
466
+ console.log("\u{1F537} [Embed] playruo_event received:", data);
467
+ const eventPayload = data;
468
+ onEvent?.(eventPayload);
469
+ forwardEventToParent({ type: "playruo_event", data: eventPayload });
470
+ } else if (type === "playruo" && message.action === "event" && message.payload && typeof message.payload === "object") {
471
+ console.log("\u{1F537} [Embed] legacy playruo event received:", message.payload);
472
+ const eventPayload = message.payload;
473
+ onEvent?.(eventPayload);
474
+ forwardEventToParent({ type: "playruo_event", data: eventPayload });
284
475
  }
285
476
  if (type === "error" || type === "closeForBuy" || type === "goToStore") {
286
477
  console.log("\u{1F537} [Embed] Legacy message received:", type);
287
- setHasPlayedOnce(true);
288
- const payload = {
289
- reason: "Demo session ended",
290
- storeUrl
291
- };
292
- onDemoCompleted?.(payload);
478
+ cleanupEmbedSession({
479
+ payload: {
480
+ reason: "Demo session ended",
481
+ storeUrl
482
+ }
483
+ });
293
484
  }
294
485
  };
295
486
  window.addEventListener("message", handleGameMessage);
296
487
  return () => window.removeEventListener("message", handleGameMessage);
297
- }, [storeUrl, onDemoCompleted, onReady, onStateChange, onPurchaseRequest, onError, iframeRef]);
488
+ }, [
489
+ cleanupEmbedSession,
490
+ exitEmbedFullscreen,
491
+ forwardEventToParent,
492
+ hasPlayedOnce,
493
+ iframeRef,
494
+ markStreamActive,
495
+ onEvent,
496
+ onError,
497
+ onPurchaseRequest,
498
+ onReady,
499
+ onStateChange,
500
+ postExitFullscreenAckToIframe,
501
+ storeUrl
502
+ ]);
298
503
  const generateUUID = () => {
299
504
  if (self.crypto.randomUUID) return self.crypto.randomUUID();
300
505
  const bytes = new Uint8Array(16);
@@ -341,9 +546,8 @@ var PlayruoEmbed = ({
341
546
  }, [iframeRef]);
342
547
  (0, import_react.useEffect)(() => {
343
548
  const handleFullscreenChange = () => {
344
- const doc = document;
345
549
  const iframe = iframeRef.current;
346
- const fullscreenElement = doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement || null;
550
+ const fullscreenElement = getFullscreenElement();
347
551
  const isEmbedFullscreen = Boolean(iframe && fullscreenElement === iframe);
348
552
  if (isEmbedFullscreen) {
349
553
  wasEmbedFullscreenRef.current = true;
@@ -360,26 +564,15 @@ var PlayruoEmbed = ({
360
564
  return;
361
565
  }
362
566
  releaseEscapeKeyLock();
567
+ if (suppressNextFullscreenExitCleanupRef.current) {
568
+ suppressNextFullscreenExitCleanupRef.current = false;
569
+ return;
570
+ }
363
571
  if (wasEmbedFullscreenRef.current) {
364
572
  wasEmbedFullscreenRef.current = false;
365
- if (iframe) {
366
- try {
367
- iframe.contentWindow?.postMessage({
368
- type: "playruo",
369
- action: "exitFullscreen",
370
- fromEmbed: true,
371
- isFullscreen: false,
372
- parentWillHandleFullscreen: true
373
- }, "*");
374
- } catch {
375
- console.error("[PlayruoEmbed] Failed to exit fullscreen");
376
- }
377
- }
378
- setShowGameIframe(false);
379
- setHasPlayedOnce(true);
380
- onDemoCompleted?.({
381
- reason: "Demo session ended",
382
- storeUrl
573
+ cleanupEmbedSession({
574
+ notifyIframe: true,
575
+ requestFullscreenExit: false
383
576
  });
384
577
  }
385
578
  };
@@ -393,7 +586,13 @@ var PlayruoEmbed = ({
393
586
  document.removeEventListener("mozfullscreenchange", handleFullscreenChange);
394
587
  document.removeEventListener("MSFullscreenChange", handleFullscreenChange);
395
588
  };
396
- }, [storeUrl, onDemoCompleted, iframeRef, lockEscapeKey, releaseEscapeKeyLock]);
589
+ }, [
590
+ cleanupEmbedSession,
591
+ getFullscreenElement,
592
+ iframeRef,
593
+ lockEscapeKey,
594
+ releaseEscapeKeyLock
595
+ ]);
397
596
  (0, import_react.useEffect)(() => {
398
597
  return () => {
399
598
  releaseEscapeKeyLock();
@@ -478,6 +677,8 @@ var PlayruoEmbed = ({
478
677
  }
479
678
  }, [isReady]);
480
679
  const isAvailable = Boolean(isReady && isCompatible && hasIframeConfirmed);
680
+ const shouldDisplayGameIframe = showGameIframe;
681
+ const shouldShowIframeMisuseMessage = Boolean(showIframe && !showGameIframe);
481
682
  const renderContent = () => {
482
683
  if (!renderState) return null;
483
684
  const canTryNow = Boolean(
@@ -507,6 +708,26 @@ var PlayruoEmbed = ({
507
708
  };
508
709
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, children: [
509
710
  renderContent(),
711
+ isCompatible && shouldShowIframeMisuseMessage && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
712
+ "div",
713
+ {
714
+ role: "status",
715
+ style: {
716
+ alignItems: "center",
717
+ background: "#000",
718
+ color: "#fff",
719
+ display: "flex",
720
+ fontFamily: "system-ui, sans-serif",
721
+ fontSize: 14,
722
+ height: "100%",
723
+ justifyContent: "center",
724
+ padding: 24,
725
+ textAlign: "center",
726
+ width: "100%"
727
+ },
728
+ children: "This iframe is only meant to be displayed through PlayruoEmbed during an active stream."
729
+ }
730
+ ),
510
731
  isCompatible && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
511
732
  "iframe",
512
733
  {
@@ -519,9 +740,9 @@ var PlayruoEmbed = ({
519
740
  title: "Playruo",
520
741
  referrerPolicy: "strict-origin-when-cross-origin",
521
742
  style: {
522
- // Only mounted when compatible; visible after click (internal) or if parent explicitly asks
523
- opacity: showGameIframe || showIframe ? 1 : 0,
524
- pointerEvents: showGameIframe || showIframe ? "auto" : "none",
743
+ // The iframe stays mounted for readiness checks, visible only during launch/play, and hidden after terminal events.
744
+ opacity: shouldDisplayGameIframe ? 1 : 0,
745
+ pointerEvents: shouldDisplayGameIframe ? "auto" : "none",
525
746
  width: "100%",
526
747
  height: "100%",
527
748
  border: 0
@@ -577,10 +798,16 @@ var EmbedTester = (props = {}) => {
577
798
  const [storeUrlDraft, setStoreUrlDraft] = (0, import_react2.useState)(
578
799
  props.storeUrl ?? "https://store.steampowered.com/app/123456"
579
800
  );
801
+ const [enableWindowedLaunchDraft, setEnableWindowedLaunchDraft] = (0, import_react2.useState)(
802
+ props.enableWindowedLaunch ?? false
803
+ );
580
804
  const [appliedOperation, setAppliedOperation] = (0, import_react2.useState)(props.operation ?? "iframe");
581
805
  const [appliedStoreUrl, setAppliedStoreUrl] = (0, import_react2.useState)(
582
806
  props.storeUrl ?? "https://store.steampowered.com/app/123456"
583
807
  );
808
+ const [appliedEnableWindowedLaunch, setAppliedEnableWindowedLaunch] = (0, import_react2.useState)(
809
+ props.enableWindowedLaunch ?? false
810
+ );
584
811
  const [key, setKey] = (0, import_react2.useState)(0);
585
812
  const [hasUserClicked, setHasUserClicked] = (0, import_react2.useState)(false);
586
813
  const [showIframe, setShowIframe] = (0, import_react2.useState)(false);
@@ -595,8 +822,9 @@ var EmbedTester = (props = {}) => {
595
822
  const iframeRef = (0, import_react2.useRef)(null);
596
823
  const isOperationPending = operationDraft !== appliedOperation;
597
824
  const isStoreUrlPending = storeUrlDraft !== appliedStoreUrl;
598
- const hasPendingConfigChanges = isOperationPending || isStoreUrlPending;
599
- const pendingChangeCount = Number(isOperationPending) + Number(isStoreUrlPending);
825
+ const isWindowedLaunchPending = enableWindowedLaunchDraft !== appliedEnableWindowedLaunch;
826
+ const hasPendingConfigChanges = isOperationPending || isStoreUrlPending || isWindowedLaunchPending;
827
+ const pendingChangeCount = Number(isOperationPending) + Number(isStoreUrlPending) + Number(isWindowedLaunchPending);
600
828
  const addEvent = (0, import_react2.useCallback)(
601
829
  (message) => {
602
830
  const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
@@ -680,6 +908,13 @@ var EmbedTester = (props = {}) => {
680
908
  console.error("Embed error:", error);
681
909
  }
682
910
  }, [addEvent, enableConsoleLog]);
911
+ const handleEvent = (0, import_react2.useCallback)((data) => {
912
+ const eventName = typeof data.event === "string" ? data.event : typeof data.name === "string" ? data.name : "unknown";
913
+ addEvent(`Event received: ${eventName}`);
914
+ if (enableConsoleLog) {
915
+ console.log("Embed event:", data);
916
+ }
917
+ }, [addEvent, enableConsoleLog]);
683
918
  const handlePurchaseRequest = (0, import_react2.useCallback)((data) => {
684
919
  addEvent(`\u{1F4B3} Explicit purchase request from Buy Now button`);
685
920
  addEvent(`Opening store: ${data.storeUrl}`);
@@ -700,16 +935,24 @@ var EmbedTester = (props = {}) => {
700
935
  }
701
936
  setAppliedOperation(operationDraft);
702
937
  setAppliedStoreUrl(storeUrlDraft);
938
+ setAppliedEnableWindowedLaunch(enableWindowedLaunchDraft);
703
939
  resetEmbedSession("Applied updated embed settings");
704
- }, [hasPendingConfigChanges, operationDraft, resetEmbedSession, storeUrlDraft]);
940
+ }, [
941
+ enableWindowedLaunchDraft,
942
+ hasPendingConfigChanges,
943
+ operationDraft,
944
+ resetEmbedSession,
945
+ storeUrlDraft
946
+ ]);
705
947
  const handleRestartSession = (0, import_react2.useCallback)(() => {
706
948
  resetEmbedSession("Embed restarted with applied settings");
707
949
  }, [resetEmbedSession]);
708
950
  const handleResetDrafts = (0, import_react2.useCallback)(() => {
709
951
  setOperationDraft(appliedOperation);
710
952
  setStoreUrlDraft(appliedStoreUrl);
953
+ setEnableWindowedLaunchDraft(appliedEnableWindowedLaunch);
711
954
  addEvent("Reverted draft changes to the applied configuration");
712
- }, [addEvent, appliedOperation, appliedStoreUrl]);
955
+ }, [addEvent, appliedEnableWindowedLaunch, appliedOperation, appliedStoreUrl]);
713
956
  const panelStyle = {
714
957
  position: "relative",
715
958
  border: "1px solid rgba(148, 163, 184, 0.18)",
@@ -1277,6 +1520,50 @@ var EmbedTester = (props = {}) => {
1277
1520
  "Applied value: ",
1278
1521
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { style: codeStyle, children: appliedStoreUrl })
1279
1522
  ] })
1523
+ ] }),
1524
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
1525
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: labelStyle, children: [
1526
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Launch mode" }),
1527
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: buildIndicatorStyle(isWindowedLaunchPending), children: [
1528
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: buildIndicatorDotStyle(isWindowedLaunchPending) }),
1529
+ isWindowedLaunchPending ? "Pending" : "Live"
1530
+ ] })
1531
+ ] }),
1532
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1533
+ "label",
1534
+ {
1535
+ htmlFor: "enableWindowedLaunch",
1536
+ style: {
1537
+ ...buildInputShellStyle(isWindowedLaunchPending),
1538
+ justifyContent: "space-between",
1539
+ minHeight: "48px",
1540
+ cursor: "pointer"
1541
+ },
1542
+ children: [
1543
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: "#f8fafc", fontSize: "15px", fontWeight: 600 }, children: "Windowed launch" }),
1544
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1545
+ "input",
1546
+ {
1547
+ id: "enableWindowedLaunch",
1548
+ type: "checkbox",
1549
+ checked: enableWindowedLaunchDraft,
1550
+ onChange: (event) => setEnableWindowedLaunchDraft(event.target.checked),
1551
+ style: {
1552
+ width: "18px",
1553
+ height: "18px",
1554
+ accentColor: "#38bdf8",
1555
+ cursor: "pointer"
1556
+ }
1557
+ }
1558
+ )
1559
+ ]
1560
+ }
1561
+ ),
1562
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { style: helperTextStyle, children: [
1563
+ "Applied value:",
1564
+ " ",
1565
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { style: codeStyle, children: appliedEnableWindowedLaunch ? "windowed" : "fullscreen request" })
1566
+ ] })
1280
1567
  ] })
1281
1568
  ] }),
1282
1569
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
@@ -1556,6 +1843,10 @@ var EmbedTester = (props = {}) => {
1556
1843
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { ...codeStyle, padding: "6px 10px" }, children: [
1557
1844
  "Operation: ",
1558
1845
  appliedOperation
1846
+ ] }),
1847
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { ...codeStyle, padding: "6px 10px" }, children: [
1848
+ "Launch: ",
1849
+ appliedEnableWindowedLaunch ? "windowed" : "fullscreen request"
1559
1850
  ] })
1560
1851
  ] })
1561
1852
  ]
@@ -1627,9 +1918,11 @@ var EmbedTester = (props = {}) => {
1627
1918
  iframeRef,
1628
1919
  onDemoCompleted: handleDemoCompleted,
1629
1920
  onError: handleError,
1921
+ onEvent: handleEvent,
1630
1922
  onPurchaseRequest: handlePurchaseRequest,
1631
1923
  onReady: handleReady,
1632
1924
  onStateChange: handleStateChange,
1925
+ enableWindowedLaunch: appliedEnableWindowedLaunch,
1633
1926
  operation: appliedOperation,
1634
1927
  renderState: ({ actions, message, status }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: !hasUserClicked && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1635
1928
  "div",
@@ -1754,7 +2047,7 @@ var isPlayruoEmbedMessage = (data) => {
1754
2047
  if (!data || typeof data !== "object") return false;
1755
2048
  const msg = data;
1756
2049
  if (typeof msg.type !== "string") return false;
1757
- 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";
2050
+ 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" || msg.type === "REQUEST_EXIT_FULLSCREEN" || msg.type === "playruo_event";
1758
2051
  };
1759
2052
  var isPlayruoInternalMessage = (data) => {
1760
2053
  if (!data || typeof data !== "object") return false;
package/dist/index.d.cts CHANGED
@@ -17,6 +17,12 @@ interface DemoCompletedPayload {
17
17
  reason?: string;
18
18
  storeUrl?: string;
19
19
  }
20
+ type PlayruoEventPayload = Record<string, unknown> & {
21
+ action?: string;
22
+ category?: string;
23
+ event?: string;
24
+ name?: string;
25
+ };
20
26
 
21
27
  interface PlayruoEmbedActions {
22
28
  /** Start the demo session */
@@ -44,10 +50,22 @@ interface PlayruoEmbedProps {
44
50
  onReady?: () => void;
45
51
  onStateChange?: (state: PlayruoEmbedState) => void;
46
52
  onDemoCompleted?: (data: DemoCompletedPayload) => void;
53
+ onEvent?: (data: PlayruoEventPayload) => void;
47
54
  onPurchaseRequest?: (data: {
48
55
  storeUrl: string;
49
56
  }) => void;
50
57
  onError?: (error: string) => void;
58
+ /**
59
+ * Skip the parent fullscreen request and start the experience inside the
60
+ * current iframe bounds. Defaults to false to preserve the current behavior.
61
+ */
62
+ enableWindowedLaunch?: boolean;
63
+ /**
64
+ * Forward playruo_event messages to the parent window when the package host
65
+ * itself is embedded in another iframe. Direct host pages already receive the
66
+ * original iframe postMessage.
67
+ */
68
+ forwardEventsToParent?: boolean;
51
69
  /**
52
70
  * Single render entry point representing the current status.
53
71
  * Provides status, message, and available actions based on current state.
@@ -66,6 +84,8 @@ interface EmbedTesterProps {
66
84
  enableConsoleLog?: boolean;
67
85
  operation?: string;
68
86
  storeUrl?: string;
87
+ /** Start the experience inside the iframe without requesting parent fullscreen. */
88
+ enableWindowedLaunch?: boolean;
69
89
  className?: string;
70
90
  }
71
91
  declare const EmbedTester: (props?: EmbedTesterProps) => react_jsx_runtime.JSX.Element;
@@ -103,7 +123,14 @@ interface PlayruoStreamStartMessage {
103
123
  interface PlayruoStreamStopMessage {
104
124
  type: "playruo_stream_stop";
105
125
  }
106
- type PlayruoEmbedMessage = PlayruoReadyMessage | PlayruoStateUpdateMessage | PlayruoPurchaseRequestMessage | PlayruoDemoCompletedMessage | PlayruoErrorMessage | PlayruoSessionOverMessage | PlayruoStreamStartMessage | PlayruoStreamStopMessage;
126
+ interface PlayruoRequestExitFullscreenMessage {
127
+ type: "REQUEST_EXIT_FULLSCREEN";
128
+ }
129
+ interface PlayruoEventMessage {
130
+ type: "playruo_event";
131
+ data: PlayruoEventPayload;
132
+ }
133
+ type PlayruoEmbedMessage = PlayruoReadyMessage | PlayruoStateUpdateMessage | PlayruoPurchaseRequestMessage | PlayruoDemoCompletedMessage | PlayruoErrorMessage | PlayruoSessionOverMessage | PlayruoStreamStartMessage | PlayruoStreamStopMessage | PlayruoRequestExitFullscreenMessage | PlayruoEventMessage;
107
134
  interface PlayruoInternalMessage {
108
135
  type: "playruo";
109
136
  action: "tryNowClick" | "exitFullscreen" | "mute" | "unmute";
@@ -136,4 +163,4 @@ interface BuildUrlParams {
136
163
  */
137
164
  declare const buildEmbedUrl: ({ operation, storeUrl }: BuildUrlParams) => string;
138
165
 
139
- 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 };
166
+ export { type DemoCompletedPayload, EmbedTester, type EmbedTesterProps, type PlayruoDemoCompletedMessage, PlayruoEmbed, type PlayruoEmbedActions, type PlayruoEmbedMessage, type PlayruoEmbedProps, type PlayruoEmbedRenderState, type PlayruoEmbedState, type PlayruoErrorMessage, type PlayruoEventMessage, type PlayruoEventPayload, type PlayruoInternalMessage, type PlayruoPurchaseRequestMessage, type PlayruoReadyMessage, type PlayruoRequestExitFullscreenMessage, type PlayruoSessionOverMessage, type PlayruoStateUpdateMessage, type PlayruoStreamStartMessage, type PlayruoStreamStopMessage, buildEmbedUrl, isPlayruoEmbedMessage, isPlayruoInternalMessage };
package/dist/index.d.ts CHANGED
@@ -17,6 +17,12 @@ interface DemoCompletedPayload {
17
17
  reason?: string;
18
18
  storeUrl?: string;
19
19
  }
20
+ type PlayruoEventPayload = Record<string, unknown> & {
21
+ action?: string;
22
+ category?: string;
23
+ event?: string;
24
+ name?: string;
25
+ };
20
26
 
21
27
  interface PlayruoEmbedActions {
22
28
  /** Start the demo session */
@@ -44,10 +50,22 @@ interface PlayruoEmbedProps {
44
50
  onReady?: () => void;
45
51
  onStateChange?: (state: PlayruoEmbedState) => void;
46
52
  onDemoCompleted?: (data: DemoCompletedPayload) => void;
53
+ onEvent?: (data: PlayruoEventPayload) => void;
47
54
  onPurchaseRequest?: (data: {
48
55
  storeUrl: string;
49
56
  }) => void;
50
57
  onError?: (error: string) => void;
58
+ /**
59
+ * Skip the parent fullscreen request and start the experience inside the
60
+ * current iframe bounds. Defaults to false to preserve the current behavior.
61
+ */
62
+ enableWindowedLaunch?: boolean;
63
+ /**
64
+ * Forward playruo_event messages to the parent window when the package host
65
+ * itself is embedded in another iframe. Direct host pages already receive the
66
+ * original iframe postMessage.
67
+ */
68
+ forwardEventsToParent?: boolean;
51
69
  /**
52
70
  * Single render entry point representing the current status.
53
71
  * Provides status, message, and available actions based on current state.
@@ -66,6 +84,8 @@ interface EmbedTesterProps {
66
84
  enableConsoleLog?: boolean;
67
85
  operation?: string;
68
86
  storeUrl?: string;
87
+ /** Start the experience inside the iframe without requesting parent fullscreen. */
88
+ enableWindowedLaunch?: boolean;
69
89
  className?: string;
70
90
  }
71
91
  declare const EmbedTester: (props?: EmbedTesterProps) => react_jsx_runtime.JSX.Element;
@@ -103,7 +123,14 @@ interface PlayruoStreamStartMessage {
103
123
  interface PlayruoStreamStopMessage {
104
124
  type: "playruo_stream_stop";
105
125
  }
106
- type PlayruoEmbedMessage = PlayruoReadyMessage | PlayruoStateUpdateMessage | PlayruoPurchaseRequestMessage | PlayruoDemoCompletedMessage | PlayruoErrorMessage | PlayruoSessionOverMessage | PlayruoStreamStartMessage | PlayruoStreamStopMessage;
126
+ interface PlayruoRequestExitFullscreenMessage {
127
+ type: "REQUEST_EXIT_FULLSCREEN";
128
+ }
129
+ interface PlayruoEventMessage {
130
+ type: "playruo_event";
131
+ data: PlayruoEventPayload;
132
+ }
133
+ type PlayruoEmbedMessage = PlayruoReadyMessage | PlayruoStateUpdateMessage | PlayruoPurchaseRequestMessage | PlayruoDemoCompletedMessage | PlayruoErrorMessage | PlayruoSessionOverMessage | PlayruoStreamStartMessage | PlayruoStreamStopMessage | PlayruoRequestExitFullscreenMessage | PlayruoEventMessage;
107
134
  interface PlayruoInternalMessage {
108
135
  type: "playruo";
109
136
  action: "tryNowClick" | "exitFullscreen" | "mute" | "unmute";
@@ -136,4 +163,4 @@ interface BuildUrlParams {
136
163
  */
137
164
  declare const buildEmbedUrl: ({ operation, storeUrl }: BuildUrlParams) => string;
138
165
 
139
- 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 };
166
+ export { type DemoCompletedPayload, EmbedTester, type EmbedTesterProps, type PlayruoDemoCompletedMessage, PlayruoEmbed, type PlayruoEmbedActions, type PlayruoEmbedMessage, type PlayruoEmbedProps, type PlayruoEmbedRenderState, type PlayruoEmbedState, type PlayruoErrorMessage, type PlayruoEventMessage, type PlayruoEventPayload, type PlayruoInternalMessage, type PlayruoPurchaseRequestMessage, type PlayruoReadyMessage, type PlayruoRequestExitFullscreenMessage, type PlayruoSessionOverMessage, type PlayruoStateUpdateMessage, type PlayruoStreamStartMessage, type PlayruoStreamStopMessage, buildEmbedUrl, isPlayruoEmbedMessage, isPlayruoInternalMessage };
package/dist/index.js CHANGED
@@ -34,14 +34,19 @@ var PlayruoEmbed = ({
34
34
  onReady,
35
35
  onStateChange,
36
36
  onDemoCompleted,
37
+ onEvent,
37
38
  onPurchaseRequest,
38
39
  onError,
40
+ enableWindowedLaunch = false,
41
+ forwardEventsToParent = true,
39
42
  renderState,
40
43
  iframeRef: externalIframeRef
41
44
  }) => {
42
45
  const internalIframeRef = useRef(null);
43
46
  const iframeRef = externalIframeRef || internalIframeRef;
44
47
  const wasEmbedFullscreenRef = useRef(false);
48
+ const hasNotifiedCompletionRef = useRef(false);
49
+ const suppressNextFullscreenExitCleanupRef = useRef(false);
45
50
  const [isIframeLoaded, setIsIframeLoaded] = useState(false);
46
51
  const [hasPlayedOnce, setHasPlayedOnce] = useState(false);
47
52
  const [showGameIframe, setShowGameIframe] = useState(false);
@@ -111,6 +116,149 @@ var PlayruoEmbed = ({
111
116
  console.warn("[PlayruoEmbed] Failed to release Escape key lock:", error);
112
117
  }
113
118
  }, [getKeyboardApi]);
119
+ const getFullscreenElement = useCallback(() => {
120
+ const doc = document;
121
+ return doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement || null;
122
+ }, []);
123
+ const notifyDemoCompletedOnce = useCallback(
124
+ (payload) => {
125
+ if (hasNotifiedCompletionRef.current) {
126
+ return;
127
+ }
128
+ hasNotifiedCompletionRef.current = true;
129
+ onDemoCompleted?.(
130
+ payload ?? {
131
+ reason: "Demo session ended",
132
+ storeUrl
133
+ }
134
+ );
135
+ },
136
+ [onDemoCompleted, storeUrl]
137
+ );
138
+ const postExitFullscreenToIframe = useCallback(() => {
139
+ const iframe = iframeRef.current;
140
+ if (!iframe) {
141
+ return;
142
+ }
143
+ try {
144
+ iframe.contentWindow?.postMessage({
145
+ type: "playruo",
146
+ action: "exitFullscreen",
147
+ fromEmbed: true,
148
+ isFullscreen: false,
149
+ parentWillHandleFullscreen: true
150
+ }, "*");
151
+ } catch {
152
+ console.error("[PlayruoEmbed] Failed to exit fullscreen");
153
+ }
154
+ }, [iframeRef]);
155
+ const postExitFullscreenAckToIframe = useCallback(() => {
156
+ const iframe = iframeRef.current;
157
+ if (!iframe) {
158
+ return;
159
+ }
160
+ try {
161
+ iframe.contentWindow?.postMessage({ type: "ACK_EXIT_FULLSCREEN" }, "*");
162
+ } catch {
163
+ console.error("[PlayruoEmbed] Failed to acknowledge fullscreen exit");
164
+ }
165
+ }, [iframeRef]);
166
+ const exitEmbedFullscreen = useCallback(
167
+ async ({
168
+ reason = "cleanup",
169
+ suppressFullscreenExitCleanup = true
170
+ } = {}) => {
171
+ const fullscreenElement = getFullscreenElement();
172
+ const doc = document;
173
+ const exitFullscreen = doc.exitFullscreen?.bind(doc) ?? doc.webkitExitFullscreen?.bind(doc) ?? doc.mozCancelFullScreen?.bind(doc) ?? doc.msExitFullscreen?.bind(doc);
174
+ const shouldAttemptExit = Boolean(fullscreenElement || wasEmbedFullscreenRef.current);
175
+ if (!shouldAttemptExit || !exitFullscreen) {
176
+ console.log("[PlayruoEmbed] Parent fullscreen exit skipped", {
177
+ hasFullscreenElement: Boolean(fullscreenElement),
178
+ hasExitFullscreen: Boolean(exitFullscreen),
179
+ reason,
180
+ wasEmbedFullscreen: wasEmbedFullscreenRef.current
181
+ });
182
+ return false;
183
+ }
184
+ console.log("[PlayruoEmbed] Parent fullscreen exit requested", {
185
+ fullscreenElement: fullscreenElement?.tagName,
186
+ reason,
187
+ wasEmbedFullscreen: wasEmbedFullscreenRef.current
188
+ });
189
+ const shouldSuppressFullscreenExitCleanup = Boolean(
190
+ suppressFullscreenExitCleanup && fullscreenElement
191
+ );
192
+ if (shouldSuppressFullscreenExitCleanup) {
193
+ suppressNextFullscreenExitCleanupRef.current = true;
194
+ wasEmbedFullscreenRef.current = false;
195
+ }
196
+ try {
197
+ await exitFullscreen();
198
+ if (shouldSuppressFullscreenExitCleanup) {
199
+ window.setTimeout(() => {
200
+ suppressNextFullscreenExitCleanupRef.current = false;
201
+ }, 1e3);
202
+ }
203
+ return true;
204
+ } catch (error) {
205
+ if (shouldSuppressFullscreenExitCleanup) {
206
+ suppressNextFullscreenExitCleanupRef.current = false;
207
+ wasEmbedFullscreenRef.current = Boolean(getFullscreenElement());
208
+ }
209
+ console.warn("[PlayruoEmbed] Failed to leave fullscreen:", error);
210
+ return false;
211
+ }
212
+ },
213
+ [getFullscreenElement]
214
+ );
215
+ const cleanupEmbedSession = useCallback(
216
+ ({
217
+ payload,
218
+ notifyCompletion = true,
219
+ requestFullscreenExit = true,
220
+ notifyIframe = false,
221
+ markPlayed = true
222
+ } = {}) => {
223
+ setShowGameIframe(false);
224
+ if (markPlayed) {
225
+ setHasPlayedOnce(true);
226
+ }
227
+ releaseEscapeKeyLock();
228
+ if (notifyIframe) {
229
+ postExitFullscreenToIframe();
230
+ }
231
+ if (requestFullscreenExit) {
232
+ void exitEmbedFullscreen();
233
+ }
234
+ if (notifyCompletion) {
235
+ notifyDemoCompletedOnce(payload);
236
+ }
237
+ },
238
+ [
239
+ exitEmbedFullscreen,
240
+ notifyDemoCompletedOnce,
241
+ postExitFullscreenToIframe,
242
+ releaseEscapeKeyLock
243
+ ]
244
+ );
245
+ const markStreamActive = useCallback(() => {
246
+ hasNotifiedCompletionRef.current = false;
247
+ setShowGameIframe(true);
248
+ }, []);
249
+ const forwardEventToParent = useCallback(
250
+ (message) => {
251
+ if (!forwardEventsToParent || typeof window === "undefined" || window.parent === window) {
252
+ return;
253
+ }
254
+ try {
255
+ window.parent.postMessage(message, "*");
256
+ } catch (error) {
257
+ console.warn("[PlayruoEmbed] Failed to forward event to parent:", error);
258
+ }
259
+ },
260
+ [forwardEventsToParent]
261
+ );
114
262
  const lockEscapeKey = useCallback(async () => {
115
263
  const keyboard = getKeyboardApi();
116
264
  if (!keyboard?.lock) {
@@ -147,19 +295,27 @@ var PlayruoEmbed = ({
147
295
  console.warn("[PlayruoEmbed] Iframe not ready");
148
296
  return;
149
297
  }
298
+ hasNotifiedCompletionRef.current = false;
150
299
  setShowGameIframe(true);
151
300
  try {
152
- try {
153
- await requestImmersiveMode(iframe);
154
- } catch (fsError) {
155
- console.warn("[PlayruoEmbed] Fullscreen denied, continuing without:", fsError);
301
+ let isParentFullscreen = false;
302
+ if (enableWindowedLaunch) {
303
+ releaseEscapeKeyLock();
304
+ console.log("[PlayruoEmbed] Windowed launch enabled, skipping fullscreen request");
305
+ } else {
306
+ try {
307
+ await requestImmersiveMode(iframe);
308
+ isParentFullscreen = true;
309
+ } catch (fsError) {
310
+ console.warn("[PlayruoEmbed] Fullscreen denied, continuing without:", fsError);
311
+ }
156
312
  }
157
313
  iframe.contentWindow?.postMessage({
158
314
  type: "playruo",
159
315
  action: "tryNowClick",
160
316
  fromEmbed: true,
161
- isFullscreen: true,
162
- parentWillHandleFullscreen: true,
317
+ isFullscreen: isParentFullscreen,
318
+ parentWillHandleFullscreen: isParentFullscreen,
163
319
  isReplay: hasPlayedOnce
164
320
  }, "*");
165
321
  console.log("[PlayruoEmbed] Sent tryNowClick to game", hasPlayedOnce ? "(replay)" : "(first time)");
@@ -170,10 +326,19 @@ var PlayruoEmbed = ({
170
326
  setHasPlayedOnce(true);
171
327
  } catch (error) {
172
328
  console.error("[PlayruoEmbed] Failed to start game:", error);
329
+ setShowGameIframe(false);
173
330
  setErrorMessage("Failed to start game");
174
331
  onError?.("Failed to start game");
175
332
  }
176
- }, [isCompatible, hasPlayedOnce, onError, iframeRef, requestImmersiveMode]);
333
+ }, [
334
+ enableWindowedLaunch,
335
+ hasPlayedOnce,
336
+ iframeRef,
337
+ isCompatible,
338
+ onError,
339
+ releaseEscapeKeyLock,
340
+ requestImmersiveMode
341
+ ]);
177
342
  const handleBuyNow = useCallback(() => {
178
343
  if (!storeUrl) {
179
344
  console.warn("[PlayruoEmbed] No store URL provided");
@@ -193,7 +358,8 @@ var PlayruoEmbed = ({
193
358
  }, [isReady, onReady]);
194
359
  useEffect(() => {
195
360
  const handleGameMessage = (event) => {
196
- const { type, data } = event.data || {};
361
+ const message = event.data || {};
362
+ const { type, data } = message;
197
363
  const iframe = iframeRef.current;
198
364
  if (!iframe || event.source !== iframe.contentWindow) {
199
365
  return;
@@ -214,8 +380,7 @@ var PlayruoEmbed = ({
214
380
  onPurchaseRequest?.(data);
215
381
  } else if (type === "playruo_demo_completed" && data) {
216
382
  console.log("\u{1F537} [Embed] playruo_demo_completed received:", data);
217
- setHasPlayedOnce(true);
218
- onDemoCompleted?.(data);
383
+ cleanupEmbedSession({ payload: data });
219
384
  } else if (type === "playruo_error") {
220
385
  const errorMessage2 = typeof data === "string" ? data : data?.message || "Unknown error";
221
386
  console.log("\u{1F537} [Embed] playruo_error received:", errorMessage2);
@@ -236,25 +401,65 @@ var PlayruoEmbed = ({
236
401
  onError?.(errorMessage2);
237
402
  } else if (type === "playruo_session_over" && data) {
238
403
  console.log("\u{1F537} [Embed] playruo_session_over received:", data);
239
- setHasPlayedOnce(true);
404
+ void exitEmbedFullscreen({ reason: "playruo_session_over" });
240
405
  setIframeState(data);
241
406
  onStateChange?.(data);
407
+ cleanupEmbedSession({ requestFullscreenExit: false });
242
408
  } else if (type === "playruo_stream_start") {
243
409
  console.log("\u{1F537} [Embed] playruo_stream_start received");
410
+ markStreamActive();
411
+ } else if (type === "playruo_stream_stop") {
412
+ console.log("\u{1F537} [Embed] playruo_stream_stop received");
413
+ void exitEmbedFullscreen({ reason: "playruo_stream_stop" });
414
+ cleanupEmbedSession({
415
+ markPlayed: false,
416
+ notifyCompletion: false,
417
+ requestFullscreenExit: false
418
+ });
419
+ } else if (type === "REQUEST_EXIT_FULLSCREEN") {
420
+ console.log("\u{1F537} [Embed] REQUEST_EXIT_FULLSCREEN received");
421
+ void exitEmbedFullscreen({
422
+ reason: "REQUEST_EXIT_FULLSCREEN",
423
+ suppressFullscreenExitCleanup: false
424
+ }).finally(postExitFullscreenAckToIframe);
425
+ } else if (type === "playruo_event" && data && typeof data === "object") {
426
+ console.log("\u{1F537} [Embed] playruo_event received:", data);
427
+ const eventPayload = data;
428
+ onEvent?.(eventPayload);
429
+ forwardEventToParent({ type: "playruo_event", data: eventPayload });
430
+ } else if (type === "playruo" && message.action === "event" && message.payload && typeof message.payload === "object") {
431
+ console.log("\u{1F537} [Embed] legacy playruo event received:", message.payload);
432
+ const eventPayload = message.payload;
433
+ onEvent?.(eventPayload);
434
+ forwardEventToParent({ type: "playruo_event", data: eventPayload });
244
435
  }
245
436
  if (type === "error" || type === "closeForBuy" || type === "goToStore") {
246
437
  console.log("\u{1F537} [Embed] Legacy message received:", type);
247
- setHasPlayedOnce(true);
248
- const payload = {
249
- reason: "Demo session ended",
250
- storeUrl
251
- };
252
- onDemoCompleted?.(payload);
438
+ cleanupEmbedSession({
439
+ payload: {
440
+ reason: "Demo session ended",
441
+ storeUrl
442
+ }
443
+ });
253
444
  }
254
445
  };
255
446
  window.addEventListener("message", handleGameMessage);
256
447
  return () => window.removeEventListener("message", handleGameMessage);
257
- }, [storeUrl, onDemoCompleted, onReady, onStateChange, onPurchaseRequest, onError, iframeRef]);
448
+ }, [
449
+ cleanupEmbedSession,
450
+ exitEmbedFullscreen,
451
+ forwardEventToParent,
452
+ hasPlayedOnce,
453
+ iframeRef,
454
+ markStreamActive,
455
+ onEvent,
456
+ onError,
457
+ onPurchaseRequest,
458
+ onReady,
459
+ onStateChange,
460
+ postExitFullscreenAckToIframe,
461
+ storeUrl
462
+ ]);
258
463
  const generateUUID = () => {
259
464
  if (self.crypto.randomUUID) return self.crypto.randomUUID();
260
465
  const bytes = new Uint8Array(16);
@@ -301,9 +506,8 @@ var PlayruoEmbed = ({
301
506
  }, [iframeRef]);
302
507
  useEffect(() => {
303
508
  const handleFullscreenChange = () => {
304
- const doc = document;
305
509
  const iframe = iframeRef.current;
306
- const fullscreenElement = doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement || null;
510
+ const fullscreenElement = getFullscreenElement();
307
511
  const isEmbedFullscreen = Boolean(iframe && fullscreenElement === iframe);
308
512
  if (isEmbedFullscreen) {
309
513
  wasEmbedFullscreenRef.current = true;
@@ -320,26 +524,15 @@ var PlayruoEmbed = ({
320
524
  return;
321
525
  }
322
526
  releaseEscapeKeyLock();
527
+ if (suppressNextFullscreenExitCleanupRef.current) {
528
+ suppressNextFullscreenExitCleanupRef.current = false;
529
+ return;
530
+ }
323
531
  if (wasEmbedFullscreenRef.current) {
324
532
  wasEmbedFullscreenRef.current = false;
325
- if (iframe) {
326
- try {
327
- iframe.contentWindow?.postMessage({
328
- type: "playruo",
329
- action: "exitFullscreen",
330
- fromEmbed: true,
331
- isFullscreen: false,
332
- parentWillHandleFullscreen: true
333
- }, "*");
334
- } catch {
335
- console.error("[PlayruoEmbed] Failed to exit fullscreen");
336
- }
337
- }
338
- setShowGameIframe(false);
339
- setHasPlayedOnce(true);
340
- onDemoCompleted?.({
341
- reason: "Demo session ended",
342
- storeUrl
533
+ cleanupEmbedSession({
534
+ notifyIframe: true,
535
+ requestFullscreenExit: false
343
536
  });
344
537
  }
345
538
  };
@@ -353,7 +546,13 @@ var PlayruoEmbed = ({
353
546
  document.removeEventListener("mozfullscreenchange", handleFullscreenChange);
354
547
  document.removeEventListener("MSFullscreenChange", handleFullscreenChange);
355
548
  };
356
- }, [storeUrl, onDemoCompleted, iframeRef, lockEscapeKey, releaseEscapeKeyLock]);
549
+ }, [
550
+ cleanupEmbedSession,
551
+ getFullscreenElement,
552
+ iframeRef,
553
+ lockEscapeKey,
554
+ releaseEscapeKeyLock
555
+ ]);
357
556
  useEffect(() => {
358
557
  return () => {
359
558
  releaseEscapeKeyLock();
@@ -438,6 +637,8 @@ var PlayruoEmbed = ({
438
637
  }
439
638
  }, [isReady]);
440
639
  const isAvailable = Boolean(isReady && isCompatible && hasIframeConfirmed);
640
+ const shouldDisplayGameIframe = showGameIframe;
641
+ const shouldShowIframeMisuseMessage = Boolean(showIframe && !showGameIframe);
441
642
  const renderContent = () => {
442
643
  if (!renderState) return null;
443
644
  const canTryNow = Boolean(
@@ -467,6 +668,26 @@ var PlayruoEmbed = ({
467
668
  };
468
669
  return /* @__PURE__ */ jsxs("div", { className, children: [
469
670
  renderContent(),
671
+ isCompatible && shouldShowIframeMisuseMessage && /* @__PURE__ */ jsx(
672
+ "div",
673
+ {
674
+ role: "status",
675
+ style: {
676
+ alignItems: "center",
677
+ background: "#000",
678
+ color: "#fff",
679
+ display: "flex",
680
+ fontFamily: "system-ui, sans-serif",
681
+ fontSize: 14,
682
+ height: "100%",
683
+ justifyContent: "center",
684
+ padding: 24,
685
+ textAlign: "center",
686
+ width: "100%"
687
+ },
688
+ children: "This iframe is only meant to be displayed through PlayruoEmbed during an active stream."
689
+ }
690
+ ),
470
691
  isCompatible && /* @__PURE__ */ jsx(
471
692
  "iframe",
472
693
  {
@@ -479,9 +700,9 @@ var PlayruoEmbed = ({
479
700
  title: "Playruo",
480
701
  referrerPolicy: "strict-origin-when-cross-origin",
481
702
  style: {
482
- // Only mounted when compatible; visible after click (internal) or if parent explicitly asks
483
- opacity: showGameIframe || showIframe ? 1 : 0,
484
- pointerEvents: showGameIframe || showIframe ? "auto" : "none",
703
+ // The iframe stays mounted for readiness checks, visible only during launch/play, and hidden after terminal events.
704
+ opacity: shouldDisplayGameIframe ? 1 : 0,
705
+ pointerEvents: shouldDisplayGameIframe ? "auto" : "none",
485
706
  width: "100%",
486
707
  height: "100%",
487
708
  border: 0
@@ -537,10 +758,16 @@ var EmbedTester = (props = {}) => {
537
758
  const [storeUrlDraft, setStoreUrlDraft] = useState2(
538
759
  props.storeUrl ?? "https://store.steampowered.com/app/123456"
539
760
  );
761
+ const [enableWindowedLaunchDraft, setEnableWindowedLaunchDraft] = useState2(
762
+ props.enableWindowedLaunch ?? false
763
+ );
540
764
  const [appliedOperation, setAppliedOperation] = useState2(props.operation ?? "iframe");
541
765
  const [appliedStoreUrl, setAppliedStoreUrl] = useState2(
542
766
  props.storeUrl ?? "https://store.steampowered.com/app/123456"
543
767
  );
768
+ const [appliedEnableWindowedLaunch, setAppliedEnableWindowedLaunch] = useState2(
769
+ props.enableWindowedLaunch ?? false
770
+ );
544
771
  const [key, setKey] = useState2(0);
545
772
  const [hasUserClicked, setHasUserClicked] = useState2(false);
546
773
  const [showIframe, setShowIframe] = useState2(false);
@@ -555,8 +782,9 @@ var EmbedTester = (props = {}) => {
555
782
  const iframeRef = useRef2(null);
556
783
  const isOperationPending = operationDraft !== appliedOperation;
557
784
  const isStoreUrlPending = storeUrlDraft !== appliedStoreUrl;
558
- const hasPendingConfigChanges = isOperationPending || isStoreUrlPending;
559
- const pendingChangeCount = Number(isOperationPending) + Number(isStoreUrlPending);
785
+ const isWindowedLaunchPending = enableWindowedLaunchDraft !== appliedEnableWindowedLaunch;
786
+ const hasPendingConfigChanges = isOperationPending || isStoreUrlPending || isWindowedLaunchPending;
787
+ const pendingChangeCount = Number(isOperationPending) + Number(isStoreUrlPending) + Number(isWindowedLaunchPending);
560
788
  const addEvent = useCallback2(
561
789
  (message) => {
562
790
  const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
@@ -640,6 +868,13 @@ var EmbedTester = (props = {}) => {
640
868
  console.error("Embed error:", error);
641
869
  }
642
870
  }, [addEvent, enableConsoleLog]);
871
+ const handleEvent = useCallback2((data) => {
872
+ const eventName = typeof data.event === "string" ? data.event : typeof data.name === "string" ? data.name : "unknown";
873
+ addEvent(`Event received: ${eventName}`);
874
+ if (enableConsoleLog) {
875
+ console.log("Embed event:", data);
876
+ }
877
+ }, [addEvent, enableConsoleLog]);
643
878
  const handlePurchaseRequest = useCallback2((data) => {
644
879
  addEvent(`\u{1F4B3} Explicit purchase request from Buy Now button`);
645
880
  addEvent(`Opening store: ${data.storeUrl}`);
@@ -660,16 +895,24 @@ var EmbedTester = (props = {}) => {
660
895
  }
661
896
  setAppliedOperation(operationDraft);
662
897
  setAppliedStoreUrl(storeUrlDraft);
898
+ setAppliedEnableWindowedLaunch(enableWindowedLaunchDraft);
663
899
  resetEmbedSession("Applied updated embed settings");
664
- }, [hasPendingConfigChanges, operationDraft, resetEmbedSession, storeUrlDraft]);
900
+ }, [
901
+ enableWindowedLaunchDraft,
902
+ hasPendingConfigChanges,
903
+ operationDraft,
904
+ resetEmbedSession,
905
+ storeUrlDraft
906
+ ]);
665
907
  const handleRestartSession = useCallback2(() => {
666
908
  resetEmbedSession("Embed restarted with applied settings");
667
909
  }, [resetEmbedSession]);
668
910
  const handleResetDrafts = useCallback2(() => {
669
911
  setOperationDraft(appliedOperation);
670
912
  setStoreUrlDraft(appliedStoreUrl);
913
+ setEnableWindowedLaunchDraft(appliedEnableWindowedLaunch);
671
914
  addEvent("Reverted draft changes to the applied configuration");
672
- }, [addEvent, appliedOperation, appliedStoreUrl]);
915
+ }, [addEvent, appliedEnableWindowedLaunch, appliedOperation, appliedStoreUrl]);
673
916
  const panelStyle = {
674
917
  position: "relative",
675
918
  border: "1px solid rgba(148, 163, 184, 0.18)",
@@ -1237,6 +1480,50 @@ var EmbedTester = (props = {}) => {
1237
1480
  "Applied value: ",
1238
1481
  /* @__PURE__ */ jsx2("code", { style: codeStyle, children: appliedStoreUrl })
1239
1482
  ] })
1483
+ ] }),
1484
+ /* @__PURE__ */ jsxs2("div", { children: [
1485
+ /* @__PURE__ */ jsxs2("div", { style: labelStyle, children: [
1486
+ /* @__PURE__ */ jsx2("span", { children: "Launch mode" }),
1487
+ /* @__PURE__ */ jsxs2("span", { style: buildIndicatorStyle(isWindowedLaunchPending), children: [
1488
+ /* @__PURE__ */ jsx2("span", { style: buildIndicatorDotStyle(isWindowedLaunchPending) }),
1489
+ isWindowedLaunchPending ? "Pending" : "Live"
1490
+ ] })
1491
+ ] }),
1492
+ /* @__PURE__ */ jsxs2(
1493
+ "label",
1494
+ {
1495
+ htmlFor: "enableWindowedLaunch",
1496
+ style: {
1497
+ ...buildInputShellStyle(isWindowedLaunchPending),
1498
+ justifyContent: "space-between",
1499
+ minHeight: "48px",
1500
+ cursor: "pointer"
1501
+ },
1502
+ children: [
1503
+ /* @__PURE__ */ jsx2("span", { style: { color: "#f8fafc", fontSize: "15px", fontWeight: 600 }, children: "Windowed launch" }),
1504
+ /* @__PURE__ */ jsx2(
1505
+ "input",
1506
+ {
1507
+ id: "enableWindowedLaunch",
1508
+ type: "checkbox",
1509
+ checked: enableWindowedLaunchDraft,
1510
+ onChange: (event) => setEnableWindowedLaunchDraft(event.target.checked),
1511
+ style: {
1512
+ width: "18px",
1513
+ height: "18px",
1514
+ accentColor: "#38bdf8",
1515
+ cursor: "pointer"
1516
+ }
1517
+ }
1518
+ )
1519
+ ]
1520
+ }
1521
+ ),
1522
+ /* @__PURE__ */ jsxs2("p", { style: helperTextStyle, children: [
1523
+ "Applied value:",
1524
+ " ",
1525
+ /* @__PURE__ */ jsx2("code", { style: codeStyle, children: appliedEnableWindowedLaunch ? "windowed" : "fullscreen request" })
1526
+ ] })
1240
1527
  ] })
1241
1528
  ] }),
1242
1529
  /* @__PURE__ */ jsxs2(
@@ -1516,6 +1803,10 @@ var EmbedTester = (props = {}) => {
1516
1803
  /* @__PURE__ */ jsxs2("span", { style: { ...codeStyle, padding: "6px 10px" }, children: [
1517
1804
  "Operation: ",
1518
1805
  appliedOperation
1806
+ ] }),
1807
+ /* @__PURE__ */ jsxs2("span", { style: { ...codeStyle, padding: "6px 10px" }, children: [
1808
+ "Launch: ",
1809
+ appliedEnableWindowedLaunch ? "windowed" : "fullscreen request"
1519
1810
  ] })
1520
1811
  ] })
1521
1812
  ]
@@ -1587,9 +1878,11 @@ var EmbedTester = (props = {}) => {
1587
1878
  iframeRef,
1588
1879
  onDemoCompleted: handleDemoCompleted,
1589
1880
  onError: handleError,
1881
+ onEvent: handleEvent,
1590
1882
  onPurchaseRequest: handlePurchaseRequest,
1591
1883
  onReady: handleReady,
1592
1884
  onStateChange: handleStateChange,
1885
+ enableWindowedLaunch: appliedEnableWindowedLaunch,
1593
1886
  operation: appliedOperation,
1594
1887
  renderState: ({ actions, message, status }) => /* @__PURE__ */ jsx2(Fragment, { children: !hasUserClicked && /* @__PURE__ */ jsx2(
1595
1888
  "div",
@@ -1714,7 +2007,7 @@ var isPlayruoEmbedMessage = (data) => {
1714
2007
  if (!data || typeof data !== "object") return false;
1715
2008
  const msg = data;
1716
2009
  if (typeof msg.type !== "string") return false;
1717
- 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";
2010
+ 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" || msg.type === "REQUEST_EXIT_FULLSCREEN" || msg.type === "playruo_event";
1718
2011
  };
1719
2012
  var isPlayruoInternalMessage = (data) => {
1720
2013
  if (!data || typeof data !== "object") return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playruo/embed",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "React SDK to embed Playruo with postMessage API.",
5
5
  "license": "MIT",
6
6
  "type": "module",