react-shopwave-connect 0.3.0 → 0.4.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/README.md +73 -4
- package/dist/core/index.cjs +612 -7
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +459 -2
- package/dist/core/index.d.ts +459 -2
- package/dist/core/index.js +579 -8
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +456 -0
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +229 -1
- package/dist/hooks/index.d.ts +229 -1
- package/dist/hooks/index.js +452 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +696 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +512 -2
- package/dist/index.d.ts +512 -2
- package/dist/index.js +658 -8
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +126 -2
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +82 -2
- package/dist/next/index.d.ts +82 -2
- package/dist/next/index.js +125 -3
- package/dist/next/index.js.map +1 -1
- package/dist/server/index.cjs +122 -2
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +90 -2
- package/dist/server/index.d.ts +90 -2
- package/dist/server/index.js +120 -3
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -66,8 +66,8 @@ function getApiErrorMap(body) {
|
|
|
66
66
|
}
|
|
67
67
|
function describeErrors(errors) {
|
|
68
68
|
return Object.entries(errors).map(([code, e]) => {
|
|
69
|
-
const text = [e?.title, e?.details].filter(Boolean).join(": ");
|
|
70
|
-
return text ? `${code} ${text}` : code;
|
|
69
|
+
const text$1 = [e?.title, e?.details].filter(Boolean).join(": ");
|
|
70
|
+
return text$1 ? `${code} ${text$1}` : code;
|
|
71
71
|
}).join("; ");
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
@@ -129,12 +129,12 @@ function buildExtras(extras) {
|
|
|
129
129
|
}
|
|
130
130
|
/** Parses a response body as JSON; `null` for an empty body, `{ raw }` for non-JSON. */
|
|
131
131
|
async function readBody(response) {
|
|
132
|
-
const text = await response.text();
|
|
133
|
-
if (!text) return null;
|
|
132
|
+
const text$1 = await response.text();
|
|
133
|
+
if (!text$1) return null;
|
|
134
134
|
try {
|
|
135
|
-
return JSON.parse(text);
|
|
135
|
+
return JSON.parse(text$1);
|
|
136
136
|
} catch {
|
|
137
|
-
return { raw: text };
|
|
137
|
+
return { raw: text$1 };
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
@@ -282,6 +282,83 @@ async function fetchProducts(params = {}, options = {}) {
|
|
|
282
282
|
assertNoApiErrors(json, 200);
|
|
283
283
|
return Object.values(json?.products ?? {});
|
|
284
284
|
}
|
|
285
|
+
/** Fields the server sets on an instance; never sent back. */
|
|
286
|
+
const SERVER_INSTANCE_FIELDS = ["timestamp"];
|
|
287
|
+
function instanceList(instances) {
|
|
288
|
+
if (!instances) return [];
|
|
289
|
+
return (Array.isArray(instances) ? [...instances] : Object.values(instances)).filter((inst) => inst != null && typeof inst === "object");
|
|
290
|
+
}
|
|
291
|
+
function hasId(id) {
|
|
292
|
+
return id != null && String(id).trim() !== "";
|
|
293
|
+
}
|
|
294
|
+
/** Same number, so `"422.0"`, `"422"` and `422` are equal; blanks equal each other. */
|
|
295
|
+
function sameAmount(a, b) {
|
|
296
|
+
const blank = (v) => v == null || String(v).trim() === "";
|
|
297
|
+
if (blank(a) || blank(b)) return blank(a) && blank(b);
|
|
298
|
+
const x = Number(a);
|
|
299
|
+
const y = Number(b);
|
|
300
|
+
return Number.isNaN(x) || Number.isNaN(y) ? String(a).trim() === String(b).trim() : x === y;
|
|
301
|
+
}
|
|
302
|
+
/** True when two instances have the same price and tax (the fields Shopwave won't update). */
|
|
303
|
+
function isSameInstancePrice(a, b) {
|
|
304
|
+
return sameAmount(a.price, b.price) && sameAmount(a.taxPercentage, b.taxPercentage);
|
|
305
|
+
}
|
|
306
|
+
const text = (v) => v == null ? "" : String(v);
|
|
307
|
+
/**
|
|
308
|
+
* Lists the instances whose price or tax differs from the `original` instance
|
|
309
|
+
* with the same id: saving them creates new instances with new ids, which can
|
|
310
|
+
* affect anything that stores instance ids (integrations, reports, modifiers).
|
|
311
|
+
* Use it to warn before saving. New rows (no id) aren't listed.
|
|
312
|
+
*/
|
|
313
|
+
function findRepricedInstances(instances, original) {
|
|
314
|
+
const byId = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));
|
|
315
|
+
const result = [];
|
|
316
|
+
instanceList(instances).forEach((inst, index) => {
|
|
317
|
+
if (!hasId(inst.id)) return;
|
|
318
|
+
const before = byId.get(String(inst.id));
|
|
319
|
+
if (!before || isSameInstancePrice(inst, before)) return;
|
|
320
|
+
result.push({
|
|
321
|
+
index,
|
|
322
|
+
id: inst.id,
|
|
323
|
+
name: text(inst.name),
|
|
324
|
+
before: {
|
|
325
|
+
price: text(before.price),
|
|
326
|
+
taxPercentage: text(before.taxPercentage)
|
|
327
|
+
},
|
|
328
|
+
after: {
|
|
329
|
+
price: text(inst.price),
|
|
330
|
+
taxPercentage: text(inst.taxPercentage)
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Builds the `instances` map for a product save:
|
|
338
|
+
* - keyed by position (`"0"`, `"1"`, …), like every other ref in a save;
|
|
339
|
+
* - an instance keeps its `id` only when `original` has an instance with that id
|
|
340
|
+
* and the same price and tax; otherwise the id is removed, so Shopwave creates
|
|
341
|
+
* a new instance instead of re-pointing or rewriting someone else's;
|
|
342
|
+
* - server fields (`timestamp`) are removed.
|
|
343
|
+
*
|
|
344
|
+
* Pass the product as last read from the API as `original`. Leave it out for a
|
|
345
|
+
* new product (including a duplicate): every id is then removed.
|
|
346
|
+
* Instances left out of the list are left out of the save (removed).
|
|
347
|
+
*/
|
|
348
|
+
function toProductInstancesForSave(instances, original) {
|
|
349
|
+
const keep = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));
|
|
350
|
+
const out = {};
|
|
351
|
+
instanceList(instances).forEach((inst, index) => {
|
|
352
|
+
const { id,...rest } = inst;
|
|
353
|
+
for (const field of SERVER_INSTANCE_FIELDS) delete rest[field];
|
|
354
|
+
const before = hasId(id) ? keep.get(String(id)) : void 0;
|
|
355
|
+
out[String(index)] = before && isSameInstancePrice(inst, before) ? {
|
|
356
|
+
id,
|
|
357
|
+
...rest
|
|
358
|
+
} : rest;
|
|
359
|
+
});
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
285
362
|
/**
|
|
286
363
|
* Fetches products by id and returns them keyed by id, transparently batching
|
|
287
364
|
* the request (default 200 ids per call) to stay within API limits. Extracted
|
|
@@ -565,7 +642,21 @@ async function fetchEntityById(kind, id, params = {}, options = {}) {
|
|
|
565
642
|
const map = body?.[def.collection] ?? {};
|
|
566
643
|
return map[String(id)] ?? Object.values(map).find((e) => String(e?.id) === String(id)) ?? null;
|
|
567
644
|
}
|
|
568
|
-
|
|
645
|
+
/**
|
|
646
|
+
* Creates (no `id`) or updates (with `id`) a product.
|
|
647
|
+
*
|
|
648
|
+
* Shopwave instances (prices) can't be updated, only replaced. Build
|
|
649
|
+
* `instances` with {@link toProductInstancesForSave} (passing the product as last
|
|
650
|
+
* read) so an edited price is sent as a new instance. As a safeguard, a new
|
|
651
|
+
* product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from
|
|
652
|
+
* another product would otherwise point the save at that product's instances.
|
|
653
|
+
*/
|
|
654
|
+
const saveProduct = (product, options) => {
|
|
655
|
+
return saveEntity("product", (product.id == null || String(product.id).trim() === "") && product.instances != null ? {
|
|
656
|
+
...product,
|
|
657
|
+
instances: toProductInstancesForSave(product.instances)
|
|
658
|
+
} : product, options);
|
|
659
|
+
};
|
|
569
660
|
const deleteProduct = (id, options) => deleteEntityById("product", id, options);
|
|
570
661
|
const fetchProduct = (id, params, options) => fetchEntityById("product", id, params, options);
|
|
571
662
|
const saveCategory = (category, options) => saveEntity("category", category, options);
|
|
@@ -734,6 +825,486 @@ function buildConsumerMap(consumers) {
|
|
|
734
825
|
return map;
|
|
735
826
|
}
|
|
736
827
|
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/core/resources.ts
|
|
830
|
+
/**
|
|
831
|
+
* - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
|
|
832
|
+
* updates it (without an `id` the API would create a second merchant, so the
|
|
833
|
+
* route requires one).
|
|
834
|
+
* - user: `GET /user` (read-only here; the API's `POST /user` invites users and
|
|
835
|
+
* needs the admin scope).
|
|
836
|
+
*/
|
|
837
|
+
const SHOPWAVE_RESOURCES = Object.freeze({
|
|
838
|
+
merchant: {
|
|
839
|
+
route: "merchant",
|
|
840
|
+
key: "merchant",
|
|
841
|
+
upstream: "merchant",
|
|
842
|
+
writable: true
|
|
843
|
+
},
|
|
844
|
+
user: {
|
|
845
|
+
route: "user",
|
|
846
|
+
key: "user",
|
|
847
|
+
upstream: "user",
|
|
848
|
+
writable: false
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
/** `UploadKind` → the API's `contentType` value. */
|
|
852
|
+
const UPLOAD_KINDS = Object.freeze({
|
|
853
|
+
merchant: "merchant",
|
|
854
|
+
merchantFeature: "merchantFeature",
|
|
855
|
+
product: "product",
|
|
856
|
+
user: "user",
|
|
857
|
+
applicationLogo: "applicaionLogo",
|
|
858
|
+
applicationImages: "applicaionImages"
|
|
859
|
+
});
|
|
860
|
+
/** How uploads are addressed (app route and Shopwave uploader). */
|
|
861
|
+
const SHOPWAVE_UPLOAD = Object.freeze({
|
|
862
|
+
route: "upload",
|
|
863
|
+
upstream: "uploader",
|
|
864
|
+
kindHeader: "contentType",
|
|
865
|
+
fileField: "file",
|
|
866
|
+
kindField: "kind"
|
|
867
|
+
});
|
|
868
|
+
function isUploadKind(value) {
|
|
869
|
+
return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
//#endregion
|
|
873
|
+
//#region src/core/imageFit.ts
|
|
874
|
+
/**
|
|
875
|
+
* The centred part of a `srcWidth × srcHeight` image with the aspect ratio of
|
|
876
|
+
* `dstWidth × dstHeight` (like CSS `object-fit: cover`).
|
|
877
|
+
*/
|
|
878
|
+
function coverCrop(srcWidth, srcHeight, dstWidth, dstHeight) {
|
|
879
|
+
if (![
|
|
880
|
+
srcWidth,
|
|
881
|
+
srcHeight,
|
|
882
|
+
dstWidth,
|
|
883
|
+
dstHeight
|
|
884
|
+
].every((n) => Number.isFinite(n) && n > 0)) throw new Error("coverCrop: sizes must be positive numbers");
|
|
885
|
+
const srcRatio = srcWidth / srcHeight;
|
|
886
|
+
const dstRatio = dstWidth / dstHeight;
|
|
887
|
+
if (srcRatio > dstRatio) {
|
|
888
|
+
const sw = Math.round(srcHeight * dstRatio);
|
|
889
|
+
return {
|
|
890
|
+
sx: Math.round((srcWidth - sw) / 2),
|
|
891
|
+
sy: 0,
|
|
892
|
+
sw,
|
|
893
|
+
sh: srcHeight
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const sh = Math.round(srcWidth / dstRatio);
|
|
897
|
+
return {
|
|
898
|
+
sx: 0,
|
|
899
|
+
sy: Math.round((srcHeight - sh) / 2),
|
|
900
|
+
sw: srcWidth,
|
|
901
|
+
sh
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function requireBrowserImages(fn) {
|
|
905
|
+
if (typeof createImageBitmap !== "function") throw new Error(`${fn} needs a browser (createImageBitmap is not available)`);
|
|
906
|
+
}
|
|
907
|
+
/** Reads an image file's pixel size (browser only). */
|
|
908
|
+
async function readImageSize(file) {
|
|
909
|
+
requireBrowserImages("readImageSize");
|
|
910
|
+
const bitmap = await createImageBitmap(file);
|
|
911
|
+
try {
|
|
912
|
+
return {
|
|
913
|
+
width: bitmap.width,
|
|
914
|
+
height: bitmap.height
|
|
915
|
+
};
|
|
916
|
+
} finally {
|
|
917
|
+
bitmap.close?.();
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Returns the image at exactly `width × height`: scaled and centre-cropped
|
|
922
|
+
* (never stretched). Browser only. The result is a `File` named like the
|
|
923
|
+
* input, with the output type's extension.
|
|
924
|
+
*/
|
|
925
|
+
async function fitImageToSize(file, width, height, options = {}) {
|
|
926
|
+
requireBrowserImages("fitImageToSize");
|
|
927
|
+
const { type = "image/png", quality, keepIfExact = true } = options;
|
|
928
|
+
const name = file.name || "image";
|
|
929
|
+
const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
|
|
930
|
+
const outName = `${name.replace(/\.[^.]+$/, "")}.${ext}`;
|
|
931
|
+
const bitmap = await createImageBitmap(file);
|
|
932
|
+
try {
|
|
933
|
+
if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) return file instanceof File ? file : new File([file], outName, { type });
|
|
934
|
+
const { sx, sy, sw, sh } = coverCrop(bitmap.width, bitmap.height, width, height);
|
|
935
|
+
let blob;
|
|
936
|
+
if (typeof OffscreenCanvas === "function") {
|
|
937
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
938
|
+
const ctx = canvas.getContext("2d");
|
|
939
|
+
if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
|
|
940
|
+
ctx.imageSmoothingQuality = "high";
|
|
941
|
+
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
942
|
+
blob = await canvas.convertToBlob({
|
|
943
|
+
type,
|
|
944
|
+
quality
|
|
945
|
+
});
|
|
946
|
+
} else {
|
|
947
|
+
const canvas = document.createElement("canvas");
|
|
948
|
+
canvas.width = width;
|
|
949
|
+
canvas.height = height;
|
|
950
|
+
const ctx = canvas.getContext("2d");
|
|
951
|
+
if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
|
|
952
|
+
ctx.imageSmoothingQuality = "high";
|
|
953
|
+
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
954
|
+
blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error("fitImageToSize: encoding failed")), type, quality));
|
|
955
|
+
}
|
|
956
|
+
return new File([blob], outName, { type });
|
|
957
|
+
} finally {
|
|
958
|
+
bitmap.close?.();
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
//#endregion
|
|
963
|
+
//#region src/core/upload.ts
|
|
964
|
+
/**
|
|
965
|
+
* HTTPS host that serves the Shopwave CDN's images. The API returns
|
|
966
|
+
* `http://static.merchantstack.com/images/…`, which has no HTTPS endpoint
|
|
967
|
+
* (checked Sep 2026), so pages served over HTTPS can't show it.
|
|
968
|
+
*/
|
|
969
|
+
const SHOPWAVE_IMAGE_BASE_URL = "https://s3-eu-west-1.amazonaws.com/static.merchantstack.com/images";
|
|
970
|
+
const STATIC_HOST = /^https?:\/\/static\.merchantstack\.com\/images\//i;
|
|
971
|
+
/**
|
|
972
|
+
* Turns an image path from the API into a URL a page can load over HTTPS:
|
|
973
|
+
* `http://static.merchantstack.com/images/x.png` → `<base>/x.png`, and a
|
|
974
|
+
* relative path (`merchant/5644/logo/x.png`) → `<base>/merchant/5644/logo/x.png`.
|
|
975
|
+
* Other absolute URLs are returned unchanged; empty input gives `""`.
|
|
976
|
+
*/
|
|
977
|
+
function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
|
|
978
|
+
if (!path) return "";
|
|
979
|
+
const root = base.replace(/\/+$/, "");
|
|
980
|
+
if (STATIC_HOST.test(path)) return `${root}/${path.replace(STATIC_HOST, "")}`;
|
|
981
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith("//")) return path;
|
|
982
|
+
return `${root}/${path.replace(/^\/+/, "")}`;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Uploads one image to the Shopwave CDN through the app's `/api/upload` route
|
|
986
|
+
* (`createShopwaveApi(...).upload()`), and returns its id and URL.
|
|
987
|
+
*
|
|
988
|
+
* The file only lands on the CDN (under `…/images/<kind>/<merchantId>/…`); a
|
|
989
|
+
* record has to point at it to use it. Live API (Sep 2026): the upload answers
|
|
990
|
+
* 201 `{ fileName, path }`.
|
|
991
|
+
*/
|
|
992
|
+
async function uploadImage(file, params, options = {}) {
|
|
993
|
+
if (!file || typeof file.size !== "number") throw new Error("uploadImage: file must be a File or Blob");
|
|
994
|
+
if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind "${params?.kind}"`);
|
|
995
|
+
const name = params.fileName ?? file.name ?? "upload";
|
|
996
|
+
const form = new FormData();
|
|
997
|
+
form.append(SHOPWAVE_UPLOAD.fileField, file, name);
|
|
998
|
+
form.append(SHOPWAVE_UPLOAD.kindField, params.kind);
|
|
999
|
+
const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, {
|
|
1000
|
+
method: "POST",
|
|
1001
|
+
body: form
|
|
1002
|
+
}, options);
|
|
1003
|
+
const body = raw;
|
|
1004
|
+
assertNoApiErrors(body, status);
|
|
1005
|
+
const id = typeof body?.fileName === "string" ? body.fileName : "";
|
|
1006
|
+
const path = typeof body?.path === "string" ? body.path : "";
|
|
1007
|
+
if (!id) throw new ShopwaveApiError(status, `Shopwave API error (${status}): the upload response has no fileName`, { body });
|
|
1008
|
+
return {
|
|
1009
|
+
id,
|
|
1010
|
+
url: getImageUrl(path),
|
|
1011
|
+
path
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
//#endregion
|
|
1016
|
+
//#region src/core/merchantImages.ts
|
|
1017
|
+
/** The three image slots on the merchant settings page. */
|
|
1018
|
+
const MERCHANT_IMAGE_SLOT_KEYS = [
|
|
1019
|
+
"receipt",
|
|
1020
|
+
"square",
|
|
1021
|
+
"featured"
|
|
1022
|
+
];
|
|
1023
|
+
/**
|
|
1024
|
+
* Where each slot lives, its upload kind and its exact size (from adminV1's
|
|
1025
|
+
* merchant settings page).
|
|
1026
|
+
*/
|
|
1027
|
+
const MERCHANT_IMAGE_SLOTS = Object.freeze({
|
|
1028
|
+
receipt: {
|
|
1029
|
+
field: "logo.receipt",
|
|
1030
|
+
uploadKind: "merchant",
|
|
1031
|
+
width: 576,
|
|
1032
|
+
height: 325
|
|
1033
|
+
},
|
|
1034
|
+
square: {
|
|
1035
|
+
field: "logo.square",
|
|
1036
|
+
uploadKind: "merchant",
|
|
1037
|
+
width: 1e3,
|
|
1038
|
+
height: 1e3
|
|
1039
|
+
},
|
|
1040
|
+
featured: {
|
|
1041
|
+
field: "feature[0]",
|
|
1042
|
+
uploadKind: "merchantFeature",
|
|
1043
|
+
width: 1200,
|
|
1044
|
+
height: 600
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
/**
|
|
1048
|
+
* The file name in an image value: `"http://…/logo/abc.png"` → `"abc.png"`,
|
|
1049
|
+
* `"abc.png"` → `"abc.png"`. Query strings and fragments are ignored.
|
|
1050
|
+
* Returns `null` for empty or non-string values.
|
|
1051
|
+
*/
|
|
1052
|
+
function imageFileName(value) {
|
|
1053
|
+
if (typeof value !== "string") return null;
|
|
1054
|
+
const last = value.trim().replace(/[?#].*$/, "").split("/").pop() ?? "";
|
|
1055
|
+
return last ? decodeURIComponent(last) : null;
|
|
1056
|
+
}
|
|
1057
|
+
/** The stored value for a slot (a URL when read from the API; a file name right after an upload). */
|
|
1058
|
+
function getMerchantImage(imageIds, slot) {
|
|
1059
|
+
if (!imageIds || typeof imageIds !== "object") return null;
|
|
1060
|
+
let value;
|
|
1061
|
+
if (slot === "featured") value = Array.isArray(imageIds.feature) ? imageIds.feature[0] : void 0;
|
|
1062
|
+
else {
|
|
1063
|
+
const logo = imageIds.logo;
|
|
1064
|
+
value = logo && typeof logo === "object" ? logo[slot] : void 0;
|
|
1065
|
+
}
|
|
1066
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* An HTTPS URL for a slot's stored image, or `""` when the slot is empty or
|
|
1070
|
+
* holds only a file name (a fresh upload not yet saved; use the upload's `url`).
|
|
1071
|
+
*/
|
|
1072
|
+
function getMerchantImageUrl(imageIds, slot, base) {
|
|
1073
|
+
const value = getMerchantImage(imageIds, slot);
|
|
1074
|
+
if (!value || !value.includes("/")) return "";
|
|
1075
|
+
return getImageUrl(value, base);
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Returns a copy of `imageIds` with one slot set to `fileName` (or cleared
|
|
1079
|
+
* with `null`). Other slots and unknown keys are kept; an empty `logo` or
|
|
1080
|
+
* `feature` is removed.
|
|
1081
|
+
*/
|
|
1082
|
+
function setMerchantImage(imageIds, slot, fileName) {
|
|
1083
|
+
const next = { ...imageIds && typeof imageIds === "object" ? imageIds : {} };
|
|
1084
|
+
if (slot === "featured") {
|
|
1085
|
+
const rest = Array.isArray(next.feature) ? next.feature.slice(1) : [];
|
|
1086
|
+
const feature = fileName ? [fileName, ...rest] : rest;
|
|
1087
|
+
if (feature.length > 0) next.feature = feature;
|
|
1088
|
+
else delete next.feature;
|
|
1089
|
+
} else {
|
|
1090
|
+
const logo = { ...next.logo && typeof next.logo === "object" ? next.logo : {} };
|
|
1091
|
+
if (fileName) logo[slot] = fileName;
|
|
1092
|
+
else delete logo[slot];
|
|
1093
|
+
if (Object.keys(logo).length > 0) next.logo = logo;
|
|
1094
|
+
else delete next.logo;
|
|
1095
|
+
}
|
|
1096
|
+
return next;
|
|
1097
|
+
}
|
|
1098
|
+
function toStored(value) {
|
|
1099
|
+
if (typeof value === "string") return imageFileName(value) ?? void 0;
|
|
1100
|
+
if (Array.isArray(value)) {
|
|
1101
|
+
const items = value.map(toStored).filter((v) => v !== void 0 && v !== null && v !== "");
|
|
1102
|
+
return items.length > 0 ? items : void 0;
|
|
1103
|
+
}
|
|
1104
|
+
if (value && typeof value === "object") {
|
|
1105
|
+
const out = {};
|
|
1106
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1107
|
+
const stored = toStored(v);
|
|
1108
|
+
if (stored !== void 0 && stored !== null && stored !== "") out[k] = stored;
|
|
1109
|
+
}
|
|
1110
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* The `imageIds` to send on a save: every URL is turned back into its file
|
|
1115
|
+
* name (what adminV1 does with images you didn't change) and empty values
|
|
1116
|
+
* are dropped. No images gives `{}`.
|
|
1117
|
+
*/
|
|
1118
|
+
function toStoredImageIds(imageIds) {
|
|
1119
|
+
return toStored(imageIds) ?? {};
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Uploads an image for a merchant slot: fits it to the slot's size, uploads
|
|
1123
|
+
* it with the slot's kind, and returns `{ id, url, path }`. Put `id` into
|
|
1124
|
+
* `imageIds` with {@link setMerchantImage} and save with `updateMerchant`.
|
|
1125
|
+
*/
|
|
1126
|
+
async function uploadMerchantImage(file, slot, options = {}) {
|
|
1127
|
+
const def = MERCHANT_IMAGE_SLOTS[slot];
|
|
1128
|
+
if (!def) throw new Error(`uploadMerchantImage: unknown slot "${slot}"`);
|
|
1129
|
+
const { fit = true, fitOptions,...requestOptions } = options;
|
|
1130
|
+
return uploadImage(fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file, { kind: def.uploadKind }, requestOptions);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
//#endregion
|
|
1134
|
+
//#region src/core/merchant.ts
|
|
1135
|
+
const MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;
|
|
1136
|
+
/** Colour keys inside `colours.primary`, in display order. */
|
|
1137
|
+
const MERCHANT_COLOUR_KEYS = [
|
|
1138
|
+
"main",
|
|
1139
|
+
"highlight",
|
|
1140
|
+
"contrast"
|
|
1141
|
+
];
|
|
1142
|
+
/** Flat link fields a form edits, in display order. */
|
|
1143
|
+
const MERCHANT_LINK_KEYS = [
|
|
1144
|
+
"website",
|
|
1145
|
+
"twitter",
|
|
1146
|
+
"facebook",
|
|
1147
|
+
"instagram"
|
|
1148
|
+
];
|
|
1149
|
+
/** Where each flat link field lives in `merchant.links`. */
|
|
1150
|
+
const MERCHANT_LINK_PATHS = Object.freeze({
|
|
1151
|
+
website: ["website", "home"],
|
|
1152
|
+
twitter: ["social", "twitter"],
|
|
1153
|
+
facebook: ["social", "facebook"],
|
|
1154
|
+
instagram: ["social", "instagram"]
|
|
1155
|
+
});
|
|
1156
|
+
function trimmed(value) {
|
|
1157
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Reads `merchant.links` as flat fields (`{ website, twitter, facebook,
|
|
1161
|
+
* instagram }`), whatever shape it was saved in:
|
|
1162
|
+
* - the nested shape adminV1 saves (`{ website: { home }, social: { … } }`);
|
|
1163
|
+
* - flat keys (`{ website: "…" }`, what SDK 0.4.0 pre-releases sent);
|
|
1164
|
+
* - an array in {@link MERCHANT_LINK_KEYS} order (older AdminUI versions).
|
|
1165
|
+
* Empty values are dropped.
|
|
1166
|
+
*/
|
|
1167
|
+
function merchantLinksToForm(links) {
|
|
1168
|
+
const out = {};
|
|
1169
|
+
if (Array.isArray(links)) {
|
|
1170
|
+
links.forEach((value, i) => {
|
|
1171
|
+
const key = MERCHANT_LINK_KEYS[i];
|
|
1172
|
+
const v = trimmed(value);
|
|
1173
|
+
if (key && v) out[key] = v;
|
|
1174
|
+
});
|
|
1175
|
+
return out;
|
|
1176
|
+
}
|
|
1177
|
+
if (!links || typeof links !== "object") return out;
|
|
1178
|
+
const obj = links;
|
|
1179
|
+
for (const key of MERCHANT_LINK_KEYS) {
|
|
1180
|
+
const [group, field] = MERCHANT_LINK_PATHS[key];
|
|
1181
|
+
const nested = obj[group];
|
|
1182
|
+
const v = nested && typeof nested === "object" && !Array.isArray(nested) ? trimmed(nested[field]) : trimmed(obj[key]);
|
|
1183
|
+
if (v) out[key] = v;
|
|
1184
|
+
}
|
|
1185
|
+
return out;
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Builds the stored `links` object from flat fields. Empty values are left
|
|
1189
|
+
* out, and so is a group with nothing in it; no links gives `{}`.
|
|
1190
|
+
*/
|
|
1191
|
+
function merchantLinksFromForm(form) {
|
|
1192
|
+
const out = {};
|
|
1193
|
+
for (const key of MERCHANT_LINK_KEYS) {
|
|
1194
|
+
const v = trimmed(form[key]);
|
|
1195
|
+
if (!v) continue;
|
|
1196
|
+
const [group, field] = MERCHANT_LINK_PATHS[key];
|
|
1197
|
+
(out[group] ??= {})[field] = v;
|
|
1198
|
+
}
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
/** Reads any stored or older `links` value into the nested shape adminV1 saves. */
|
|
1202
|
+
function normalizeMerchantLinks(links) {
|
|
1203
|
+
return merchantLinksFromForm(merchantLinksToForm(links));
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Object fields of the merchant. The API keeps one you leave out, and
|
|
1207
|
+
* replaces it as a whole when you send it.
|
|
1208
|
+
*/
|
|
1209
|
+
const MERCHANT_OBJECT_FIELDS = [
|
|
1210
|
+
"colours",
|
|
1211
|
+
"links",
|
|
1212
|
+
"imageIds"
|
|
1213
|
+
];
|
|
1214
|
+
/** Fields the API sets itself; `updateMerchant` never sends them back. */
|
|
1215
|
+
const MERCHANT_READ_ONLY_FIELDS = ["createdDate", "modifiedDate"];
|
|
1216
|
+
/**
|
|
1217
|
+
* Longest values the API stores. Longer values are cut off silently
|
|
1218
|
+
* (live API, Sep 2026), so `updateMerchant` rejects them instead.
|
|
1219
|
+
* `vatNumber` fits a UK VAT number without the `GB` prefix.
|
|
1220
|
+
*/
|
|
1221
|
+
const MERCHANT_FIELD_MAX_LENGTH = Object.freeze({
|
|
1222
|
+
companyNumber: 11,
|
|
1223
|
+
vatNumber: 9
|
|
1224
|
+
});
|
|
1225
|
+
/** Returns `"<field> is longer than <n> characters"` messages for over-long fields. */
|
|
1226
|
+
function validateMerchant(merchant) {
|
|
1227
|
+
const problems = [];
|
|
1228
|
+
for (const [field, max] of Object.entries(MERCHANT_FIELD_MAX_LENGTH)) {
|
|
1229
|
+
const value = merchant[field];
|
|
1230
|
+
if (typeof value === "string" && value.length > max) problems.push(`${field} is longer than ${max} characters`);
|
|
1231
|
+
}
|
|
1232
|
+
return problems;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Reads the logged-in user's merchant (a user has exactly one), or `null`
|
|
1236
|
+
* when the API returns none.
|
|
1237
|
+
*/
|
|
1238
|
+
async function fetchMerchant(options = {}) {
|
|
1239
|
+
const json = await apiGet(MERCHANT_PATH, {}, options);
|
|
1240
|
+
assertNoApiErrors(json, 200);
|
|
1241
|
+
return json?.merchant ?? null;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Updates the merchant and returns it as stored.
|
|
1245
|
+
*
|
|
1246
|
+
* Shopwave's `POST /merchant` replaces the whole record: a scalar field you
|
|
1247
|
+
* leave out (`companyNumber`, `vatNumber`, `note`, …) is set to null, while
|
|
1248
|
+
* an object field you leave out (`colours`, `links`, `imageIds`) is kept and
|
|
1249
|
+
* one you send replaces the stored one; sending `{}` clears it (live API,
|
|
1250
|
+
* Sep 2026). So by default the
|
|
1251
|
+
* current merchant is read first and the patch is merged over it.
|
|
1252
|
+
*
|
|
1253
|
+
* `imageIds` is always sent as file names ({@link toStoredImageIds}), so you
|
|
1254
|
+
* can pass back what you read. See `MerchantImageIds` for the shape.
|
|
1255
|
+
*
|
|
1256
|
+
* The API answers 205 with an empty body, so the merchant is read again after
|
|
1257
|
+
* the save and that is what's returned (it shows exactly what was kept).
|
|
1258
|
+
* Throws before sending when a value is longer than the API stores
|
|
1259
|
+
* ({@link MERCHANT_FIELD_MAX_LENGTH}).
|
|
1260
|
+
*/
|
|
1261
|
+
async function updateMerchant(patch, options = {}) {
|
|
1262
|
+
const { merge = true,...requestOptions } = options;
|
|
1263
|
+
let merchant = { ...patch };
|
|
1264
|
+
if (merge) {
|
|
1265
|
+
const current = await fetchMerchant(requestOptions);
|
|
1266
|
+
if (!current) throw new ShopwaveApiError(404, "Shopwave API error (404): no merchant to update");
|
|
1267
|
+
if (patch.id != null && String(patch.id) !== String(current.id)) throw new Error(`Merchant id ${patch.id} is not the logged-in merchant (${current.id})`);
|
|
1268
|
+
merchant = {
|
|
1269
|
+
...current,
|
|
1270
|
+
...patch,
|
|
1271
|
+
id: current.id
|
|
1272
|
+
};
|
|
1273
|
+
for (const field of MERCHANT_OBJECT_FIELDS) if (!(field in patch)) delete merchant[field];
|
|
1274
|
+
}
|
|
1275
|
+
if (merchant.imageIds && typeof merchant.imageIds === "object") merchant.imageIds = toStoredImageIds(merchant.imageIds);
|
|
1276
|
+
if (merchant.id == null || merchant.id === "") throw new Error("merchant id is missing");
|
|
1277
|
+
for (const field of MERCHANT_READ_ONLY_FIELDS) delete merchant[field];
|
|
1278
|
+
const problems = validateMerchant(merchant);
|
|
1279
|
+
if (problems.length > 0) throw new Error(`Invalid merchant: ${problems.join("; ")}`);
|
|
1280
|
+
const { status, body: raw } = await apiRequest(MERCHANT_PATH, {
|
|
1281
|
+
method: "PUT",
|
|
1282
|
+
headers: { "Content-Type": "application/json" },
|
|
1283
|
+
body: JSON.stringify({ merchant })
|
|
1284
|
+
}, requestOptions);
|
|
1285
|
+
const body = raw;
|
|
1286
|
+
assertNoApiErrors(body, status);
|
|
1287
|
+
const echoed = body?.merchant && typeof body.merchant === "object" ? body.merchant : null;
|
|
1288
|
+
if (echoed) return {
|
|
1289
|
+
...merchant,
|
|
1290
|
+
...echoed,
|
|
1291
|
+
id: Number(echoed.id ?? merchant.id)
|
|
1292
|
+
};
|
|
1293
|
+
return await fetchMerchant(requestOptions) ?? {
|
|
1294
|
+
...merchant,
|
|
1295
|
+
id: Number(merchant.id)
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
//#endregion
|
|
1300
|
+
//#region src/core/user.ts
|
|
1301
|
+
/** Reads the logged-in user, or `null` when the API returns none. */
|
|
1302
|
+
async function fetchUser(options = {}) {
|
|
1303
|
+
const json = await apiGet(`/api/${SHOPWAVE_RESOURCES.user.route}`, {}, options);
|
|
1304
|
+
assertNoApiErrors(json, 200);
|
|
1305
|
+
return json?.user ?? null;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
737
1308
|
//#endregion
|
|
738
1309
|
//#region src/hooks/shared.ts
|
|
739
1310
|
function toMessage(e) {
|
|
@@ -1091,6 +1662,85 @@ function useSession(options) {
|
|
|
1091
1662
|
}), [options?.baseUrl]);
|
|
1092
1663
|
}
|
|
1093
1664
|
|
|
1665
|
+
//#endregion
|
|
1666
|
+
//#region src/hooks/useMerchant.ts
|
|
1667
|
+
/**
|
|
1668
|
+
* The logged-in user's merchant, fetched on mount. `data` is `null` while
|
|
1669
|
+
* loading or when the API has no merchant.
|
|
1670
|
+
*
|
|
1671
|
+
* @example
|
|
1672
|
+
* const { data: merchant, loading, refetch } = useMerchant();
|
|
1673
|
+
*/
|
|
1674
|
+
function useMerchant(options) {
|
|
1675
|
+
return useQuery((signal) => fetchMerchant({
|
|
1676
|
+
...options,
|
|
1677
|
+
signal
|
|
1678
|
+
}), [options?.baseUrl, options?.token]);
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Manually-triggered merchant update (see `updateMerchant`: the patch is merged
|
|
1682
|
+
* over the current merchant by default). Resolves to the saved merchant, or
|
|
1683
|
+
* `null` on error (then `error` / `errorStatus` are set).
|
|
1684
|
+
*
|
|
1685
|
+
* @example
|
|
1686
|
+
* const { mutate: saveMerchant, loading: saving } = useUpdateMerchant();
|
|
1687
|
+
* const saved = await saveMerchant({ vatNumber: "GB123" });
|
|
1688
|
+
*/
|
|
1689
|
+
function useUpdateMerchant(options) {
|
|
1690
|
+
return useMutation((patch) => updateMerchant(patch, options), [
|
|
1691
|
+
options?.baseUrl,
|
|
1692
|
+
options?.token,
|
|
1693
|
+
options?.merge
|
|
1694
|
+
]);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
//#endregion
|
|
1698
|
+
//#region src/hooks/useUser.ts
|
|
1699
|
+
/**
|
|
1700
|
+
* The logged-in user, fetched on mount. `errorStatus` is 401 when logged out.
|
|
1701
|
+
*
|
|
1702
|
+
* @example
|
|
1703
|
+
* const { data: user } = useUser();
|
|
1704
|
+
*/
|
|
1705
|
+
function useUser(options) {
|
|
1706
|
+
const enabled = options?.enabled ?? true;
|
|
1707
|
+
return useQuery((signal) => fetchUser({
|
|
1708
|
+
...options,
|
|
1709
|
+
signal
|
|
1710
|
+
}), [options?.baseUrl, options?.token], enabled);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
//#endregion
|
|
1714
|
+
//#region src/hooks/useUploadImage.ts
|
|
1715
|
+
/**
|
|
1716
|
+
* Manually-triggered image upload. Resolves to `{ id, url }`, or `null` on
|
|
1717
|
+
* error (then `error` / `errorStatus` are set).
|
|
1718
|
+
*
|
|
1719
|
+
* @example
|
|
1720
|
+
* const { mutate: upload, loading: uploading } = useUploadImage();
|
|
1721
|
+
* const image = await upload(file, { kind: "merchant" });
|
|
1722
|
+
*/
|
|
1723
|
+
function useUploadImage(options) {
|
|
1724
|
+
return useMutation((file, params) => uploadImage(file, params, options), [options?.baseUrl, options?.token]);
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Manually-triggered upload for a merchant image slot (`receipt`, `square`,
|
|
1728
|
+
* `featured`): fits the image to the slot's exact size, uploads it with the
|
|
1729
|
+
* slot's kind, and resolves to `{ id, url }`. Put `id` into `imageIds` with
|
|
1730
|
+
* `setMerchantImage`.
|
|
1731
|
+
*
|
|
1732
|
+
* @example
|
|
1733
|
+
* const { mutate: upload } = useUploadMerchantImage();
|
|
1734
|
+
* const image = await upload(file, "square");
|
|
1735
|
+
*/
|
|
1736
|
+
function useUploadMerchantImage(options) {
|
|
1737
|
+
return useMutation((file, slot) => uploadMerchantImage(file, slot, options), [
|
|
1738
|
+
options?.baseUrl,
|
|
1739
|
+
options?.token,
|
|
1740
|
+
options?.fit
|
|
1741
|
+
]);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1094
1744
|
//#endregion
|
|
1095
1745
|
//#region src/hooks/useBasketReport.ts
|
|
1096
1746
|
/**
|
|
@@ -1197,9 +1847,21 @@ function useBasketReport(options) {
|
|
|
1197
1847
|
//#endregion
|
|
1198
1848
|
exports.DEFAULT_BASKET_FILTERS = DEFAULT_BASKET_FILTERS;
|
|
1199
1849
|
exports.DEFAULT_PAGINATION = DEFAULT_PAGINATION;
|
|
1850
|
+
exports.MERCHANT_COLOUR_KEYS = MERCHANT_COLOUR_KEYS;
|
|
1851
|
+
exports.MERCHANT_FIELD_MAX_LENGTH = MERCHANT_FIELD_MAX_LENGTH;
|
|
1852
|
+
exports.MERCHANT_IMAGE_SLOTS = MERCHANT_IMAGE_SLOTS;
|
|
1853
|
+
exports.MERCHANT_IMAGE_SLOT_KEYS = MERCHANT_IMAGE_SLOT_KEYS;
|
|
1854
|
+
exports.MERCHANT_LINK_KEYS = MERCHANT_LINK_KEYS;
|
|
1855
|
+
exports.MERCHANT_LINK_PATHS = MERCHANT_LINK_PATHS;
|
|
1856
|
+
exports.MERCHANT_OBJECT_FIELDS = MERCHANT_OBJECT_FIELDS;
|
|
1857
|
+
exports.MERCHANT_READ_ONLY_FIELDS = MERCHANT_READ_ONLY_FIELDS;
|
|
1200
1858
|
exports.PromotionRuleSet = PromotionRuleSet;
|
|
1201
1859
|
exports.SHOPWAVE_ENTITIES = SHOPWAVE_ENTITIES;
|
|
1860
|
+
exports.SHOPWAVE_IMAGE_BASE_URL = SHOPWAVE_IMAGE_BASE_URL;
|
|
1861
|
+
exports.SHOPWAVE_RESOURCES = SHOPWAVE_RESOURCES;
|
|
1862
|
+
exports.SHOPWAVE_UPLOAD = SHOPWAVE_UPLOAD;
|
|
1202
1863
|
exports.ShopwaveApiError = ShopwaveApiError;
|
|
1864
|
+
exports.UPLOAD_KINDS = UPLOAD_KINDS;
|
|
1203
1865
|
exports.apiDelete = apiDelete;
|
|
1204
1866
|
exports.apiGet = apiGet;
|
|
1205
1867
|
exports.apiRequest = apiRequest;
|
|
@@ -1212,6 +1874,7 @@ exports.collectConsumerIds = collectConsumerIds;
|
|
|
1212
1874
|
exports.collectProductIds = collectProductIds;
|
|
1213
1875
|
exports.combineBasketRows = combineBasketRows;
|
|
1214
1876
|
exports.computeBasketSummary = computeBasketSummary;
|
|
1877
|
+
exports.coverCrop = coverCrop;
|
|
1215
1878
|
exports.deleteCategory = deleteCategory;
|
|
1216
1879
|
exports.deleteEmployee = deleteEmployee;
|
|
1217
1880
|
exports.deleteEntity = deleteEntity;
|
|
@@ -1226,6 +1889,7 @@ exports.fetchConsumers = fetchConsumers;
|
|
|
1226
1889
|
exports.fetchEmployee = fetchEmployee;
|
|
1227
1890
|
exports.fetchEmployees = fetchEmployees;
|
|
1228
1891
|
exports.fetchEntityById = fetchEntityById;
|
|
1892
|
+
exports.fetchMerchant = fetchMerchant;
|
|
1229
1893
|
exports.fetchProduct = fetchProduct;
|
|
1230
1894
|
exports.fetchProducts = fetchProducts;
|
|
1231
1895
|
exports.fetchProductsMap = fetchProductsMap;
|
|
@@ -1235,13 +1899,26 @@ exports.fetchReport = fetchReport;
|
|
|
1235
1899
|
exports.fetchSession = fetchSession;
|
|
1236
1900
|
exports.fetchStore = fetchStore;
|
|
1237
1901
|
exports.fetchStores = fetchStores;
|
|
1902
|
+
exports.fetchUser = fetchUser;
|
|
1903
|
+
exports.findRepricedInstances = findRepricedInstances;
|
|
1904
|
+
exports.fitImageToSize = fitImageToSize;
|
|
1238
1905
|
exports.formatDateForQuery = formatDateForQuery;
|
|
1239
1906
|
exports.getApiErrorMap = getApiErrorMap;
|
|
1240
1907
|
exports.getApiErrors = getApiErrors;
|
|
1908
|
+
exports.getImageUrl = getImageUrl;
|
|
1909
|
+
exports.getMerchantImage = getMerchantImage;
|
|
1910
|
+
exports.getMerchantImageUrl = getMerchantImageUrl;
|
|
1911
|
+
exports.imageFileName = imageFileName;
|
|
1912
|
+
exports.isSameInstancePrice = isSameInstancePrice;
|
|
1913
|
+
exports.isUploadKind = isUploadKind;
|
|
1241
1914
|
exports.loginPath = loginPath;
|
|
1242
1915
|
exports.logout = logout;
|
|
1243
1916
|
exports.logoutPath = logoutPath;
|
|
1917
|
+
exports.merchantLinksFromForm = merchantLinksFromForm;
|
|
1918
|
+
exports.merchantLinksToForm = merchantLinksToForm;
|
|
1919
|
+
exports.normalizeMerchantLinks = normalizeMerchantLinks;
|
|
1244
1920
|
exports.parseBasketReportData = parseBasketReportData;
|
|
1921
|
+
exports.readImageSize = readImageSize;
|
|
1245
1922
|
exports.rolesById = rolesById;
|
|
1246
1923
|
exports.saveCategory = saveCategory;
|
|
1247
1924
|
exports.saveEmployee = saveEmployee;
|
|
@@ -1250,19 +1927,31 @@ exports.saveEntity = saveEntity;
|
|
|
1250
1927
|
exports.saveProduct = saveProduct;
|
|
1251
1928
|
exports.savePromotion = savePromotion;
|
|
1252
1929
|
exports.saveStore = saveStore;
|
|
1930
|
+
exports.setMerchantImage = setMerchantImage;
|
|
1253
1931
|
exports.submitEntity = submitEntity;
|
|
1932
|
+
exports.toProductInstancesForSave = toProductInstancesForSave;
|
|
1254
1933
|
exports.toShopwaveApiError = toShopwaveApiError;
|
|
1934
|
+
exports.toStoredImageIds = toStoredImageIds;
|
|
1935
|
+
exports.updateMerchant = updateMerchant;
|
|
1936
|
+
exports.uploadImage = uploadImage;
|
|
1937
|
+
exports.uploadMerchantImage = uploadMerchantImage;
|
|
1255
1938
|
exports.useBasketReport = useBasketReport;
|
|
1256
1939
|
exports.useCategory = useCategory;
|
|
1257
1940
|
exports.useConsumer = useConsumer;
|
|
1258
1941
|
exports.useDelete = useDelete;
|
|
1259
1942
|
exports.useEmployee = useEmployee;
|
|
1260
1943
|
exports.useLogout = useLogout;
|
|
1944
|
+
exports.useMerchant = useMerchant;
|
|
1261
1945
|
exports.useProduct = useProduct;
|
|
1262
1946
|
exports.usePromotion = usePromotion;
|
|
1263
1947
|
exports.useReport = useReport;
|
|
1264
1948
|
exports.useSession = useSession;
|
|
1265
1949
|
exports.useStore = useStore;
|
|
1266
1950
|
exports.useSubmit = useSubmit;
|
|
1951
|
+
exports.useUpdateMerchant = useUpdateMerchant;
|
|
1952
|
+
exports.useUploadImage = useUploadImage;
|
|
1953
|
+
exports.useUploadMerchantImage = useUploadMerchantImage;
|
|
1954
|
+
exports.useUser = useUser;
|
|
1955
|
+
exports.validateMerchant = validateMerchant;
|
|
1267
1956
|
exports.withToken = withToken;
|
|
1268
1957
|
//# sourceMappingURL=index.cjs.map
|