@playruo/embed 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -211,6 +211,14 @@ var PlayruoEmbed = ({
211
211
  }, []);
212
212
  const isCompatible = !isMobile && isValidChromium;
213
213
  const isReady = isCompatible ? isIframeLoaded : true;
214
+ (0, import_react.useEffect)(() => {
215
+ const iframe = iframeRef.current;
216
+ if (!iframe || iframe.getAttribute("src") === src) {
217
+ return;
218
+ }
219
+ iframe.setAttribute("credentialless", "");
220
+ iframe.src = src;
221
+ }, [iframeRef, isCompatible, src]);
214
222
  const currentState = (0, import_react.useMemo)(() => {
215
223
  if (iframeState) {
216
224
  return iframeState;
@@ -283,7 +291,7 @@ var PlayruoEmbed = ({
283
291
  embedOrigin
284
292
  );
285
293
  } catch {
286
- console.error("[PlayruoEmbed] Failed to send the password action");
294
+ console.error("[PlayruoEmbed] Failed to send iframe action");
287
295
  }
288
296
  },
289
297
  [embedOrigin, iframeRef]
@@ -806,7 +814,9 @@ var PlayruoEmbed = ({
806
814
  tryNow: handleTryNow,
807
815
  buyNow: handleBuyNow,
808
816
  canTryNow,
809
- canBuyNow: Boolean(isReady),
817
+ canBuyNow: Boolean(isReady && storeUrl),
818
+ mute: () => postPasswordAction({ action: "mute" }),
819
+ unmute: () => postPasswordAction({ action: "unmute" }),
810
820
  submitPassword: (password2) => {
811
821
  if (!password2) {
812
822
  return;
@@ -872,10 +882,8 @@ var PlayruoEmbed = ({
872
882
  "iframe",
873
883
  {
874
884
  ref: iframeRef,
875
- src,
876
885
  allow: PLAYRUO_IFRAME_ALLOW,
877
886
  className: "w-full h-[70vh] rounded-lg border",
878
- ...{ credentialless: "true" },
879
887
  sandbox: "allow-same-origin allow-scripts allow-popups allow-forms allow-presentation allow-top-navigation allow-modals allow-pointer-lock",
880
888
  title: "Playruo",
881
889
  referrerPolicy: "strict-origin-when-cross-origin",
@@ -887,8 +895,10 @@ var PlayruoEmbed = ({
887
895
  height: "100%",
888
896
  border: 0
889
897
  },
890
- onLoad: () => {
891
- setIsIframeLoaded(true);
898
+ onLoad: (event) => {
899
+ if (event.currentTarget.getAttribute("src")) {
900
+ setIsIframeLoaded(true);
901
+ }
892
902
  },
893
903
  onError: () => {
894
904
  console.error("[PlayruoEmbed] Iframe failed to load");
@@ -929,6 +939,7 @@ var createInitialDiagnostics = () => ({
929
939
  });
930
940
  var EmbedTester = (props = {}) => {
931
941
  const [operationDraft, setOperationDraft] = (0, import_react2.useState)(props.operation ?? "iframe");
942
+ const [operationKeyDraft, setOperationKeyDraft] = (0, import_react2.useState)(props.operationKey ?? "");
932
943
  const [storeUrlDraft, setStoreUrlDraft] = (0, import_react2.useState)(
933
944
  props.storeUrl ?? "https://store.steampowered.com/app/123456"
934
945
  );
@@ -936,6 +947,7 @@ var EmbedTester = (props = {}) => {
936
947
  props.enableWindowedLaunch ?? false
937
948
  );
938
949
  const [appliedOperation, setAppliedOperation] = (0, import_react2.useState)(props.operation ?? "iframe");
950
+ const [appliedOperationKey, setAppliedOperationKey] = (0, import_react2.useState)(props.operationKey ?? "");
939
951
  const [appliedStoreUrl, setAppliedStoreUrl] = (0, import_react2.useState)(
940
952
  props.storeUrl ?? "https://store.steampowered.com/app/123456"
941
953
  );
@@ -945,6 +957,7 @@ var EmbedTester = (props = {}) => {
945
957
  const [key, setKey] = (0, import_react2.useState)(0);
946
958
  const [hasUserClicked, setHasUserClicked] = (0, import_react2.useState)(false);
947
959
  const [showIframe, setShowIframe] = (0, import_react2.useState)(false);
960
+ const [overlayPassword, setOverlayPassword] = (0, import_react2.useState)("");
948
961
  const [embedState, setEmbedState] = (0, import_react2.useState)(createInitialEmbedState);
949
962
  const [eventLog, setEventLog] = (0, import_react2.useState)([]);
950
963
  const [diagnostics, setDiagnostics] = (0, import_react2.useState)(createInitialDiagnostics);
@@ -955,10 +968,11 @@ var EmbedTester = (props = {}) => {
955
968
  const previousStateRef = (0, import_react2.useRef)("");
956
969
  const iframeRef = (0, import_react2.useRef)(null);
957
970
  const isOperationPending = operationDraft !== appliedOperation;
971
+ const isOperationKeyPending = operationKeyDraft !== appliedOperationKey;
958
972
  const isStoreUrlPending = storeUrlDraft !== appliedStoreUrl;
959
973
  const isWindowedLaunchPending = enableWindowedLaunchDraft !== appliedEnableWindowedLaunch;
960
- const hasPendingConfigChanges = isOperationPending || isStoreUrlPending || isWindowedLaunchPending;
961
- const pendingChangeCount = Number(isOperationPending) + Number(isStoreUrlPending) + Number(isWindowedLaunchPending);
974
+ const hasPendingConfigChanges = isOperationPending || isOperationKeyPending || isStoreUrlPending || isWindowedLaunchPending;
975
+ const pendingChangeCount = Number(isOperationPending) + Number(isOperationKeyPending) + Number(isStoreUrlPending) + Number(isWindowedLaunchPending);
962
976
  const addEvent = (0, import_react2.useCallback)(
963
977
  (message) => {
964
978
  const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
@@ -1043,7 +1057,7 @@ var EmbedTester = (props = {}) => {
1043
1057
  }
1044
1058
  }, [addEvent, enableConsoleLog]);
1045
1059
  const handleEvent = (0, import_react2.useCallback)((data) => {
1046
- const eventName = typeof data.event === "string" ? data.event : typeof data.name === "string" ? data.name : "unknown";
1060
+ const eventName = typeof data.name === "string" ? data.name : typeof data.event === "string" ? data.event : "unknown";
1047
1061
  addEvent(`Event received: ${eventName}`);
1048
1062
  if (enableConsoleLog) {
1049
1063
  console.log("Embed event:", data);
@@ -1059,25 +1073,40 @@ var EmbedTester = (props = {}) => {
1059
1073
  setKey((prev) => prev + 1);
1060
1074
  setHasUserClicked(false);
1061
1075
  setShowIframe(false);
1076
+ setOverlayPassword("");
1062
1077
  hasReadyBeenCalled.current = false;
1063
1078
  previousStateRef.current = "";
1064
1079
  addEvent(message);
1065
1080
  }, [addEvent]);
1066
1081
  const handleApply = (0, import_react2.useCallback)(() => {
1067
1082
  setAppliedOperation(operationDraft);
1083
+ setAppliedOperationKey(operationKeyDraft);
1068
1084
  setAppliedStoreUrl(storeUrlDraft);
1069
1085
  setAppliedEnableWindowedLaunch(enableWindowedLaunchDraft);
1070
1086
  resetEmbedSession("Applied updated embed settings");
1071
- }, [enableWindowedLaunchDraft, operationDraft, resetEmbedSession, storeUrlDraft]);
1087
+ }, [
1088
+ enableWindowedLaunchDraft,
1089
+ operationDraft,
1090
+ operationKeyDraft,
1091
+ resetEmbedSession,
1092
+ storeUrlDraft
1093
+ ]);
1072
1094
  const handleRestartSession = (0, import_react2.useCallback)(() => {
1073
1095
  resetEmbedSession("Embed restarted with applied settings");
1074
1096
  }, [resetEmbedSession]);
1075
1097
  const handleResetDrafts = (0, import_react2.useCallback)(() => {
1076
1098
  setOperationDraft(appliedOperation);
1099
+ setOperationKeyDraft(appliedOperationKey);
1077
1100
  setStoreUrlDraft(appliedStoreUrl);
1078
1101
  setEnableWindowedLaunchDraft(appliedEnableWindowedLaunch);
1079
1102
  addEvent("Reverted draft changes to the applied configuration");
1080
- }, [addEvent, appliedEnableWindowedLaunch, appliedOperation, appliedStoreUrl]);
1103
+ }, [
1104
+ addEvent,
1105
+ appliedEnableWindowedLaunch,
1106
+ appliedOperation,
1107
+ appliedOperationKey,
1108
+ appliedStoreUrl
1109
+ ]);
1081
1110
  const panelStyle = {
1082
1111
  position: "relative",
1083
1112
  border: "1px solid rgba(148, 163, 184, 0.18)",
@@ -1609,6 +1638,46 @@ var EmbedTester = (props = {}) => {
1609
1638
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { style: codeStyle, children: appliedOperation })
1610
1639
  ] })
1611
1640
  ] }),
1641
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
1642
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { htmlFor: "operationKey", style: labelStyle, children: [
1643
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Op key" }),
1644
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: buildIndicatorStyle(isOperationKeyPending), children: [
1645
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: buildIndicatorDotStyle(isOperationKeyPending) }),
1646
+ isOperationKeyPending ? "Pending" : "Live"
1647
+ ] })
1648
+ ] }),
1649
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: buildInputShellStyle(isOperationKeyPending), children: [
1650
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1651
+ "input",
1652
+ {
1653
+ id: "operationKey",
1654
+ style: inputStyle,
1655
+ onChange: (e) => setOperationKeyDraft(e.target.value),
1656
+ placeholder: "e.g. embed-password",
1657
+ value: operationKeyDraft
1658
+ }
1659
+ ),
1660
+ isOperationKeyPending && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1661
+ "span",
1662
+ {
1663
+ "aria-hidden": "true",
1664
+ style: {
1665
+ fontSize: "16px",
1666
+ color: "#fbbf24",
1667
+ fontWeight: 700
1668
+ },
1669
+ children: "!"
1670
+ }
1671
+ )
1672
+ ] }),
1673
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { style: helperTextStyle, children: [
1674
+ "Optional ",
1675
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { style: codeStyle, children: "?op=" }),
1676
+ " when the path is the campaign. Applied:",
1677
+ " ",
1678
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { style: codeStyle, children: appliedOperationKey || "(none)" })
1679
+ ] })
1680
+ ] }),
1612
1681
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
1613
1682
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { htmlFor: "storeUrl", style: labelStyle, children: [
1614
1683
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Store URL" }),
@@ -2049,7 +2118,8 @@ var EmbedTester = (props = {}) => {
2049
2118
  onStateChange: handleStateChange,
2050
2119
  enableWindowedLaunch: appliedEnableWindowedLaunch,
2051
2120
  operation: appliedOperation,
2052
- renderState: ({ actions, message, status }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: !hasUserClicked && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2121
+ operationKey: appliedOperationKey || void 0,
2122
+ renderState: ({ actions, message, status, password }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: !hasUserClicked && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2053
2123
  "div",
2054
2124
  {
2055
2125
  style: {
@@ -2104,48 +2174,117 @@ var EmbedTester = (props = {}) => {
2104
2174
  fontSize: "28px",
2105
2175
  letterSpacing: "-0.03em"
2106
2176
  },
2107
- children: embedState.hasPlayedOnce ? "Resume the session" : "Start the demo"
2177
+ children: password?.required && !password.unlocked ? "Password required" : embedState.hasPlayedOnce ? "Resume the session" : "Start the demo"
2108
2178
  }
2109
2179
  ),
2110
2180
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { style: { margin: 0, color: "#94a3b8", lineHeight: 1.7 }, children: [
2111
- "Status: ",
2181
+ "Status:",
2182
+ " ",
2112
2183
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: "#f8fafc", fontWeight: 700 }, children: status })
2113
2184
  ] }),
2114
- message && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { margin: "10px 0 0 0", color: "#cbd5e1", lineHeight: 1.7 }, children: message }),
2115
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: "12px", marginTop: "24px" }, children: [
2116
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2117
- "button",
2118
- {
2119
- type: "button",
2120
- style: buildButtonStyle("primary", !actions.canTryNow),
2121
- disabled: !actions.canTryNow,
2122
- onClick: () => {
2123
- setHasUserClicked(true);
2124
- setShowIframe(true);
2125
- addEvent(
2126
- embedState.hasPlayedOnce ? "User clicked - Resuming game" : "User clicked - Starting game with tryNow"
2127
- );
2128
- actions.tryNow();
2129
- },
2130
- children: [
2131
- "\u{1F3AE} ",
2132
- embedState.hasPlayedOnce ? "Resume game" : "Try demo now"
2133
- ]
2134
- }
2135
- ),
2136
- actions.canBuyNow && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2137
- "button",
2138
- {
2139
- type: "button",
2140
- style: buildButtonStyle("secondary"),
2141
- onClick: () => {
2142
- addEvent("Buy Now clicked");
2143
- actions.buyNow();
2144
- },
2145
- children: "Buy Now"
2146
- }
2147
- )
2148
- ] })
2185
+ message && !(password?.required && !password.unlocked) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { margin: "10px 0 0 0", color: "#cbd5e1", lineHeight: 1.7 }, children: message }),
2186
+ password?.required && !password.unlocked ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2187
+ "form",
2188
+ {
2189
+ style: {
2190
+ display: "flex",
2191
+ flexWrap: "wrap",
2192
+ gap: "12px",
2193
+ marginTop: "24px",
2194
+ alignItems: "center"
2195
+ },
2196
+ onSubmit: (event) => {
2197
+ event.preventDefault();
2198
+ if (!overlayPassword) {
2199
+ return;
2200
+ }
2201
+ actions.submitPassword(overlayPassword);
2202
+ setOverlayPassword("");
2203
+ },
2204
+ children: [
2205
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2206
+ "input",
2207
+ {
2208
+ "aria-label": "Operation password",
2209
+ autoComplete: "off",
2210
+ onChange: (event) => setOverlayPassword(event.target.value),
2211
+ placeholder: "Operation password",
2212
+ style: {
2213
+ flex: "1 1 180px",
2214
+ minWidth: "140px",
2215
+ padding: "12px 14px",
2216
+ borderRadius: "12px",
2217
+ border: "1px solid rgba(148, 163, 184, 0.28)",
2218
+ backgroundColor: "rgba(15, 23, 42, 0.9)",
2219
+ color: "#f8fafc"
2220
+ },
2221
+ type: "password",
2222
+ value: overlayPassword
2223
+ }
2224
+ ),
2225
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2226
+ "button",
2227
+ {
2228
+ type: "submit",
2229
+ style: buildButtonStyle("primary", !overlayPassword),
2230
+ disabled: !overlayPassword,
2231
+ children: "Unlock"
2232
+ }
2233
+ ),
2234
+ password.error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2235
+ "p",
2236
+ {
2237
+ style: {
2238
+ flex: "1 1 100%",
2239
+ margin: 0,
2240
+ color: "#fca5a5",
2241
+ fontSize: "13px"
2242
+ },
2243
+ children: password.error
2244
+ }
2245
+ ) : null
2246
+ ]
2247
+ }
2248
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2249
+ "div",
2250
+ {
2251
+ style: { display: "flex", flexWrap: "wrap", gap: "12px", marginTop: "24px" },
2252
+ children: [
2253
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2254
+ "button",
2255
+ {
2256
+ type: "button",
2257
+ style: buildButtonStyle("primary", !actions.canTryNow),
2258
+ disabled: !actions.canTryNow,
2259
+ onClick: () => {
2260
+ setHasUserClicked(true);
2261
+ setShowIframe(true);
2262
+ addEvent(
2263
+ embedState.hasPlayedOnce ? "User clicked - Resuming game" : "User clicked - Starting game with tryNow"
2264
+ );
2265
+ actions.tryNow();
2266
+ },
2267
+ children: [
2268
+ "\u{1F3AE} ",
2269
+ embedState.hasPlayedOnce ? "Resume game" : "Try demo now"
2270
+ ]
2271
+ }
2272
+ ),
2273
+ actions.canBuyNow && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2274
+ "button",
2275
+ {
2276
+ type: "button",
2277
+ style: buildButtonStyle("secondary"),
2278
+ onClick: () => {
2279
+ addEvent("Buy Now clicked");
2280
+ actions.buyNow();
2281
+ },
2282
+ children: "Buy Now"
2283
+ }
2284
+ )
2285
+ ]
2286
+ }
2287
+ )
2149
2288
  ]
