react-shopwave-connect 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -734,6 +734,486 @@ function buildConsumerMap(consumers) {
734
734
  return map;
735
735
  }
736
736
 
737
+ //#endregion
738
+ //#region src/core/resources.ts
739
+ /**
740
+ * - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
741
+ * updates it (without an `id` the API would create a second merchant, so the
742
+ * route requires one).
743
+ * - user: `GET /user` (read-only here; the API's `POST /user` invites users and
744
+ * needs the admin scope).
745
+ */
746
+ const SHOPWAVE_RESOURCES = Object.freeze({
747
+ merchant: {
748
+ route: "merchant",
749
+ key: "merchant",
750
+ upstream: "merchant",
751
+ writable: true
752
+ },
753
+ user: {
754
+ route: "user",
755
+ key: "user",
756
+ upstream: "user",
757
+ writable: false
758
+ }
759
+ });
760
+ /** `UploadKind` → the API's `contentType` value. */
761
+ const UPLOAD_KINDS = Object.freeze({
762
+ merchant: "merchant",
763
+ merchantFeature: "merchantFeature",
764
+ product: "product",
765
+ user: "user",
766
+ applicationLogo: "applicaionLogo",
767
+ applicationImages: "applicaionImages"
768
+ });
769
+ /** How uploads are addressed (app route and Shopwave uploader). */
770
+ const SHOPWAVE_UPLOAD = Object.freeze({
771
+ route: "upload",
772
+ upstream: "uploader",
773
+ kindHeader: "contentType",
774
+ fileField: "file",
775
+ kindField: "kind"
776
+ });
777
+ function isUploadKind(value) {
778
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
779
+ }
780
+
781
+ //#endregion
782
+ //#region src/core/imageFit.ts
783
+ /**
784
+ * The centred part of a `srcWidth × srcHeight` image with the aspect ratio of
785
+ * `dstWidth × dstHeight` (like CSS `object-fit: cover`).
786
+ */
787
+ function coverCrop(srcWidth, srcHeight, dstWidth, dstHeight) {
788
+ if (![
789
+ srcWidth,
790
+ srcHeight,
791
+ dstWidth,
792
+ dstHeight
793
+ ].every((n) => Number.isFinite(n) && n > 0)) throw new Error("coverCrop: sizes must be positive numbers");
794
+ const srcRatio = srcWidth / srcHeight;
795
+ const dstRatio = dstWidth / dstHeight;
796
+ if (srcRatio > dstRatio) {
797
+ const sw = Math.round(srcHeight * dstRatio);
798
+ return {
799
+ sx: Math.round((srcWidth - sw) / 2),
800
+ sy: 0,
801
+ sw,
802
+ sh: srcHeight
803
+ };
804
+ }
805
+ const sh = Math.round(srcWidth / dstRatio);
806
+ return {
807
+ sx: 0,
808
+ sy: Math.round((srcHeight - sh) / 2),
809
+ sw: srcWidth,
810
+ sh
811
+ };
812
+ }
813
+ function requireBrowserImages(fn) {
814
+ if (typeof createImageBitmap !== "function") throw new Error(`${fn} needs a browser (createImageBitmap is not available)`);
815
+ }
816
+ /** Reads an image file's pixel size (browser only). */
817
+ async function readImageSize(file) {
818
+ requireBrowserImages("readImageSize");
819
+ const bitmap = await createImageBitmap(file);
820
+ try {
821
+ return {
822
+ width: bitmap.width,
823
+ height: bitmap.height
824
+ };
825
+ } finally {
826
+ bitmap.close?.();
827
+ }
828
+ }
829
+ /**
830
+ * Returns the image at exactly `width × height`: scaled and centre-cropped
831
+ * (never stretched). Browser only. The result is a `File` named like the
832
+ * input, with the output type's extension.
833
+ */
834
+ async function fitImageToSize(file, width, height, options = {}) {
835
+ requireBrowserImages("fitImageToSize");
836
+ const { type = "image/png", quality, keepIfExact = true } = options;
837
+ const name = file.name || "image";
838
+ const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
839
+ const outName = `${name.replace(/\.[^.]+$/, "")}.${ext}`;
840
+ const bitmap = await createImageBitmap(file);
841
+ try {
842
+ if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) return file instanceof File ? file : new File([file], outName, { type });
843
+ const { sx, sy, sw, sh } = coverCrop(bitmap.width, bitmap.height, width, height);
844
+ let blob;
845
+ if (typeof OffscreenCanvas === "function") {
846
+ const canvas = new OffscreenCanvas(width, height);
847
+ const ctx = canvas.getContext("2d");
848
+ if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
849
+ ctx.imageSmoothingQuality = "high";
850
+ ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
851
+ blob = await canvas.convertToBlob({
852
+ type,
853
+ quality
854
+ });
855
+ } else {
856
+ const canvas = document.createElement("canvas");
857
+ canvas.width = width;
858
+ canvas.height = height;
859
+ const ctx = canvas.getContext("2d");
860
+ if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
861
+ ctx.imageSmoothingQuality = "high";
862
+ ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
863
+ blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error("fitImageToSize: encoding failed")), type, quality));
864
+ }
865
+ return new File([blob], outName, { type });
866
+ } finally {
867
+ bitmap.close?.();
868
+ }
869
+ }
870
+
871
+ //#endregion
872
+ //#region src/core/upload.ts
873
+ /**
874
+ * HTTPS host that serves the Shopwave CDN's images. The API returns
875
+ * `http://static.merchantstack.com/images/…`, which has no HTTPS endpoint
876
+ * (checked Sep 2026), so pages served over HTTPS can't show it.
877
+ */
878
+ const SHOPWAVE_IMAGE_BASE_URL = "https://s3-eu-west-1.amazonaws.com/static.merchantstack.com/images";
879
+ const STATIC_HOST = /^https?:\/\/static\.merchantstack\.com\/images\//i;
880
+ /**
881
+ * Turns an image path from the API into a URL a page can load over HTTPS:
882
+ * `http://static.merchantstack.com/images/x.png` → `<base>/x.png`, and a
883
+ * relative path (`merchant/5644/logo/x.png`) → `<base>/merchant/5644/logo/x.png`.
884
+ * Other absolute URLs are returned unchanged; empty input gives `""`.
885
+ */
886
+ function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
887
+ if (!path) return "";
888
+ const root = base.replace(/\/+$/, "");
889
+ if (STATIC_HOST.test(path)) return `${root}/${path.replace(STATIC_HOST, "")}`;
890
+ if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith("//")) return path;
891
+ return `${root}/${path.replace(/^\/+/, "")}`;
892
+ }
893
+ /**
894
+ * Uploads one image to the Shopwave CDN through the app's `/api/upload` route
895
+ * (`createShopwaveApi(...).upload()`), and returns its id and URL.
896
+ *
897
+ * The file only lands on the CDN (under `…/images/<kind>/<merchantId>/…`); a
898
+ * record has to point at it to use it. Live API (Sep 2026): the upload answers
899
+ * 201 `{ fileName, path }`.
900
+ */
901
+ async function uploadImage(file, params, options = {}) {
902
+ if (!file || typeof file.size !== "number") throw new Error("uploadImage: file must be a File or Blob");
903
+ if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind "${params?.kind}"`);
904
+ const name = params.fileName ?? file.name ?? "upload";
905
+ const form = new FormData();
906
+ form.append(SHOPWAVE_UPLOAD.fileField, file, name);
907
+ form.append(SHOPWAVE_UPLOAD.kindField, params.kind);
908
+ const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, {
909
+ method: "POST",
910
+ body: form
911
+ }, options);
912
+ const body = raw;
913
+ assertNoApiErrors(body, status);
914
+ const id = typeof body?.fileName === "string" ? body.fileName : "";
915
+ const path = typeof body?.path === "string" ? body.path : "";
916
+ if (!id) throw new ShopwaveApiError(status, `Shopwave API error (${status}): the upload response has no fileName`, { body });
917
+ return {
918
+ id,
919
+ url: getImageUrl(path),
920
+ path
921
+ };
922
+ }
923
+
924
+ //#endregion
925
+ //#region src/core/merchantImages.ts
926
+ /** The three image slots on the merchant settings page. */
927
+ const MERCHANT_IMAGE_SLOT_KEYS = [
928
+ "receipt",
929
+ "square",
930
+ "featured"
931
+ ];
932
+ /**
933
+ * Where each slot lives, its upload kind and its exact size (from adminV1's
934
+ * merchant settings page).
935
+ */
936
+ const MERCHANT_IMAGE_SLOTS = Object.freeze({
937
+ receipt: {
938
+ field: "logo.receipt",
939
+ uploadKind: "merchant",
940
+ width: 576,
941
+ height: 325
942
+ },
943
+ square: {
944
+ field: "logo.square",
945
+ uploadKind: "merchant",
946
+ width: 1e3,
947
+ height: 1e3
948
+ },
949
+ featured: {
950
+ field: "feature[0]",
951
+ uploadKind: "merchantFeature",
952
+ width: 1200,
953
+ height: 600
954
+ }
955
+ });
956
+ /**
957
+ * The file name in an image value: `"http://…/logo/abc.png"` → `"abc.png"`,
958
+ * `"abc.png"` → `"abc.png"`. Query strings and fragments are ignored.
959
+ * Returns `null` for empty or non-string values.
960
+ */
961
+ function imageFileName(value) {
962
+ if (typeof value !== "string") return null;
963
+ const last = value.trim().replace(/[?#].*$/, "").split("/").pop() ?? "";
964
+ return last ? decodeURIComponent(last) : null;
965
+ }
966
+ /** The stored value for a slot (a URL when read from the API; a file name right after an upload). */
967
+ function getMerchantImage(imageIds, slot) {
968
+ if (!imageIds || typeof imageIds !== "object") return null;
969
+ let value;
970
+ if (slot === "featured") value = Array.isArray(imageIds.feature) ? imageIds.feature[0] : void 0;
971
+ else {
972
+ const logo = imageIds.logo;
973
+ value = logo && typeof logo === "object" ? logo[slot] : void 0;
974
+ }
975
+ return typeof value === "string" && value.trim() ? value.trim() : null;
976
+ }
977
+ /**
978
+ * An HTTPS URL for a slot's stored image, or `""` when the slot is empty or
979
+ * holds only a file name (a fresh upload not yet saved; use the upload's `url`).
980
+ */
981
+ function getMerchantImageUrl(imageIds, slot, base) {
982
+ const value = getMerchantImage(imageIds, slot);
983
+ if (!value || !value.includes("/")) return "";
984
+ return getImageUrl(value, base);
985
+ }
986
+ /**
987
+ * Returns a copy of `imageIds` with one slot set to `fileName` (or cleared
988
+ * with `null`). Other slots and unknown keys are kept; an empty `logo` or
989
+ * `feature` is removed.
990
+ */
991
+ function setMerchantImage(imageIds, slot, fileName) {
992
+ const next = { ...imageIds && typeof imageIds === "object" ? imageIds : {} };
993
+ if (slot === "featured") {
994
+ const rest = Array.isArray(next.feature) ? next.feature.slice(1) : [];
995
+ const feature = fileName ? [fileName, ...rest] : rest;
996
+ if (feature.length > 0) next.feature = feature;
997
+ else delete next.feature;
998
+ } else {
999
+ const logo = { ...next.logo && typeof next.logo === "object" ? next.logo : {} };
1000
+ if (fileName) logo[slot] = fileName;
1001
+ else delete logo[slot];
1002
+ if (Object.keys(logo).length > 0) next.logo = logo;
1003
+ else delete next.logo;
1004
+ }
1005
+ return next;
1006
+ }
1007
+ function toStored(value) {
1008
+ if (typeof value === "string") return imageFileName(value) ?? void 0;
1009
+ if (Array.isArray(value)) {
1010
+ const items = value.map(toStored).filter((v) => v !== void 0 && v !== null && v !== "");
1011
+ return items.length > 0 ? items : void 0;
1012
+ }
1013
+ if (value && typeof value === "object") {
1014
+ const out = {};
1015
+ for (const [k, v] of Object.entries(value)) {
1016
+ const stored = toStored(v);
1017
+ if (stored !== void 0 && stored !== null && stored !== "") out[k] = stored;
1018
+ }
1019
+ return Object.keys(out).length > 0 ? out : void 0;
1020
+ }
1021
+ }
1022
+ /**
1023
+ * The `imageIds` to send on a save: every URL is turned back into its file
1024
+ * name (what adminV1 does with images you didn't change) and empty values
1025
+ * are dropped. No images gives `{}`.
1026
+ */
1027
+ function toStoredImageIds(imageIds) {
1028
+ return toStored(imageIds) ?? {};
1029
+ }
1030
+ /**
1031
+ * Uploads an image for a merchant slot: fits it to the slot's size, uploads
1032
+ * it with the slot's kind, and returns `{ id, url, path }`. Put `id` into
1033
+ * `imageIds` with {@link setMerchantImage} and save with `updateMerchant`.
1034
+ */
1035
+ async function uploadMerchantImage(file, slot, options = {}) {
1036
+ const def = MERCHANT_IMAGE_SLOTS[slot];
1037
+ if (!def) throw new Error(`uploadMerchantImage: unknown slot "${slot}"`);
1038
+ const { fit = true, fitOptions,...requestOptions } = options;
1039
+ return uploadImage(fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file, { kind: def.uploadKind }, requestOptions);
1040
+ }
1041
+
1042
+ //#endregion
1043
+ //#region src/core/merchant.ts
1044
+ const MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;
1045
+ /** Colour keys inside `colours.primary`, in display order. */
1046
+ const MERCHANT_COLOUR_KEYS = [
1047
+ "main",
1048
+ "highlight",
1049
+ "contrast"
1050
+ ];
1051
+ /** Flat link fields a form edits, in display order. */
1052
+ const MERCHANT_LINK_KEYS = [
1053
+ "website",
1054
+ "twitter",
1055
+ "facebook",
1056
+ "instagram"
1057
+ ];
1058
+ /** Where each flat link field lives in `merchant.links`. */
1059
+ const MERCHANT_LINK_PATHS = Object.freeze({
1060
+ website: ["website", "home"],
1061
+ twitter: ["social", "twitter"],
1062
+ facebook: ["social", "facebook"],
1063
+ instagram: ["social", "instagram"]
1064
+ });
1065
+ function trimmed(value) {
1066
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1067
+ }
1068
+ /**
1069
+ * Reads `merchant.links` as flat fields (`{ website, twitter, facebook,
1070
+ * instagram }`), whatever shape it was saved in:
1071
+ * - the nested shape adminV1 saves (`{ website: { home }, social: { … } }`);
1072
+ * - flat keys (`{ website: "…" }`, what SDK 0.4.0 pre-releases sent);
1073
+ * - an array in {@link MERCHANT_LINK_KEYS} order (older AdminUI versions).
1074
+ * Empty values are dropped.
1075
+ */
1076
+ function merchantLinksToForm(links) {
1077
+ const out = {};
1078
+ if (Array.isArray(links)) {
1079
+ links.forEach((value, i) => {
1080
+ const key = MERCHANT_LINK_KEYS[i];
1081
+ const v = trimmed(value);
1082
+ if (key && v) out[key] = v;
1083
+ });
1084
+ return out;
1085
+ }
1086
+ if (!links || typeof links !== "object") return out;
1087
+ const obj = links;
1088
+ for (const key of MERCHANT_LINK_KEYS) {
1089
+ const [group, field] = MERCHANT_LINK_PATHS[key];
1090
+ const nested = obj[group];
1091
+ const v = nested && typeof nested === "object" && !Array.isArray(nested) ? trimmed(nested[field]) : trimmed(obj[key]);
1092
+ if (v) out[key] = v;
1093
+ }
1094
+ return out;
1095
+ }
1096
+ /**
1097
+ * Builds the stored `links` object from flat fields. Empty values are left
1098
+ * out, and so is a group with nothing in it; no links gives `{}`.
1099
+ */
1100
+ function merchantLinksFromForm(form) {
1101
+ const out = {};
1102
+ for (const key of MERCHANT_LINK_KEYS) {
1103
+ const v = trimmed(form[key]);
1104
+ if (!v) continue;
1105
+ const [group, field] = MERCHANT_LINK_PATHS[key];
1106
+ (out[group] ??= {})[field] = v;
1107
+ }
1108
+ return out;
1109
+ }
1110
+ /** Reads any stored or older `links` value into the nested shape adminV1 saves. */
1111
+ function normalizeMerchantLinks(links) {
1112
+ return merchantLinksFromForm(merchantLinksToForm(links));
1113
+ }
1114
+ /**
1115
+ * Object fields of the merchant. The API keeps one you leave out, and
1116
+ * replaces it as a whole when you send it.
1117
+ */
1118
+ const MERCHANT_OBJECT_FIELDS = [
1119
+ "colours",
1120
+ "links",
1121
+ "imageIds"
1122
+ ];
1123
+ /** Fields the API sets itself; `updateMerchant` never sends them back. */
1124
+ const MERCHANT_READ_ONLY_FIELDS = ["createdDate", "modifiedDate"];
1125
+ /**
1126
+ * Longest values the API stores. Longer values are cut off silently
1127
+ * (live API, Sep 2026), so `updateMerchant` rejects them instead.
1128
+ * `vatNumber` fits a UK VAT number without the `GB` prefix.
1129
+ */
1130
+ const MERCHANT_FIELD_MAX_LENGTH = Object.freeze({
1131
+ companyNumber: 11,
1132
+ vatNumber: 9
1133
+ });
1134
+ /** Returns `"<field> is longer than <n> characters"` messages for over-long fields. */
1135
+ function validateMerchant(merchant) {
1136
+ const problems = [];
1137
+ for (const [field, max] of Object.entries(MERCHANT_FIELD_MAX_LENGTH)) {
1138
+ const value = merchant[field];
1139
+ if (typeof value === "string" && value.length > max) problems.push(`${field} is longer than ${max} characters`);
1140
+ }
1141
+ return problems;
1142
+ }
1143
+ /**
1144
+ * Reads the logged-in user's merchant (a user has exactly one), or `null`
1145
+ * when the API returns none.
1146
+ */
1147
+ async function fetchMerchant(options = {}) {
1148
+ const json = await apiGet(MERCHANT_PATH, {}, options);
1149
+ assertNoApiErrors(json, 200);
1150
+ return json?.merchant ?? null;
1151
+ }
1152
+ /**
1153
+ * Updates the merchant and returns it as stored.
1154
+ *
1155
+ * Shopwave's `POST /merchant` replaces the whole record: a scalar field you
1156
+ * leave out (`companyNumber`, `vatNumber`, `note`, …) is set to null, while
1157
+ * an object field you leave out (`colours`, `links`, `imageIds`) is kept and
1158
+ * one you send replaces the stored one; sending `{}` clears it (live API,
1159
+ * Sep 2026). So by default the
1160
+ * current merchant is read first and the patch is merged over it.
1161
+ *
1162
+ * `imageIds` is always sent as file names ({@link toStoredImageIds}), so you
1163
+ * can pass back what you read. See `MerchantImageIds` for the shape.
1164
+ *
1165
+ * The API answers 205 with an empty body, so the merchant is read again after
1166
+ * the save and that is what's returned (it shows exactly what was kept).
1167
+ * Throws before sending when a value is longer than the API stores
1168
+ * ({@link MERCHANT_FIELD_MAX_LENGTH}).
1169
+ */
1170
+ async function updateMerchant(patch, options = {}) {
1171
+ const { merge = true,...requestOptions } = options;
1172
+ let merchant = { ...patch };
1173
+ if (merge) {
1174
+ const current = await fetchMerchant(requestOptions);
1175
+ if (!current) throw new ShopwaveApiError(404, "Shopwave API error (404): no merchant to update");
1176
+ 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})`);
1177
+ merchant = {
1178
+ ...current,
1179
+ ...patch,
1180
+ id: current.id
1181
+ };
1182
+ for (const field of MERCHANT_OBJECT_FIELDS) if (!(field in patch)) delete merchant[field];
1183
+ }
1184
+ if (merchant.imageIds && typeof merchant.imageIds === "object") merchant.imageIds = toStoredImageIds(merchant.imageIds);
1185
+ if (merchant.id == null || merchant.id === "") throw new Error("merchant id is missing");
1186
+ for (const field of MERCHANT_READ_ONLY_FIELDS) delete merchant[field];
1187
+ const problems = validateMerchant(merchant);
1188
+ if (problems.length > 0) throw new Error(`Invalid merchant: ${problems.join("; ")}`);
1189
+ const { status, body: raw } = await apiRequest(MERCHANT_PATH, {
1190
+ method: "PUT",
1191
+ headers: { "Content-Type": "application/json" },
1192
+ body: JSON.stringify({ merchant })
1193
+ }, requestOptions);
1194
+ const body = raw;
1195
+ assertNoApiErrors(body, status);
1196
+ const echoed = body?.merchant && typeof body.merchant === "object" ? body.merchant : null;
1197
+ if (echoed) return {
1198
+ ...merchant,
1199
+ ...echoed,
1200
+ id: Number(echoed.id ?? merchant.id)
1201
+ };
1202
+ return await fetchMerchant(requestOptions) ?? {
1203
+ ...merchant,
1204
+ id: Number(merchant.id)
1205
+ };
1206
+ }
1207
+
1208
+ //#endregion
1209
+ //#region src/core/user.ts
1210
+ /** Reads the logged-in user, or `null` when the API returns none. */
1211
+ async function fetchUser(options = {}) {
1212
+ const json = await apiGet(`/api/${SHOPWAVE_RESOURCES.user.route}`, {}, options);
1213
+ assertNoApiErrors(json, 200);
1214
+ return json?.user ?? null;
1215
+ }
1216
+
737
1217
  //#endregion
