@playruo/embed 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,15 +8,33 @@ var normalizeOperation = (operation) => {
8
8
  if (trimmedOperation === "") {
9
9
  throw new Error("`operation` is required to build a Playruo embed URL.");
10
10
  }
11
+ const operationQuery = trimmedOperation.split("?")[1];
12
+ if (operationQuery != null && new URLSearchParams(operationQuery).has("op")) {
13
+ console.warn(
14
+ "[PlayruoEmbed] `?op=` inside `operation` is deprecated: pass it as the `operationKey` prop."
15
+ );
16
+ }
11
17
  return trimmedOperation.startsWith("/") ? trimmedOperation.slice(1) : trimmedOperation;
12
18
  };
13
- var buildEmbedUrl = ({ operation, storeUrl }) => {
14
- return buildEmbedUrlWithOrigin(PLAYRUO_ORIGIN, { operation, storeUrl });
19
+ var buildEmbedUrl = ({
20
+ operation,
21
+ operationKey,
22
+ storeUrl
23
+ }) => {
24
+ return buildEmbedUrlWithOrigin(PLAYRUO_ORIGIN, {
25
+ operation,
26
+ operationKey,
27
+ storeUrl
28
+ });
15
29
  };
16
- var buildEmbedUrlWithOrigin = (origin, { operation, storeUrl }) => {
30
+ var buildEmbedUrlWithOrigin = (origin, { operation, operationKey, storeUrl }) => {
17
31
  const operationPath = normalizeOperation(operation);
18
32
  const url = new URL(`/${operationPath}`, origin);
19
33
  url.searchParams.set("iframe", "true");
34
+ const trimmedOperationKey = operationKey?.trim();
35
+ if (trimmedOperationKey) {
36
+ url.searchParams.set("op", trimmedOperationKey);
37
+ }
20
38
  if (storeUrl) {
21
39
  url.searchParams.set("storeUrl", storeUrl);
22
40
  }
@@ -28,6 +46,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
28
46
  var PlayruoEmbed = ({
29
47
  baseUrl,
30
48
  operation,
49
+ operationKey,
31
50
  storeUrl,
32
51
  showIframe = false,
33
52
  className,
@@ -37,6 +56,8 @@ var PlayruoEmbed = ({
37
56
  onEvent,
38
57
  onPurchaseRequest,
39
58
  onError,
59
+ onPasswordRequired,
60
+ onPasswordResult,
40
61
  enableWindowedLaunch = false,
41
62
  forwardEventsToParent = true,
42
63
  renderState,
@@ -54,12 +75,24 @@ var PlayruoEmbed = ({
54
75
  const [isValidChromium, setIsValidChromium] = useState(true);
55
76
  const [errorMessage, setErrorMessage] = useState(null);
56
77
  const [iframeState, setIframeState] = useState(null);
78
+ const [passwordRequired, setPasswordRequired] = useState(false);
79
+ const [passwordChecked, setPasswordChecked] = useState(false);
80
+ const [passwordUnlocked, setPasswordUnlocked] = useState(false);
81
+ const [passwordError, setPasswordError] = useState(
82
+ void 0
83
+ );
57
84
  const src = useMemo(
58
- () => baseUrl != null && baseUrl !== "" ? buildEmbedUrlWithOrigin(baseUrl, { operation, storeUrl }) : buildEmbedUrl({ operation, storeUrl }),
59
- [baseUrl, operation, storeUrl]
85
+ () => baseUrl != null && baseUrl !== "" ? buildEmbedUrlWithOrigin(baseUrl, { operation, operationKey, storeUrl }) : buildEmbedUrl({ operation, operationKey, storeUrl }),
86
+ [baseUrl, operation, operationKey, storeUrl]
60
87
  );
88
+ const embedOrigin = useMemo(() => new URL(src).origin, [src]);
89
+ useEffect(() => {
90
+ setPasswordRequired(false);
91
+ setPasswordChecked(false);
92
+ setPasswordUnlocked(false);
93
+ setPasswordError(void 0);
94
+ }, [src]);
61
95
  useEffect(() => {
62
- if (typeof window === "undefined") return;
63
96
  const detectMobile = () => {
64
97
  const userAgent = navigator.userAgent || navigator.vendor || window.opera;
65
98
  return /android|iPad|iPhone|iPod/i.test(userAgent);
@@ -97,15 +130,14 @@ var PlayruoEmbed = ({
97
130
  tryNow: {
98
131
  ...currentState.tryNow,
99
132
  available: hasIframeConfirmed,
100
- status: currentState.tryNow.status ?? (hasIframeConfirmed ? "Ready" : isCompatible ? "Checking availability\u2026" : "Browser not compatible")
133
+ // A missing status means the state came from the iframe, which only loads on
134
+ // compatible browsers — the incompatible copy is supplied by currentState.
135
+ status: currentState.tryNow.status ?? (hasIframeConfirmed ? "Ready" : "Checking availability\u2026")
101
136
  }
102
137
  }),
103
- [currentState, hasIframeConfirmed, isCompatible]
138
+ [currentState, hasIframeConfirmed]
104
139
  );
105
140
  const getKeyboardApi = useCallback(() => {
106
- if (typeof navigator === "undefined") {
107
- return void 0;
108
- }
109
141
  return navigator.keyboard;
110
142
  }, []);
111
143
  const releaseEscapeKeyLock = useCallback(() => {
@@ -135,6 +167,23 @@ var PlayruoEmbed = ({
135
167
  },
136
168
  [onDemoCompleted, storeUrl]
137
169
  );
170
+ const postPasswordAction = useCallback(
171
+ (message) => {
172
+ const contentWindow = iframeRef.current?.contentWindow;
173
+ if (!contentWindow) {
174
+ return;
175
+ }
176
+ try {
177
+ contentWindow.postMessage(
178
+ { type: "playruo", fromEmbed: true, ...message },
179
+ embedOrigin
180
+ );
181
+ } catch {
182
+ console.error("[PlayruoEmbed] Failed to send the password action");
183
+ }
184
+ },
185
+ [embedOrigin, iframeRef]
186
+ );
138
187
  const postExitFullscreenToIframe = useCallback(() => {
139
188
  const iframe = iframeRef.current;
140
189
  if (!iframe) {
@@ -147,22 +196,22 @@ var PlayruoEmbed = ({
147
196
  fromEmbed: true,
148
197
  isFullscreen: false,
149
198
  parentWillHandleFullscreen: true
150
- }, "*");
199
+ }, embedOrigin);
151
200
  } catch {
152
201
  console.error("[PlayruoEmbed] Failed to exit fullscreen");
153
202
  }
154
- }, [iframeRef]);
203
+ }, [embedOrigin, iframeRef]);
155
204
  const postExitFullscreenAckToIframe = useCallback(() => {
156
205
  const iframe = iframeRef.current;
157
206
  if (!iframe) {
158
207
  return;
159
208
  }
160
209
  try {
161
- iframe.contentWindow?.postMessage({ type: "ACK_EXIT_FULLSCREEN" }, "*");
210
+ iframe.contentWindow?.postMessage({ type: "ACK_EXIT_FULLSCREEN" }, embedOrigin);
162
211
  } catch {
163
212
  console.error("[PlayruoEmbed] Failed to acknowledge fullscreen exit");
164
213
  }
165
- }, [iframeRef]);
214
+ }, [embedOrigin, iframeRef]);
166
215
  const exitEmbedFullscreen = useCallback(
167
216
  async ({
168
217
  reason = "cleanup",
@@ -248,7 +297,7 @@ var PlayruoEmbed = ({
248
297
  }, []);
249
298
  const forwardEventToParent = useCallback(
250
299
  (message) => {
251
- if (!forwardEventsToParent || typeof window === "undefined" || window.parent === window) {
300
+ if (!forwardEventsToParent || window.parent === window) {
252
301
  return;
253
302
  }
254
303
  try {
@@ -317,7 +366,7 @@ var PlayruoEmbed = ({
317
366
  isFullscreen: isParentFullscreen,
318
367
  parentWillHandleFullscreen: isParentFullscreen,
319
368
  isReplay: hasPlayedOnce
320
- }, "*");
369
+ }, embedOrigin);
321
370
  console.log("[PlayruoEmbed] Sent tryNowClick to game", hasPlayedOnce ? "(replay)" : "(first time)");
322
371
  iframe.focus();
323
372
  requestAnimationFrame(() => {
@@ -364,6 +413,9 @@ var PlayruoEmbed = ({
364
413
  if (!iframe || event.source !== iframe.contentWindow) {
365
414
  return;
366
415
  }
416
+ if (event.origin !== embedOrigin) {
417
+ return;
418
+ }
367
419
  console.log("\u{1F537} [Embed] Message received from iframe:", { type, data });
368
420
  if (type === "playruo_ready") {
369
421
  console.log("\u{1F537} [Embed] playruo_ready received");
@@ -432,6 +484,24 @@ var PlayruoEmbed = ({
432
484
  const eventPayload = message.payload;
433
485
  onEvent?.(eventPayload);
434
486
  forwardEventToParent({ type: "playruo_event", data: eventPayload });
487
+ } else if (type === "playruo_password_required") {
488
+ console.log("\u{1F537} [Embed] playruo_password_required received");
489
+ setPasswordRequired(true);
490
+ setPasswordChecked(true);
491
+ setPasswordUnlocked(false);
492
+ setPasswordError(void 0);
493
+ onPasswordRequired?.();
494
+ } else if (type === "playruo_password_not_required") {
495
+ console.log("\u{1F537} [Embed] playruo_password_not_required received");
496
+ setPasswordRequired(false);
497
+ setPasswordChecked(true);
498
+ } else if (type === "playruo_password_result" && data) {
499
+ const result = data;
500
+ const success = result.success === true;
501
+ console.log("\u{1F537} [Embed] playruo_password_result received:", result);
502
+ setPasswordUnlocked(success);
503
+ setPasswordError(success ? void 0 : result.error ?? "unknown");
504
+ onPasswordResult?.({ success, error: result.error });
435
505
  }
436
506
  if (type === "error" || type === "closeForBuy" || type === "goToStore") {
437
507
  console.log("\u{1F537} [Embed] Legacy message received:", type);
@@ -447,6 +517,7 @@ var PlayruoEmbed = ({
447
517
  return () => window.removeEventListener("message", handleGameMessage);
448
518
  }, [
449
519
  cleanupEmbedSession,
520
+ embedOrigin,
450
521
  exitEmbedFullscreen,
451
522
  forwardEventToParent,
452
523
  hasPlayedOnce,
@@ -454,6 +525,8 @@ var PlayruoEmbed = ({
454
525
  markStreamActive,
455
526
  onEvent,
456
527
  onError,
528
+ onPasswordRequired,
529
+ onPasswordResult,
457
530
  onPurchaseRequest,
458
531
  onReady,
459
532
  onStateChange,
@@ -476,6 +549,9 @@ var PlayruoEmbed = ({
476
549
  if (!iframe || event.source !== iframe.contentWindow) {
477
550
  return;
478
551
  }
552
+ if (event.origin !== embedOrigin) {
553
+ return;
554
+ }
479
555
  const { type, requestId } = event.data || {};
480
556
  if (type === "playruo_uuid_request") {
481
557
  try {
@@ -490,34 +566,27 @@ var PlayruoEmbed = ({
490
566
  requestId,
491
567
  uuid: storedUUID
492
568
  },
493
- "*"
569
+ embedOrigin
494
570
  );
495
571
  } catch (e) {
496
572
  }
497
- } else if (type === "playruo_uuid_remove") {
498
- try {
499
- localStorage.removeItem("playruo_uuid");
500
- } catch (e) {
501
- }
502
573
  }
503
574
  };
504
575
  window.addEventListener("message", handleUUIDMessage);
505
576
  return () => window.removeEventListener("message", handleUUIDMessage);
506
- }, [iframeRef]);
577
+ }, [embedOrigin, iframeRef]);
507
578
  useEffect(() => {
508
579
  const handleFullscreenChange = () => {
509
580
  const iframe = iframeRef.current;
510
581
  const fullscreenElement = getFullscreenElement();
511
582
  const isEmbedFullscreen = Boolean(iframe && fullscreenElement === iframe);
512
- if (isEmbedFullscreen) {
583
+ if (isEmbedFullscreen && iframe) {
513
584
  wasEmbedFullscreenRef.current = true;
514
585
  void lockEscapeKey();
515
- if (iframe) {
516
- setTimeout(() => {
517
- iframe.focus();
518
- iframe.contentWindow?.focus();
519
- }, 200);
520
- }
586
+ setTimeout(() => {
587
+ iframe.focus();
588
+ iframe.contentWindow?.focus();
589
+ }, 200);
521
590
  return;
522
591
  }
523
592
  if (fullscreenElement) {
@@ -562,17 +631,16 @@ var PlayruoEmbed = ({
562
631
  const logCrossOriginStatus = () => {
563
632
  const isIsolated = self.crossOriginIsolated;
564
633
  const hasSharedArrayBuffer = typeof SharedArrayBuffer !== "undefined";
565
- const hasHighResTime = typeof performance !== "undefined" && typeof performance.now === "function";
566
634
  console.group("[PlayruoEmbed] \u{1F512} Cross-Origin Isolation Status");
567
635
  if (isIsolated) {
568
636
  console.log("\u2705 Cross-origin isolation: ENABLED");
569
637
  console.log("\u2705 SharedArrayBuffer support:", hasSharedArrayBuffer ? "Available" : "Not available");
570
- console.log("\u2705 High-resolution timers:", hasHighResTime ? "Available" : "Not available");
638
+ console.log("\u2705 High-resolution timers: Full precision");
571
639
  console.log("\u{1F3AE} Game performance: Optimal");
572
640
  } else {
573
641
  console.warn("\u26A0\uFE0F Cross-origin isolation: DISABLED");
574
642
  console.warn("\u26A0\uFE0F SharedArrayBuffer support:", hasSharedArrayBuffer ? "Available" : "Not available");
575
- console.warn("\u26A0\uFE0F High-resolution timers:", hasHighResTime ? "Available" : "Not available");
643
+ console.warn("\u26A0\uFE0F High-resolution timers: Reduced precision");
576
644
  console.warn("\u{1F3AE} Game performance: May be degraded");
577
645
  console.group("\u{1F4CB} Setup Instructions:");
578
646
  console.log("Add these headers to your server:");
@@ -616,7 +684,7 @@ var PlayruoEmbed = ({
616
684
  console.log("User agent:", navigator.userAgent);
617
685
  console.log("Available APIs:", {
618
686
  SharedArrayBuffer: hasSharedArrayBuffer,
619
- Performance: hasHighResTime,
687
+ Performance: true,
620
688
  WebGL: !!document.createElement("canvas").getContext("webgl"),
621
689
  WebGL2: !!document.createElement("canvas").getContext("webgl2"),
622
690
  WebAssembly: typeof WebAssembly !== "undefined"
@@ -648,22 +716,44 @@ var PlayruoEmbed = ({
648
716
  tryNow: handleTryNow,
649
717
  buyNow: handleBuyNow,
650
718
  canTryNow,
651
- canBuyNow: Boolean(isReady)
719
+ canBuyNow: Boolean(isReady),
720
+ submitPassword: (password2) => {
721
+ if (!password2) {
722
+ return;
723
+ }
724
+ postPasswordAction({ action: "submitPassword", password: password2 });
725
+ },
726
+ requestPassword: () => postPasswordAction({ action: "requestPassword" })
727
+ };
728
+ const password = {
729
+ required: passwordRequired,
730
+ checked: passwordChecked,
731
+ unlocked: passwordUnlocked,
732
+ error: passwordError
652
733
  };
653
734
  if (errorMessage) {
654
- return renderState({ status: "error", message: errorMessage, actions });
735
+ return renderState({ status: "error", message: errorMessage, actions, password });
736
+ }
737
+ if (password.required && !password.unlocked) {
738
+ return renderState({
739
+ status: "loading",
740
+ message: "Password required",
741
+ actions,
742
+ password
743
+ });
655
744
  }
656
745
  if (!isReady) {
657
- return renderState({ status: "loading", message: "Loading\u2026", actions });
746
+ return renderState({ status: "loading", message: "Loading\u2026", actions, password });
658
747
  }
659
748
  if (isCompatible && !iframeState) {
660
- return renderState({ status: "loading", message: "Checking availability\u2026", actions });
749
+ return renderState({ status: "loading", message: "Checking availability\u2026", actions, password });
661
750
  }
662
751
  const message = !isAvailable ? currentState.tryNow.status ?? exposedState.tryNow.status : void 0;
663
752
  return renderState({
664
753
  status: isAvailable ? "available" : "unavailable",
665
754
  message,
666
- actions
755
+ actions,
756
+ password
667
757
  });
668
758
  };
669
759
  return /* @__PURE__ */ jsxs("div", { className, children: [
@@ -890,20 +980,11 @@ var EmbedTester = (props = {}) => {
890
980
  addEvent(message);
891
981
  }, [addEvent]);
892
982
  const handleApply = useCallback2(() => {
893
- if (!hasPendingConfigChanges) {
894
- return;
895
- }
896
983
  setAppliedOperation(operationDraft);
897
984
  setAppliedStoreUrl(storeUrlDraft);
898
985
  setAppliedEnableWindowedLaunch(enableWindowedLaunchDraft);
899
986
  resetEmbedSession("Applied updated embed settings");
900
- }, [
901
- enableWindowedLaunchDraft,
902
- hasPendingConfigChanges,
903
- operationDraft,
904
- resetEmbedSession,
905
- storeUrlDraft
906
- ]);
987
+ }, [enableWindowedLaunchDraft, operationDraft, resetEmbedSession, storeUrlDraft]);
907
988
  const handleRestartSession = useCallback2(() => {
908
989
  resetEmbedSession("Embed restarted with applied settings");
909
990
  }, [resetEmbedSession]);
@@ -2007,12 +2088,12 @@ var isPlayruoEmbedMessage = (data) => {
2007
2088
  if (!data || typeof data !== "object") return false;
2008
2089
  const msg = data;
2009
2090
  if (typeof msg.type !== "string") return false;
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";
2091
+ 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" || msg.type === "playruo_password_required" || msg.type === "playruo_password_not_required" || msg.type === "playruo_password_result";
2011
2092
  };
2012
2093
  var isPlayruoInternalMessage = (data) => {
2013
2094
  if (!data || typeof data !== "object") return false;
2014
2095
  const msg = data;
2015
- return msg.type === "playruo" && typeof msg.action === "string" && (msg.action === "tryNowClick" || msg.action === "exitFullscreen" || msg.action === "mute" || msg.action === "unmute") && msg.fromEmbed === true;
2096
+ return msg.type === "playruo" && typeof msg.action === "string" && (msg.action === "tryNowClick" || msg.action === "exitFullscreen" || msg.action === "mute" || msg.action === "unmute" || msg.action === "submitPassword" || msg.action === "requestPassword") && msg.fromEmbed === true;
2016
2097
  };
2017
2098
  export {
2018
2099
  EmbedTester,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playruo/embed",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "React SDK to embed Playruo with postMessage API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,27 +9,50 @@
9
9
  "types": "dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "types": "./dist/index.d.ts",
13
- "import": "./dist/index.js",
14
- "require": "./dist/index.cjs",
15
- "default": "./dist/index.js"
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
16
20
  }
17
21
  },
18
- "files": ["dist"],
22
+ "files": [
23
+ "dist"
24
+ ],
19
25
  "sideEffects": false,
20
26
  "peerDependencies": {
21
27
  "react": ">=18",
22
28
  "react-dom": ">=18"
23
29
  },
24
30
  "devDependencies": {
31
+ "@arethetypeswrong/cli": "^0.18.5",
32
+ "@testing-library/jest-dom": "^6.6.3",
33
+ "@testing-library/react": "^16.3.0",
34
+ "@testing-library/user-event": "^14.6.1",
35
+ "@types/node": "^26.2.0",
36
+ "@types/react": "^19",
37
+ "@types/react-dom": "^19",
38
+ "@vitest/browser": "^3.2.7",
39
+ "@vitest/coverage-v8": "^3.2.4",
40
+ "jsdom": "^26.1.0",
41
+ "playwright": "^1.62.1",
42
+ "react": "^19.1.0",
43
+ "react-dom": "^19.1.0",
25
44
  "tsup": "^8.0.0",
26
45
  "typescript": "^5.4.0",
27
- "@types/react": "^19",
28
- "@types/react-dom": "^19"
46
+ "vitest": "^3.2.4"
29
47
  },
30
48
  "scripts": {
31
49
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",
32
50
  "dev": "tsup src/index.ts --format esm --dts --watch",
33
- "prepare": "npm run build"
51
+ "prepare": "npm run build",
52
+ "typecheck": "tsc -p tsconfig.test.json",
53
+ "check:exports": "attw --pack .",
54
+ "test": "npm run build && vitest run --coverage",
55
+ "test:browser": "vitest run --config vitest.browser.config.ts",
56
+ "test:watch": "npm run build && vitest"
34
57
  }
35
58
  }