2150
2289
  }
2151
2290
  )
package/dist/index.d.cts CHANGED
@@ -1,5 +1,4 @@
1
- import { FC, ReactNode } from 'react';
2
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import React$1, { FC, ReactNode } from 'react';
3
2
 
4
3
  /**
5
4
  * iframe `allow` feature policy delegated to the Playruo player.
@@ -28,7 +27,13 @@ interface DemoCompletedPayload {
28
27
  type PlayruoEventPayload = Record<string, unknown> & {
29
28
  action?: string;
30
29
  category?: string;
30
+ /**
31
+ * @deprecated The webapp puts the event identifier on `name`. Nothing it
32
+ * posts carries `event`, so a host switching on it never matches. Kept so
33
+ * older hosts still compile.
34
+ */
31
35
  event?: string;
36
+ /** Event identifier, snake_case (`auth_ok`, `streaming`, `vmnok`). */
32
37
  name?: string;
33
38
  };
34
39
 
@@ -162,8 +167,16 @@ interface PlayruoEmbedActions {
162
167
  buyNow: () => void;
163
168
  /** Whether try now is currently available */
164
169
  canTryNow: boolean;
165
- /** Whether buy now is currently available */
170
+ /**
171
+ * Whether buy now can call `onPurchaseRequest`. True only when the SDK is
172
+ * ready and a `storeUrl` prop is set. Compatible with mobile/unavailable
173
+ * hosts that still want a store CTA.
174
+ */
166
175
  canBuyNow: boolean;
176
+ /** Mute active stream audio. No acknowledgement is returned. */
177
+ mute: () => void;
178
+ /** Resume and unmute stream audio. No acknowledgement is returned. */
179
+ unmute: () => void;
167
180
  /** Send the password collected by the host to a protected operation. */
168
181
  submitPassword: (password: string) => void;
169
182
  /**
@@ -263,12 +276,14 @@ interface EmbedTesterProps {
263
276
  baseUrl?: string;
264
277
  enableConsoleLog?: boolean;
265
278
  operation?: string;
279
+ /** Optional ?op= key when the campaign path is not the operation itself. */
280
+ operationKey?: string;
266
281
  storeUrl?: string;
267
282
  /** Start the experience inside the iframe without requesting parent fullscreen. */
268
283
  enableWindowedLaunch?: boolean;
269
284
  className?: string;
270
285
  }