738
1218
  //#region src/hooks/shared.ts
739
1219
  function toMessage(e) {
@@ -1091,6 +1571,85 @@ function useSession(options) {
1091
1571
  }), [options?.baseUrl]);
1092
1572
  }
1093
1573
 
1574
+ //#endregion
1575
+ //#region src/hooks/useMerchant.ts
1576
+ /**
1577
+ * The logged-in user's merchant, fetched on mount. `data` is `null` while
1578
+ * loading or when the API has no merchant.
1579
+ *
1580
+ * @example
1581
+ * const { data: merchant, loading, refetch } = useMerchant();
1582
+ */
1583
+ function useMerchant(options) {
1584
+ return useQuery((signal) => fetchMerchant({
1585
+ ...options,
1586
+ signal
1587
+ }), [options?.baseUrl, options?.token]);
1588
+ }
1589
+ /**
1590
+ * Manually-triggered merchant update (see `updateMerchant`: the patch is merged
1591
+ * over the current merchant by default). Resolves to the saved merchant, or
1592
+ * `null` on error (then `error` / `errorStatus` are set).
1593
+ *
1594
+ * @example
1595
+ * const { mutate: saveMerchant, loading: saving } = useUpdateMerchant();
1596
+ * const saved = await saveMerchant({ vatNumber: "GB123" });
1597
+ */
1598
+ function useUpdateMerchant(options) {
1599
+ return useMutation((patch) => updateMerchant(patch, options), [
1600
+ options?.baseUrl,
1601
+ options?.token,
1602
+ options?.merge
1603
+ ]);
1604
+ }
1605
+
1606
+ //#endregion
1607
+ //#region src/hooks/useUser.ts
1608
+ /**
1609
+ * The logged-in user, fetched on mount. `errorStatus` is 401 when logged out.
1610
+ *
1611
+ * @example
1612
+ * const { data: user } = useUser();
1613
+ */
1614
+ function useUser(options) {
1615
+ const enabled = options?.enabled ?? true;
1616
+ return useQuery((signal) => fetchUser({
1617
+ ...options,
1618
+ signal
1619
+ }), [options?.baseUrl, options?.token], enabled);
1620
+ }
1621
+
1622
+ //#endregion
1623
+ //#region src/hooks/useUploadImage.ts
1624
+ /**
1625
+ * Manually-triggered image upload. Resolves to `{ id, url }`, or `null` on
1626
+ * error (then `error` / `errorStatus` are set).
1627
+ *
1628
+ * @example
1629
+ * const { mutate: upload, loading: uploading } = useUploadImage();
1630
+ * const image = await upload(file, { kind: "merchant" });
1631
+ */
1632
+ function useUploadImage(options) {
1633
+ return useMutation((file, params) => uploadImage(file, params, options), [options?.baseUrl, options?.token]);
1634
+ }
1635
+ /**
1636
+ * Manually-triggered upload for a merchant image slot (`receipt`, `square`,
1637
+ * `featured`): fits the image to the slot's exact size, uploads it with the
1638
+ * slot's kind, and resolves to `{ id, url }`. Put `id` into `imageIds` with
1639
+ * `setMerchantImage`.
1640
+ *
1641
+ * @example
1642
+ * const { mutate: upload } = useUploadMerchantImage();
1643
+ * const image = await upload(file, "square");
1644
+ */
1645
+ function useUploadMerchantImage(options) {
1646
+ return useMutation((file, slot) => uploadMerchantImage(file, slot, options), [
1647
+ options?.baseUrl,
1648
+ options?.token,
1649
+ options?.fit
1650
+ ]);
1651
+ }
1652
+
1094
1653
  //#endregion
