@accelerated-agency/visual-editor 0.6.1 → 0.7.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
@@ -4801,7 +4801,110 @@ function PlatformVisualEditor({
4801
4801
  /* @__PURE__ */ jsx("div", { className: editorClassName, children: /* @__PURE__ */ jsx(ToastProvider, { children: /* @__PURE__ */ jsx(EditorShell, { initialExperiment: experiment, embeddedMode: true, proxyBaseUrl }) }) })
4802
4802
  ] });
4803
4803
  }
4804
+
4805
+ // src/lib/uploadImageMessage.ts
4806
+ var VISUAL_EDITOR_MAX_UPLOAD_BYTES = 1e7;
4807
+ var ALLOWED_TYPES = /* @__PURE__ */ new Set([
4808
+ "image/jpeg",
4809
+ "image/png",
4810
+ "image/gif",
4811
+ "image/webp"
4812
+ ]);
4813
+ var EXT_TO_TYPE = {
4814
+ jpg: "image/jpeg",
4815
+ jpeg: "image/jpeg",
4816
+ png: "image/png",
4817
+ gif: "image/gif",
4818
+ webp: "image/webp"
4819
+ };
4820
+ function uploadImageErrorMessage(err, fallback = "The upload was refused") {
4821
+ if (err instanceof Error && err.message) return err.message;
4822
+ if (typeof err === "string" && err.trim()) return err;
4823
+ return fallback;
4824
+ }
4825
+ function fileFromUploadImagePayload(payload) {
4826
+ if (!payload || typeof payload !== "object") {
4827
+ throw new Error("No image was sent");
4828
+ }
4829
+ const existing = asBlob(payload.file);
4830
+ const name = sanitizeName(payload.name, existing instanceof File ? existing.name : "");
4831
+ const type = normalizeType(
4832
+ typeof payload.type === "string" ? payload.type : existing?.type,
4833
+ name
4834
+ );
4835
+ const bytes = existing ? void 0 : bytesFromUnknown(payload.bytes !== void 0 ? payload.bytes : payload.data);
4836
+ if (!existing && !bytes) {
4837
+ throw new Error("No image bytes were sent");
4838
+ }
4839
+ const size = existing?.size ?? bytes.byteLength;
4840
+ if (typeof payload.size === "number" && payload.size > VISUAL_EDITOR_MAX_UPLOAD_BYTES) {
4841
+ throw new Error("Image is larger than 10MB");
4842
+ }
4843
+ if (size > VISUAL_EDITOR_MAX_UPLOAD_BYTES) {
4844
+ throw new Error("Image is larger than 10MB");
4845
+ }
4846
+ if (!ALLOWED_TYPES.has(type)) {
4847
+ throw new Error("Use a JPEG, PNG, GIF, or WebP image");
4848
+ }
4849
+ if (existing instanceof File) {
4850
+ if (existing.type && !ALLOWED_TYPES.has(normalizeType(existing.type, existing.name))) {
4851
+ throw new Error("Use a JPEG, PNG, GIF, or WebP image");
4852
+ }
4853
+ return existing;
4854
+ }
4855
+ if (existing) {
4856
+ return new File([existing], name, { type });
4857
+ }
4858
+ return new File([bytes], name, { type });
4859
+ }
4860
+ function sanitizeName(value, fallback) {
4861
+ const raw = typeof value === "string" ? value.trim() : "";
4862
+ const name = raw || fallback || "image.png";
4863
+ return name.replace(/[/\\]/g, "").slice(0, 180) || "image.png";
4864
+ }
4865
+ function normalizeType(value, name) {
4866
+ const raw = typeof value === "string" ? value.trim().toLowerCase() : "";
4867
+ if (raw === "image/jpg") return "image/jpeg";
4868
+ if (ALLOWED_TYPES.has(raw)) return raw;
4869
+ const ext = name.split(".").pop()?.toLowerCase() || "";
4870
+ return EXT_TO_TYPE[ext] || raw;
4871
+ }
4872
+ function asBlob(value) {
4873
+ if (typeof File !== "undefined" && value instanceof File) return value;
4874
+ if (typeof Blob !== "undefined" && value instanceof Blob) return value;
4875
+ return null;
4876
+ }
4877
+ function bytesFromUnknown(value) {
4878
+ if (!value) return null;
4879
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
4880
+ if (ArrayBuffer.isView(value)) {
4881
+ const view = value;
4882
+ return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
4883
+ }
4884
+ if (Array.isArray(value)) {
4885
+ return Uint8Array.from(value.map((n) => Number(n) & 255));
4886
+ }
4887
+ if (typeof value === "string") {
4888
+ return bytesFromBase64(value);
4889
+ }
4890
+ return null;
4891
+ }
4892
+ function bytesFromBase64(value) {
4893
+ const comma = value.indexOf(",");
4894
+ const raw = value.startsWith("data:") && comma !== -1 ? value.slice(comma + 1) : value;
4895
+ const binary = atob(raw);
4896
+ const out = new Uint8Array(binary.length);
4897
+ for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
4898
+ return out;
4899
+ }
4804
4900
  var VVVEB_CHANNEL = "vvveb-bridge";