271
- declare const EmbedTester: (props?: EmbedTesterProps) => react_jsx_runtime.JSX.Element;
286
+ declare const EmbedTester: (props?: EmbedTesterProps) => React$1.JSX.Element;
272
287
 
273
288
  interface BuildUrlParams {
274
289
  operation: string;
@@ -291,8 +306,8 @@ declare const buildEmbedUrl: ({ operation, operationKey, storeUrl, }: BuildUrlPa
291
306
  declare const PLAYRUO_UUID_STORAGE_KEY = "playruo_uuid";
292
307
  /**
293
308
  * Visitor identity the host answers on `playruo_uuid_request`.
294
- * Same rule as PlayruoEmbed `clientFingerprint`: a non-empty value is used as-is
295
- * and is not written to localStorage. Otherwise the SDK-managed UUID is used.
309
+ * Same rule as PlayruoEmbed `clientFingerprint`: a non-empty trimmed value is
310
+ * used and is not written to localStorage. Otherwise the SDK-managed UUID is used.
296
311
  */
297
312
  declare const resolveVisitorUuid: (clientFingerprint?: string) => string;
298
313
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { FC, ReactNode } from 'react';
2
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import React$1, { FC, ReactNode } from 'react';
3
2
 
4
3
  /**
5
4
  * iframe `allow` feature policy delegated to the Playruo player.
@@ -28,7 +27,13 @@ interface DemoCompletedPayload {
28
27
  type PlayruoEventPayload = Record<string, unknown> & {
29
28
  action?: string;
30
29
  category?: string;
30
+ /**
31
+ * @deprecated The webapp puts the event identifier on `name`. Nothing it
32
+ * posts carries `event`, so a host switching on it never matches. Kept so
33
+ * older hosts still compile.
34
+ */
31
35
  event?: string;
36
+ /** Event identifier, snake_case (`auth_ok`, `streaming`, `vmnok`). */
32
37
  name?: string;
33
38
  };
34
39
 
@@ -162,8 +167,16 @@ interface PlayruoEmbedActions {
162
167
  buyNow: () => void;
163
168
  /** Whether try now is currently available */
164
169
  canTryNow: boolean;
165
- /** Whether buy now is currently available */
170
+ /**
171
+ * Whether buy now can call `onPurchaseRequest`. True only when the SDK is
172
+ * ready and a `storeUrl` prop is set. Compatible with mobile/unavailable
173
+ * hosts that still want a store CTA.
174
+ */
166
175
  canBuyNow: boolean;
176
+ /** Mute active stream audio. No acknowledgement is returned. */
177
+ mute: () => void;
178
+ /** Resume and unmute stream audio. No acknowledgement is returned. */
179
+ unmute: () => void;
167
180
  /** Send the password collected by the host to a protected operation. */
168
181
  submitPassword: (password: string) => void;
169
182
  /**
@@ -263,12 +276,14 @@ interface EmbedTesterProps {
263
276
  baseUrl?: string;
264
277
  enableConsoleLog?: boolean;
265
278
  operation?: string;
279
+ /** Optional ?op= key when the campaign path is not the operation itself. */
280
+ operationKey?: string;
266
281
  storeUrl?: string;
267
282
  /** Start the experience inside the iframe without requesting parent fullscreen. */
268
283
  enableWindowedLaunch?: boolean;
269
284
  className?: string;
270
285
  }
271
- declare const EmbedTester: (props?: EmbedTesterProps) => react_jsx_runtime.JSX.Element;
286
+ declare const EmbedTester: (props?: EmbedTesterProps) => React$1.JSX.Element;
272
287
 
273
288
  interface BuildUrlParams {
274
289
  operation: string;
@@ -291,8 +306,8 @@ declare const buildEmbedUrl: ({ operation, operationKey, storeUrl, }: BuildUrlPa
291
306
  declare const PLAYRUO_UUID_STORAGE_KEY = "playruo_uuid";
292
307
  /**
293
308
  * Visitor identity the host answers on `playruo_uuid_request`.
294
- * Same rule as PlayruoEmbed `clientFingerprint`: a non-empty value is used as-is
295
- * and is not written to localStorage. Otherwise the SDK-managed UUID is used.
309
+ * Same rule as PlayruoEmbed `clientFingerprint`: a non-empty trimmed value is
310
+ * used and is not written to localStorage. Otherwise the SDK-managed UUID is used.
296
311
  */
297
312
  declare const resolveVisitorUuid: (clientFingerprint?: string) => string;
298
313
 
package/dist/index.js CHANGED
@@ -167,6 +167,14 @@ var PlayruoEmbed = ({
167
167
  }, []);
168
168
  const isCompatible = !isMobile && isValidChromium;
169
169
  const isReady = isCompatible ? isIframeLoaded : true;
170
+ useEffect(() => {
171
+ const iframe = iframeRef.current;
172
+ if (!iframe || iframe.getAttribute("src") === src) {
173
+ return;
174
+ }
175
+ iframe.setAttribute("credentialless", "");
176
+ iframe.src = src;
177
+ }, [iframeRef, isCompatible, src]);
170
178
  const currentState = useMemo(() => {
171
179
  if (iframeState) {
172
180
  return iframeState;
@@ -239,7 +247,7 @@ var PlayruoEmbed = ({
239
247
  embedOrigin
240
248
  );
241
249
  } catch {
242
- console.error("[PlayruoEmbed] Failed to send the password action");
250
+ console.error("[PlayruoEmbed] Failed to send iframe action");
243
251
  }
244
252
  },
245
253
  [embedOrigin, iframeRef]
@@ -762,7 +770,9 @@ var PlayruoEmbed = ({
762
770
  tryNow: handleTryNow,
763
771
  buyNow: handleBuyNow,
764
772
  canTryNow,
765
- canBuyNow: Boolean(isReady),
773
+ canBuyNow: Boolean(isReady && storeUrl),
774
+ mute: () => postPasswordAction({ action: "mute" }),
775
+ unmute: () => postPasswordAction({ action: "unmute" }),
766
776
  submitPassword: (password2) => {
767
777
  if (!password2) {
768
778
  return;
@@ -828,10 +838,8 @@ var PlayruoEmbed = ({
828
838
  "iframe",
829
839
  {
830
840
  ref: iframeRef,
831
- src,
832
841
  allow: PLAYRUO_IFRAME_ALLOW,
833
842
  className: "w-full h-[70vh] rounded-lg border",
834
- ...{ credentialless: "true" },
835
843
  sandbox: "allow-same-origin allow-scripts allow-popups allow-forms allow-presentation allow-top-navigation allow-modals allow-pointer-lock",
836
844
  title: "Playruo",
837
845
  referrerPolicy: "strict-origin-when-cross-origin",
@@ -843,8 +851,10 @@ var PlayruoEmbed = ({
843
851
  height: "100%",
844
852
  border: 0
845
853
  },
846
- onLoad: () => {
847
- setIsIframeLoaded(true);
854
+ onLoad: (event) => {
855
+ if (event.currentTarget.getAttribute("src")) {
856
+ setIsIframeLoaded(true);
857
+ }
848
858
  },
849
859
  onError: () => {
850
860
  console.error("[PlayruoEmbed] Iframe failed to load");
@@ -885,6 +895,7 @@ var createInitialDiagnostics = () => ({
885
895
  });
886
896
  var EmbedTester = (props = {}) => {
887
897
  const [operationDraft, setOperationDraft] = useState2(props.operation ?? "iframe");
898
+ const [operationKeyDraft, setOperationKeyDraft] = useState2(props.operationKey ?? "");
888
899
  const [storeUrlDraft, setStoreUrlDraft] = useState2(
889
900
  props.storeUrl ?? "https://store.steampowered.com/app/123456"
890
901
  );
@@ -892,6 +903,7 @@ var EmbedTester = (props = {}) => {
892
903
  props.enableWindowedLaunch ?? false
893
904
  );
894
905
  const [appliedOperation, setAppliedOperation] = useState2(props.operation ?? "iframe");
906
+ const [appliedOperationKey, setAppliedOperationKey] = useState2(props.operationKey ?? "");
895
907
  const [appliedStoreUrl, setAppliedStoreUrl] = useState2(
896
908
  props.storeUrl ?? "https://store.steampowered.com/app/123456"
897
909
  );
@@ -901,6 +913,7 @@ var EmbedTester = (props = {}) => {
901
913
  const [key, setKey] = useState2(0);
902
914
  const [hasUserClicked, setHasUserClicked] = useState2(false);
903
915
  const [showIframe, setShowIframe] = useState2(false);
916
+ const [overlayPassword, setOverlayPassword] = useState2("");
904
917
  const [embedState, setEmbedState] = useState2(createInitialEmbedState);
905
918
  const [eventLog, setEventLog] = useState2([]);
906
919
  const [diagnostics, setDiagnostics] = useState2(createInitialDiagnostics);
@@ -911,10 +924,11 @@ var EmbedTester = (props = {}) => {
911
924
  const previousStateRef = useRef2("");
912
925
  const iframeRef = useRef2(null);
913
926
  const isOperationPending = operationDraft !== appliedOperation;
927
+ const isOperationKeyPending = operationKeyDraft !== appliedOperationKey;
914
928
  const isStoreUrlPending = storeUrlDraft !== appliedStoreUrl;
915
929
  const isWindowedLaunchPending = enableWindowedLaunchDraft !== appliedEnableWindowedLaunch;
916
- const hasPendingConfigChanges = isOperationPending || isStoreUrlPending || isWindowedLaunchPending;
917
- const pendingChangeCount = Number(isOperationPending) + Number(isStoreUrlPending) + Number(isWindowedLaunchPending);
930
+ const hasPendingConfigChanges = isOperationPending || isOperationKeyPending || isStoreUrlPending || isWindowedLaunchPending;
931
+ const pendingChangeCount = Number(isOperationPending) + Number(isOperationKeyPending) + Number(isStoreUrlPending) + Number(isWindowedLaunchPending);
918
932
  const addEvent = useCallback2(
919
933
  (message) => {
920
934
  const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
@@ -999,7 +1013,7 @@ var EmbedTester = (props = {}) => {
999
1013
  }
1000
1014
  }, [addEvent, enableConsoleLog]);
1001
1015
  const handleEvent = useCallback2((data) => {
1002
- const eventName = typeof data.event === "string" ? data.event : typeof data.name === "string" ? data.name : "unknown";
1016
+ const eventName = typeof data.name === "string" ? data.name : typeof data.event === "string" ? data.event : "unknown";
1003
1017
  addEvent(`Event received: ${eventName}`);
1004
1018
  if (enableConsoleLog) {
1005
1019
  console.log("Embed event:", data);
@@ -1015,25 +1029,40 @@ var EmbedTester = (props = {}) => {
1015
1029
  setKey((prev) => prev + 1);
1016
1030
  setHasUserClicked(false);
1017
1031
  setShowIframe(false);
1032
+ setOverlayPassword("");
1018
1033
  hasReadyBeenCalled.current = false;
1019
1034
  previousStateRef.current = "";
1020
1035
  addEvent(message);
1021
1036
  }, [addEvent]);
1022
1037
  const handleApply = useCallback2(() => {
1023
1038
  setAppliedOperation(operationDraft);
1039
+ setAppliedOperationKey(operationKeyDraft);
1024
1040
  setAppliedStoreUrl(storeUrlDraft);
1025
1041
  setAppliedEnableWindowedLaunch(enableWindowedLaunchDraft);
1026
1042
  resetEmbedSession("Applied updated embed settings");
1027
- }, [enableWindowedLaunchDraft, operationDraft, resetEmbedSession, storeUrlDraft]);
1043
+ }, [
1044
+ enableWindowedLaunchDraft,
1045
+ operationDraft,
1046
+ operationKeyDraft,
1047
+ resetEmbedSession,
1048
+ storeUrlDraft
1049
+ ]);
1028
1050
  const handleRestartSession = useCallback2(() => {
1029
1051
  resetEmbedSession("Embed restarted with applied settings");
1030
1052
  }, [resetEmbedSession]);
1031
1053
  const handleResetDrafts = useCallback2(() => {
1032
1054
  setOperationDraft(appliedOperation);
1055
+ setOperationKeyDraft(appliedOperationKey);
1033
1056
  setStoreUrlDraft(appliedStoreUrl);
1034
1057
  setEnableWindowedLaunchDraft(appliedEnableWindowedLaunch);
1035
1058
  addEvent("Reverted draft changes to the applied configuration");
1036
- }, [addEvent, appliedEnableWindowedLaunch, appliedOperation, appliedStoreUrl]);
1059
+ }, [
1060
+ addEvent,
1061
+ appliedEnableWindowedLaunch,
1062
+ appliedOperation,
1063
+ appliedOperationKey,
1064
+ appliedStoreUrl
1065
+ ]);
1037
1066
  const panelStyle = {
1038
1067
  position: "relative",
1039
1068
  border: "1px solid rgba(148, 163, 184, 0.18)",
@@ -1565,6 +1594,46 @@ var EmbedTester = (props = {}) => {
1565
1594
  /* @__PURE__ */ jsx2("code", { style: codeStyle, children: appliedOperation })
1566
1595
  ] })
1567
1596
  ] }),
1597
+ /* @__PURE__ */ jsxs2("div", { children: [
1598
+ /* @__PURE__ */ jsxs2("label", { htmlFor: "operationKey", style: labelStyle, children: [
1599
+ /* @__PURE__ */ jsx2("span", { children: "Op key" }),
1600
+ /* @__PURE__ */ jsxs2("span", { style: buildIndicatorStyle(isOperationKeyPending), children: [
1601
+ /* @__PURE__ */ jsx2("span", { style: buildIndicatorDotStyle(isOperationKeyPending) }),
1602
+ isOperationKeyPending ? "Pending" : "Live"
1603
+ ] })
1604
+ ] }),
1605
+ /* @__PURE__ */ jsxs2("div", { style: buildInputShellStyle(isOperationKeyPending), children: [
1606
+ /* @__PURE__ */ jsx2(
1607
+ "input",
1608
+ {
1609
+ id: "operationKey",
1610
+ style: inputStyle,
1611
+ onChange: (e) => setOperationKeyDraft(e.target.value),
1612
+ placeholder: "e.g. embed-password",
1613
+ value: operationKeyDraft
1614
+ }
1615
+ ),
1616
+ isOperationKeyPending && /* @__PURE__ */ jsx2(
1617
+ "span",
1618
+ {
1619
+ "aria-hidden": "true",
1620
+ style: {
1621
+ fontSize: "16px",
1622
+ color: "#fbbf24",
1623
+ fontWeight: 700
1624
+ },
1625
+ children: "!"
1626
+ }
1627
+ )
1628
+ ] }),
1629
+ /* @__PURE__ */ jsxs2("p", { style: helperTextStyle, children: [
1630
+ "Optional ",
1631
+ /* @__PURE__ */ jsx2("code", { style: codeStyle, children: "?op=" }),
1632
+ " when the path is the campaign. Applied:",
1633
+ " ",
1634
+ /* @__PURE__ */ jsx2("code", { style: codeStyle, children: appliedOperationKey || "(none)" })
1635
+ ] })
1636
+ ] }),
1568
1637
  /* @__PURE__ */ jsxs2("div", { children: [
1569
1638
  /* @__PURE__ */ jsxs2("label", { htmlFor: "storeUrl", style: labelStyle, children: [
1570
1639
  /* @__PURE__ */ jsx2("span", { children: "Store URL" }),
@@ -2005,7 +2074,8 @@ var EmbedTester = (props = {}) => {
2005
2074
  onStateChange: handleStateChange,
2006
2075
  enableWindowedLaunch: appliedEnableWindowedLaunch,
2007
2076
  operation: appliedOperation,
2008
- renderState: ({ actions, message, status }) => /* @__PURE__ */ jsx2(Fragment, { children: !hasUserClicked && /* @__PURE__ */ jsx2(
2077
+ operationKey: appliedOperationKey || void 0,
2078
+ renderState: ({ actions, message, status, password }) => /* @__PURE__ */ jsx2(Fragment, { children: !hasUserClicked && /* @__PURE__ */ jsx2(
2009
2079
  "div",
2010
2080
  {
2011
2081
  style: {
@@ -2060,48 +2130,117 @@ var EmbedTester = (props = {}) => {
2060
2130
  fontSize: "28px",
2061
2131
  letterSpacing: "-0.03em"
2062
2132
  },
2063
- children: embedState.hasPlayedOnce ? "Resume the session" : "Start the demo"
2133
+ children: password?.required && !password.unlocked ? "Password required" : embedState.hasPlayedOnce ? "Resume the session" : "Start the demo"
2064
2134
  }
2065
2135
  ),
2066
2136
  /* @__PURE__ */ jsxs2("p", { style: { margin: 0, color: "#94a3b8", lineHeight: 1.7 }, children: [
2067
- "Status: ",
2137
+ "Status:",
2138
+ " ",
2068
2139
  /* @__PURE__ */ jsx2("span", { style: { color: "#f8fafc", fontWeight: 700 }, children: status })
2069
2140
  ] }),
2070
- message && /* @__PURE__ */ jsx2("p", { style: { margin: "10px 0 0 0", color: "#cbd5e1", lineHeight: 1.7 }, children: message }),
2071
- /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexWrap: "wrap", gap: "12px", marginTop: "24px" }, children: [
2072
- /* @__PURE__ */ jsxs2(
2073
- "button",
2074
- {
2075
- type: "button",
2076
- style: buildButtonStyle("primary", !actions.canTryNow),
2077
- disabled: !actions.canTryNow,
2078
- onClick: () => {
2079
- setHasUserClicked(true);
2080
- setShowIframe(true);
2081
- addEvent(
2082
- embedState.hasPlayedOnce ? "User clicked - Resuming game" : "User clicked - Starting game with tryNow"
2083
- );
2084
- actions.tryNow();
2085
- },
2086
- children: [
2087
- "\u{1F3AE} ",
2088
- embedState.hasPlayedOnce ? "Resume game" : "Try demo now"
2089
- ]
2090
- }
2091
- ),
2092
- actions.canBuyNow && /* @__PURE__ */ jsx2(
2093
- "button",
2094
- {
2095
- type: "button",
2096
- style: buildButtonStyle("secondary"),
2097
- onClick: () => {
2098
- addEvent("Buy Now clicked");
2099
- actions.buyNow();
2100
- },
2101
- children: "Buy Now"
2102
- }
2103
- )
2104
- ] })
2141
+ message && !(password?.required && !password.unlocked) && /* @__PURE__ */ jsx2("p", { style: { margin: "10px 0 0 0", color: "#cbd5e1", lineHeight: 1.7 }, children: message }),
2142
+ password?.required && !password.unlocked ? /* @__PURE__ */ jsxs2(
2143
+ "form",
2144
+ {
2145
+ style: {
2146
+ display: "flex",
2147
+ flexWrap: "wrap",
2148
+ gap: "12px",
2149
+ marginTop: "24px",
2150
+ alignItems: "center"
2151
+ },
2152
+ onSubmit: (event) => {
2153
+ event.preventDefault();
2154
+ if (!overlayPassword) {
2155
+ return;
2156
+ }
2157
+ actions.submitPassword(overlayPassword);
2158
+ setOverlayPassword("");
2159
+ },
2160
+ children: [
2161
+ /* @__PURE__ */ jsx2(
2162
+ "input",
2163
+ {
2164
+ "aria-label": "Operation password",
2165
+ autoComplete: "off",
2166
+ onChange: (event) => setOverlayPassword(event.target.value),
2167
+ placeholder: "Operation password",
2168
+ style: {
2169
+ flex: "1 1 180px",
2170
+ minWidth: "140px",
2171
+ padding: "12px 14px",
2172
+ borderRadius: "12px",
2173
+ border: "1px solid rgba(148, 163, 184, 0.28)",
2174
+ backgroundColor: "rgba(15, 23, 42, 0.9)",
2175
+ color: "#f8fafc"
2176
+ },
2177
+ type: "password",
2178
+ value: overlayPassword
2179
+ }
2180
+ ),
2181
+ /* @__PURE__ */ jsx2(
2182
+ "button",
2183
+ {
2184
+ type: "submit",
2185
+ style: buildButtonStyle("primary", !overlayPassword),
2186
+ disabled: !overlayPassword,
2187
+ children: "Unlock"
2188
+ }
2189
+ ),
2190
+ password.error ? /* @__PURE__ */ jsx2(
2191
+ "p",
2192
+ {
2193
+ style: {
2194
+ flex: "1 1 100%",
2195
+ margin: 0,
2196
+ color: "#fca5a5",
2197
+ fontSize: "13px"
2198
+ },
2199
+ children: password.error
2200
+ }
2201
+ ) : null
2202
+ ]
2203
+ }
2204
+ ) : /* @__PURE__ */ jsxs2(
2205
+ "div",
2206
+ {
2207
+ style: { display: "flex", flexWrap: "wrap", gap: "12px", marginTop: "24px" },
2208
+ children: [
2209
+ /* @__PURE__ */ jsxs2(
2210
+ "button",
2211
+ {
2212
+ type: "button",
2213
+ style: buildButtonStyle("primary", !actions.canTryNow),
2214
+ disabled: !actions.canTryNow,
2215
+ onClick: () => {
2216
+ setHasUserClicked(true);
2217
+ setShowIframe(true);
2218
+ addEvent(
2219
+ embedState.hasPlayedOnce ? "User clicked - Resuming game" : "User clicked - Starting game with tryNow"
2220
+ );
2221
+ actions.tryNow();
2222
+ },
2223
+ children: [
2224
+ "\u{1F3AE} ",
2225
+ embedState.hasPlayedOnce ? "Resume game" : "Try demo now"
2226
+ ]
2227
+ }
2228
+ ),
2229
+ actions.canBuyNow && /* @__PURE__ */ jsx2(
2230
+ "button",
2231
+ {
2232
+ type: "button",
2233
+ style: buildButtonStyle("secondary"),
2234
+ onClick: () => {
2235
+ addEvent("Buy Now clicked");
2236
+ actions.buyNow();
2237
+ },
2238
+ children: "Buy Now"
2239
+ }
2240
+ )
2241
+ ]
2242
+ }
2243
+ )
2105
2244
  ]
2106
2245
  }
2107
2246
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playruo/embed",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "description": "React SDK to embed Playruo with postMessage API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,15 +35,22 @@
35
35
  "@types/node": "^26.2.0",
36
36
  "@types/react": "^19",
37
37
  "@types/react-dom": "^19",
38
- "@vitest/browser": "^3.2.7",
39
- "@vitest/coverage-v8": "^3.2.4",
38
+ "@vitest/browser": "^5.0.2",
39
+ "@vitest/browser-playwright": "^5.0.2",
40
+ "@vitest/coverage-v8": "^5.0.2",
40
41
  "jsdom": "^26.1.0",
41
- "playwright": "^1.62.1",
42
+ "playwright": "1.62.1",
42
43
  "react": "^19.1.0",
43
44
  "react-dom": "^19.1.0",
44
45
  "tsup": "^8.0.0",
45
46
  "typescript": "^5.4.0",
46
- "vitest": "^3.2.4"
47
+ "vitest": "^5.0.2"
48
+ },
49
+ "overrides": {
50
+ "esbuild": "^0.28.1",
51
+ "picomatch": "^4.0.4",
52
+ "rollup": "^4.59.0",
53
+ "source-map-js": "^1.2.2"
47
54
  },
48
55
  "scripts": {
49
56
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",
@@ -52,9 +59,11 @@
52
59
  "typecheck": "tsc -p tsconfig.test.json",
53
60
  "check:exports": "attw --pack .",
54
61
  "test": "npm run build && vitest run --coverage",
62
+ "test:smoke": "vitest run tests/smoke",
55
63
  "test:browser": "vitest run --config vitest.browser.config.ts",
56
64
  "test:watch": "npm run build && vitest",
57
65
  "docs:notion:dry-run": "node scripts/sync-notion-docs.mjs",
58
- "docs:notion:publish": "node scripts/sync-notion-docs.mjs --publish"
66
+ "docs:notion:publish": "node scripts/sync-notion-docs.mjs --publish",
67
+ "docs:catalog:check": "node scripts/check-embed-event-catalog.mjs && node scripts/check-embed-error-catalog.mjs"
59
68
  }
60
69
  }