@accelerated-agency/visual-editor 0.6.2 → 0.7.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.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,10 @@ function PlatformVisualEditorV2({
4832
4935
  onSaveSuccess,
4833
4936
  onSaveError,
4834
4937
  onRequestSave,
4938
+ onUploadImage,
4939
+ onListImages,
4940
+ onAiAssist,
4941
+ onAiApplied,
4835
4942
  onNavigateRequested,
4836
4943
  onDiscardDirty,
4837
4944
  renderHeader,
@@ -4860,9 +4967,88 @@ function PlatformVisualEditorV2({
4860
4967
  const sendToVvveb = useCallback((type, payload) => {
4861
4968
  iframeRef.current?.contentWindow?.postMessage(
4862
4969
  { channel: VVVEB_CHANNEL, type, payload },
4863
- "*"
4970
+ frameOrigin(iframeRef.current)
4864
4971
  );
4865
4972
  }, []);
4973
+ const sendSaveResult = useCallback(
4974
+ (payload, err) => {
4975
+ const requestId = typeof payload?.requestId === "string" ? payload.requestId : null;
4976
+ if (!err) {
4977
+ sendToVvveb("save-result", { requestId, ok: true, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
4978
+ return;
4979
+ }
4980
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : "The save was refused";
4981
+ sendToVvveb("save-result", { requestId, ok: false, error: message });
4982
+ },
4983
+ [sendToVvveb]
4984
+ );
4985
+ const sendUploadImageResult = useCallback(
4986
+ (payload, result, err) => {
4987
+ const requestId = typeof payload?.requestId === "string" ? payload.requestId : null;
4988
+ if (!err) {
4989
+ sendToVvveb("upload-image-result", { requestId, ok: true, url: result?.url || "" });
4990
+ return;
4991
+ }
4992
+ sendToVvveb("upload-image-result", {
4993
+ requestId,
4994
+ ok: false,
4995
+ error: uploadImageErrorMessage(err)
4996
+ });
4997
+ },
4998
+ [sendToVvveb]
4999
+ );
5000
+ const handleListImages = useCallback(
5001
+ async (payload) => {
5002
+ const requestId = typeof payload?.requestId === "string" ? payload.requestId : null;
5003
+ if (!onListImages) {
5004
+ sendToVvveb("list-images-result", {
5005
+ requestId,
5006
+ ok: false,
5007
+ error: "Image list is not available"
5008
+ });
5009
+ return;
5010
+ }
5011
+ try {
5012
+ const result = await onListImages();
5013
+ const images = Array.isArray(result?.images) ? result.images.filter((img) => img && typeof img.url === "string" && img.url) : [];
5014
+ sendToVvveb("list-images-result", { requestId, ok: true, images });
5015
+ } catch (err) {
5016
+ sendToVvveb("list-images-result", {
5017
+ requestId,
5018
+ ok: false,
5019
+ error: uploadImageErrorMessage(err, "The list was refused")
5020
+ });
5021
+ }
5022
+ },
5023
+ [onListImages, sendToVvveb]
5024
+ );
5025
+ const handleAiRequest = useCallback(
5026
+ async (payload) => {
5027
+ const requestId = typeof payload?.requestId === "string" ? payload.requestId : null;
5028
+ if (!onAiAssist) {
5029
+ sendToVvveb("ai-result", {
5030
+ requestId,
5031
+ ok: false,
5032
+ error: "The assistant is not switched on for this workspace"
5033
+ });
5034
+ return;
5035
+ }
5036
+ try {
5037
+ const result = await onAiAssist(payload ?? {});
5038
+ sendToVvveb("ai-result", {
5039
+ ...result,
5040
+ requestId: result?.requestId || requestId
5041
+ });
5042
+ } catch (err) {
5043
+ sendToVvveb("ai-result", {
5044
+ requestId,
5045
+ ok: false,
5046
+ error: uploadImageErrorMessage(err, "The assistant did not answer. Try again.")
5047
+ });
5048
+ }
5049
+ },
5050
+ [onAiAssist, sendToVvveb]
5051
+ );
4866
5052
  const loadPayload = useMemo(
4867
5053
  () => ({
4868
5054
  iid: experiment?.iid,
@@ -4874,9 +5060,26 @@ function PlatformVisualEditorV2({
4874
5060
  conversionProxyBaseUrl: typeof conversionProxyBaseUrl === "string" ? conversionProxyBaseUrl.trim().replace(/\/+$/, "") : "",
4875
5061
  strictObserverFreeze: !!strictObserverFreeze,
4876
5062
  trackingMarkers: Array.isArray(trackingMarkers) ? trackingMarkers.filter((m) => typeof m === "string" && m.trim().length > 0) : [],
4877
- variations: experiment?.variations ?? []
5063
+ variations: experiment?.variations ?? [],
5064
+ // S1: this package answers every save with "save-result". A shell that
5065
+ // knows the field waits for the answer before it clears its working
5066
+ // state; an older shell ignores the field and behaves as it did before.
5067
+ capabilities: {
5068
+ saveResult: true,
5069
+ ...onUploadImage ? { uploadImage: true } : {},
5070
+ ...onListImages ? { listImages: true } : {},
5071
+ ...onAiAssist ? { ai: true } : {}
5072
+ }
4878
5073
  }),
4879
- [conversionProxyBaseUrl, experiment, strictObserverFreeze, trackingMarkers]
5074
+ [
5075
+ conversionProxyBaseUrl,
5076
+ experiment,
5077
+ onAiAssist,
5078
+ onListImages,
5079
+ onUploadImage,
5080
+ strictObserverFreeze,
5081
+ trackingMarkers
5082
+ ]
4880
5083
  );
4881
5084
  const editorSrc = useMemo(() => {
4882
5085
  const workerBase = normalizeProxyBaseUrl(conversionProxyBaseUrl);
@@ -4895,6 +5098,8 @@ function PlatformVisualEditorV2({
4895
5098
  const handleMessage = useCallback(
4896
5099
  async (e) => {
4897
5100
  if (!e.data || e.data.channel !== VVVEB_CHANNEL) return;
5101
+ if (!iframeRef.current || e.source !== iframeRef.current.contentWindow) return;
5102
+ if (e.origin !== frameOrigin(iframeRef.current)) return;
4898
5103
  const { type, payload } = e.data;
4899
5104
  switch (type) {
4900
5105
  case "editor-ready":
@@ -4922,8 +5127,10 @@ function PlatformVisualEditorV2({
4922
5127
  try {
4923
5128
  await onRequestSave(payload ?? {});
4924
5129
  setDirty(false);
5130
+ sendSaveResult(payload, null);
4925
5131
  onSaveSuccess?.(payload ?? {});
4926
5132
  } catch (err) {
5133
+ sendSaveResult(payload, err);
4927
5134
  onSaveError?.(err);
4928
5135
  }
4929
5136
  break;
@@ -4932,13 +5139,50 @@ function PlatformVisualEditorV2({
4932
5139
  try {
4933
5140
  await onRequestSave(payload ?? {});
4934
5141
  setDirty(false);
5142
+ sendSaveResult(payload, null);
4935
5143
  onSaveSuccess?.(payload ?? {});
4936
5144
  if (payload?.hash) onNavigateRequested?.(payload.hash);
4937
5145
  } catch (err) {
5146
+ sendSaveResult(payload, err);
4938
5147
  onSaveError?.(err);
4939
5148
  }
4940
5149
  break;
5150
+ case "upload-image":
5151
+ if (typeof e.stopImmediatePropagation === "function") e.stopImmediatePropagation();
5152
+ if (!onUploadImage) {
5153
+ sendUploadImageResult(payload, null, "Image upload is not available");
5154
+ return;
5155
+ }
5156
+ try {
5157
+ const file = fileFromUploadImagePayload(payload);
5158
+ const result = await onUploadImage(file);
5159
+ const url = typeof result?.url === "string" ? result.url.trim() : "";
5160
+ if (!url) throw new Error("The upload did not return a URL");
5161
+ sendUploadImageResult(payload, { url }, null);
5162
+ } catch (err) {
5163
+ sendUploadImageResult(payload, null, err);
5164
+ }
5165
+ break;
5166
+ case "list-images":
5167
+ if (typeof e.stopImmediatePropagation === "function") e.stopImmediatePropagation();
5168
+ await handleListImages(payload);
5169
+ break;
5170
+ case "ai-request":
5171
+ if (typeof e.stopImmediatePropagation === "function") e.stopImmediatePropagation();
5172
+ await handleAiRequest(payload);
5173
+ break;
5174
+ case "ai-applied":
5175
+ if (typeof e.stopImmediatePropagation === "function") e.stopImmediatePropagation();
5176
+ try {
5177
+ await onAiApplied?.(payload ?? {});
5178
+ } catch {
5179
+ }
5180
+ break;
4941
5181
  case "close-editor":
5182
+ if (payload?.discard === true) {
5183
+ onClose?.();
5184
+ return;
5185
+ }
4942
5186
  if (!dirtyRef.current) {
4943
5187
  onClose?.();
4944
5188
  return;
@@ -4959,12 +5203,20 @@ function PlatformVisualEditorV2({
4959
5203
  onEditorReady,
4960
5204
  onEditorUrlChanged,
4961
5205
  onRequestSave,
5206
+ onUploadImage,
5207
+ onListImages,
5208
+ onAiAssist,
5209
+ onAiApplied,
5210
+ handleListImages,
5211
+ handleAiRequest,
4962
5212
  onSaveSuccess,
4963
5213
  onSaveError,
4964
5214
  onNavigateRequested,
4965
5215
  onClose,
4966
5216
  onDiscardDirty,
4967
- sendToVvveb
5217
+ sendToVvveb,
5218
+ sendSaveResult,
5219
+ sendUploadImageResult
4968
5220
  ]
4969
5221
  );
4970
5222
  useLayoutEffect(() => {