4901
+ function frameOrigin(frame) {
4902
+ try {
4903
+ return new URL(frame?.src || "", window.location.href).origin;
4904
+ } catch {
4905
+ return window.location.origin;
4906
+ }
4907
+ }
4805
4908
  function PlatformVisualEditorV2({
4806
4909
  // channel kept for API compatibility; VvvebJs uses its own internal channel
4807
4910
  embeddedGlobalKey = "__CONVERSION_EMBEDDED__",
@@ -4832,6 +4935,7 @@ function PlatformVisualEditorV2({
4832
4935
  onSaveSuccess,
4833
4936
  onSaveError,
4834
4937
  onRequestSave,
4938
+ onUploadImage,
4835
4939
  onNavigateRequested,
4836
4940
  onDiscardDirty,
4837
4941
  renderHeader,
@@ -4860,9 +4964,36 @@ function PlatformVisualEditorV2({
4860
4964
  const sendToVvveb = useCallback((type, payload) => {
4861
4965
  iframeRef.current?.contentWindow?.postMessage(
4862
4966
  { channel: VVVEB_CHANNEL, type, payload },
4863
- "*"
4967
+ frameOrigin(iframeRef.current)
4864
4968
  );
4865
4969
  }, []);
4970
+ const sendSaveResult = useCallback(
4971
+ (payload, err) => {
4972
+ const requestId = typeof payload?.requestId === "string" ? payload.requestId : null;
4973
+ if (!err) {
4974
+ sendToVvveb("save-result", { requestId, ok: true, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
4975
+ return;
4976
+ }
4977
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : "The save was refused";
4978
+ sendToVvveb("save-result", { requestId, ok: false, error: message });
4979
+ },
4980
+ [sendToVvveb]
4981
+ );
4982
+ const sendUploadImageResult = useCallback(
4983
+ (payload, result, err) => {
4984
+ const requestId = typeof payload?.requestId === "string" ? payload.requestId : null;
4985
+ if (!err) {
4986
+ sendToVvveb("upload-image-result", { requestId, ok: true, url: result?.url || "" });
4987
+ return;
4988
+ }
4989
+ sendToVvveb("upload-image-result", {
4990
+ requestId,
4991
+ ok: false,
4992
+ error: uploadImageErrorMessage(err)
4993
+ });
4994
+ },
4995
+ [sendToVvveb]
4996
+ );
4866
4997
  const loadPayload = useMemo(
4867
4998
  () => ({
4868
4999
  iid: experiment?.iid,
@@ -4874,9 +5005,16 @@ function PlatformVisualEditorV2({
4874
5005
  conversionProxyBaseUrl: typeof conversionProxyBaseUrl === "string" ? conversionProxyBaseUrl.trim().replace(/\/+$/, "") : "",
4875
5006
  strictObserverFreeze: !!strictObserverFreeze,
4876
5007
  trackingMarkers: Array.isArray(trackingMarkers) ? trackingMarkers.filter((m) => typeof m === "string" && m.trim().length > 0) : [],
4877
- variations: experiment?.variations ?? []
5008
+ variations: experiment?.variations ?? [],
5009
+ // S1: this package answers every save with "save-result". A shell that
5010
+ // knows the field waits for the answer before it clears its working
5011
+ // state; an older shell ignores the field and behaves as it did before.
5012
+ capabilities: {
5013
+ saveResult: true,
5014
+ ...onUploadImage ? { uploadImage: true } : {}
5015
+ }
4878
5016
  }),
4879
- [conversionProxyBaseUrl, experiment, strictObserverFreeze, trackingMarkers]
5017
+ [conversionProxyBaseUrl, experiment, onUploadImage, strictObserverFreeze, trackingMarkers]
4880
5018
  );
4881
5019
  const editorSrc = useMemo(() => {
4882
5020
  const workerBase = normalizeProxyBaseUrl(conversionProxyBaseUrl);
@@ -4895,6 +5033,8 @@ function PlatformVisualEditorV2({
4895
5033
  const handleMessage = useCallback(
4896
5034
  async (e) => {
4897
5035
  if (!e.data || e.data.channel !== VVVEB_CHANNEL) return;
5036
+ if (!iframeRef.current || e.source !== iframeRef.current.contentWindow) return;
5037
+ if (e.origin !== frameOrigin(iframeRef.current)) return;
4898
5038
  const { type, payload } = e.data;
4899
5039
  switch (type) {
4900
5040
  case "editor-ready":
@@ -4922,8 +5062,10 @@ function PlatformVisualEditorV2({
4922
5062
  try {
4923
5063
  await onRequestSave(payload ?? {});
4924
5064
  setDirty(false);
5065
+ sendSaveResult(payload, null);
4925
5066
  onSaveSuccess?.(payload ?? {});
4926
5067
  } catch (err) {
5068
+ sendSaveResult(payload, err);
4927
5069
  onSaveError?.(err);
4928
5070
  }
4929
5071
  break;
@@ -4932,13 +5074,35 @@ function PlatformVisualEditorV2({
4932
5074
  try {
4933
5075
  await onRequestSave(payload ?? {});
4934
5076
  setDirty(false);
5077
+ sendSaveResult(payload, null);
4935
5078
  onSaveSuccess?.(payload ?? {});
4936
5079
  if (payload?.hash) onNavigateRequested?.(payload.hash);
4937
5080
  } catch (err) {
5081
+ sendSaveResult(payload, err);
4938
5082
  onSaveError?.(err);
4939
5083
  }
4940
5084
  break;
5085
+ case "upload-image":
5086
+ if (typeof e.stopImmediatePropagation === "function") e.stopImmediatePropagation();
5087
+ if (!onUploadImage) {
5088
+ sendUploadImageResult(payload, null, "Image upload is not available");
5089
+ return;
5090
+ }
5091
+ try {
5092
+ const file = fileFromUploadImagePayload(payload);
5093
+ const result = await onUploadImage(file);
5094
+ const url = typeof result?.url === "string" ? result.url.trim() : "";
5095
+ if (!url) throw new Error("The upload did not return a URL");
5096
+ sendUploadImageResult(payload, { url }, null);
5097
+ } catch (err) {
5098
+ sendUploadImageResult(payload, null, err);
5099
+ }
5100
+ break;
4941
5101
  case "close-editor":
5102
+ if (payload?.discard === true) {
5103
+ onClose?.();
5104
+ return;
5105
+ }
4942
5106
  if (!dirtyRef.current) {
4943
5107
  onClose?.();
4944
5108
  return;
@@ -4959,12 +5123,15 @@ function PlatformVisualEditorV2({
4959
5123
  onEditorReady,
4960
5124
  onEditorUrlChanged,
4961
5125
  onRequestSave,
5126
+ onUploadImage,
4962
5127
  onSaveSuccess,
4963
5128
  onSaveError,
4964
5129
  onNavigateRequested,
4965
5130
  onClose,
4966
5131
  onDiscardDirty,
4967
- sendToVvveb
5132
+ sendToVvveb,
5133
+ sendSaveResult,
5134
+ sendUploadImageResult
4968
5135
  ]
4969
5136
  );
4970
5137
  useLayoutEffect(() => {