1095
1654
  //#region src/hooks/useBasketReport.ts
1096
1655
  /**
@@ -1197,9 +1756,21 @@ function useBasketReport(options) {
1197
1756
  //#endregion
1198
1757
  exports.DEFAULT_BASKET_FILTERS = DEFAULT_BASKET_FILTERS;
1199
1758
  exports.DEFAULT_PAGINATION = DEFAULT_PAGINATION;
1759
+ exports.MERCHANT_COLOUR_KEYS = MERCHANT_COLOUR_KEYS;
1760
+ exports.MERCHANT_FIELD_MAX_LENGTH = MERCHANT_FIELD_MAX_LENGTH;
1761
+ exports.MERCHANT_IMAGE_SLOTS = MERCHANT_IMAGE_SLOTS;
1762
+ exports.MERCHANT_IMAGE_SLOT_KEYS = MERCHANT_IMAGE_SLOT_KEYS;
1763
+ exports.MERCHANT_LINK_KEYS = MERCHANT_LINK_KEYS;
1764
+ exports.MERCHANT_LINK_PATHS = MERCHANT_LINK_PATHS;
1765
+ exports.MERCHANT_OBJECT_FIELDS = MERCHANT_OBJECT_FIELDS;
1766
+ exports.MERCHANT_READ_ONLY_FIELDS = MERCHANT_READ_ONLY_FIELDS;
1200
1767
  exports.PromotionRuleSet = PromotionRuleSet;
1201
1768
  exports.SHOPWAVE_ENTITIES = SHOPWAVE_ENTITIES;
1769
+ exports.SHOPWAVE_IMAGE_BASE_URL = SHOPWAVE_IMAGE_BASE_URL;
1770
+ exports.SHOPWAVE_RESOURCES = SHOPWAVE_RESOURCES;
1771
+ exports.SHOPWAVE_UPLOAD = SHOPWAVE_UPLOAD;
1202
1772
  exports.ShopwaveApiError = ShopwaveApiError;
1773
+ exports.UPLOAD_KINDS = UPLOAD_KINDS;
1203
1774
  exports.apiDelete = apiDelete;
1204
1775
  exports.apiGet = apiGet;
1205
1776
  exports.apiRequest = apiRequest;
@@ -1212,6 +1783,7 @@ exports.collectConsumerIds = collectConsumerIds;
1212
1783
  exports.collectProductIds = collectProductIds;
1213
1784
  exports.combineBasketRows = combineBasketRows;
1214
1785
  exports.computeBasketSummary = computeBasketSummary;
1786
+ exports.coverCrop = coverCrop;
1215
1787
  exports.deleteCategory = deleteCategory;
1216
1788
  exports.deleteEmployee = deleteEmployee;
1217
1789
  exports.deleteEntity = deleteEntity;
@@ -1226,6 +1798,7 @@ exports.fetchConsumers = fetchConsumers;
1226
1798
  exports.fetchEmployee = fetchEmployee;
1227
1799
  exports.fetchEmployees = fetchEmployees;
1228
1800
  exports.fetchEntityById = fetchEntityById;
1801
+ exports.fetchMerchant = fetchMerchant;
1229
1802
  exports.fetchProduct = fetchProduct;
1230
1803
  exports.fetchProducts = fetchProducts;
1231
1804
  exports.fetchProductsMap = fetchProductsMap;
@@ -1235,13 +1808,24 @@ exports.fetchReport = fetchReport;
1235
1808
  exports.fetchSession = fetchSession;
1236
1809
  exports.fetchStore = fetchStore;
1237
1810
  exports.fetchStores = fetchStores;
1811
+ exports.fetchUser = fetchUser;
1812
+ exports.fitImageToSize = fitImageToSize;
1238
1813
  exports.formatDateForQuery = formatDateForQuery;
1239
1814
  exports.getApiErrorMap = getApiErrorMap;
1240
1815
  exports.getApiErrors = getApiErrors;
1816
+ exports.getImageUrl = getImageUrl;
1817
+ exports.getMerchantImage = getMerchantImage;
1818
+ exports.getMerchantImageUrl = getMerchantImageUrl;
1819
+ exports.imageFileName = imageFileName;
1820
+ exports.isUploadKind = isUploadKind;
1241
1821
  exports.loginPath = loginPath;
1242
1822
  exports.logout = logout;
1243
1823
  exports.logoutPath = logoutPath;
1824
+ exports.merchantLinksFromForm = merchantLinksFromForm;
1825
+ exports.merchantLinksToForm = merchantLinksToForm;
1826
+ exports.normalizeMerchantLinks = normalizeMerchantLinks;
1244
1827
  exports.parseBasketReportData = parseBasketReportData;
1828
+ exports.readImageSize = readImageSize;
1245
1829
  exports.rolesById = rolesById;
1246
1830
  exports.saveCategory = saveCategory;
1247
1831
  exports.saveEmployee = saveEmployee;
@@ -1250,19 +1834,30 @@ exports.saveEntity = saveEntity;
1250
1834
  exports.saveProduct = saveProduct;
1251
1835
  exports.savePromotion = savePromotion;
1252
1836
  exports.saveStore = saveStore;
1837
+ exports.setMerchantImage = setMerchantImage;
1253
1838
  exports.submitEntity = submitEntity;
1254
1839
  exports.toShopwaveApiError = toShopwaveApiError;
1840
+ exports.toStoredImageIds = toStoredImageIds;
1841
+ exports.updateMerchant = updateMerchant;
1842
+ exports.uploadImage = uploadImage;
1843
+ exports.uploadMerchantImage = uploadMerchantImage;
1255
1844
  exports.useBasketReport = useBasketReport;
1256
1845
  exports.useCategory = useCategory;
1257
1846
  exports.useConsumer = useConsumer;
1258
1847
  exports.useDelete = useDelete;
1259
1848
  exports.useEmployee = useEmployee;
1260
1849
  exports.useLogout = useLogout;
1850
+ exports.useMerchant = useMerchant;
1261
1851
  exports.useProduct = useProduct;
1262
1852
  exports.usePromotion = usePromotion;
1263
1853
  exports.useReport = useReport;
1264
1854
  exports.useSession = useSession;
1265
1855
  exports.useStore = useStore;
1266
1856
  exports.useSubmit = useSubmit;
1857
+ exports.useUpdateMerchant = useUpdateMerchant;
1858
+ exports.useUploadImage = useUploadImage;
1859
+ exports.useUploadMerchantImage = useUploadMerchantImage;
1860
+ exports.useUser = useUser;
1861
+ exports.validateMerchant = validateMerchant;
1267
1862
  exports.withToken = withToken;
1268
1863
  //# sourceMappingURL=index.cjs.map