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/README.md
CHANGED
|
@@ -161,10 +161,64 @@ How they behave (matches what the Shopwave API does):
|
|
|
161
161
|
- **Promotions:** Shopwave has no promotion DELETE; `deletePromotion` (and `DELETE /api/promotions/:id`) ends the promotion by setting `endDate` to now.
|
|
162
162
|
- **Employees:** Shopwave has no employee DELETE, so `deleteEmployee` (and `DELETE /api/employees/:id`) retires the employee by setting `exitDate`. Updating an existing employee only changes `roleId`, `joinedDate` and `exitDate` — names and email are fixed — and the echo carries only those fields, so re-read with `fetchEmployee` if you need the stored record.
|
|
163
163
|
- **Consumers** are read-only (`GET /consumer` by `ids`); their write/delete routes answer 405.
|
|
164
|
+
- **Product prices (instances) can't be updated** — the API keeps them for history; a price change means a new instance with a new id. Build `instances` with `toProductInstancesForSave(formInstances, productAsLastRead)`: it keys them `"0"…"n"`, keeps an instance's `id` only when the original has that id with the same price and tax, and drops it otherwise (and drops server `timestamp`). `findRepricedInstances(formInstances, productAsLastRead)` lists the instances that will get new ids, so you can warn first. `saveProduct` without an `id` (a new product or a duplicate) always strips instance ids, so a copy can't point at the original's prices.
|
|
164
165
|
- Errors are read from `api.message.errors` and also `api.message.error` (the name used in the [API reference](https://developer.merchantstack.com/api-reference.html)).
|
|
165
166
|
|
|
166
167
|
The generic forms are `saveEntity(kind, item)`, `deleteEntityById(kind, id)` and `fetchEntityById(kind, id)`; `SHOPWAVE_ENTITIES` lists each entity's route, collection key, Shopwave path and id headers.
|
|
167
168
|
|
|
169
|
+
### Merchant, current user and image uploads
|
|
170
|
+
|
|
171
|
+
The merchant and the logged-in user are single records (one per login), and images go to the Shopwave CDN:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import {
|
|
175
|
+
fetchMerchant, updateMerchant, fetchUser,
|
|
176
|
+
uploadMerchantImage, setMerchantImage, getMerchantImageUrl,
|
|
177
|
+
merchantLinksToForm, merchantLinksFromForm,
|
|
178
|
+
} from "react-shopwave-connect/core";
|
|
179
|
+
|
|
180
|
+
const merchant = await fetchMerchant(); // Merchant | null
|
|
181
|
+
const user = await fetchUser(); // User | null ({ id, firstName, lastName, email, employee })
|
|
182
|
+
|
|
183
|
+
// Reads the merchant, merges the patch over it and saves the record, so
|
|
184
|
+
// fields you don't mention (companyNumber, vatNumber, links, …) are kept.
|
|
185
|
+
// Returns the merchant as stored (read back after the save).
|
|
186
|
+
await updateMerchant({ vatNumber: "123456789", links: merchantLinksFromForm({ website: "https://example.com" }) });
|
|
187
|
+
|
|
188
|
+
// Images: fit to the slot's size, upload with the slot's kind, then save the file name.
|
|
189
|
+
const image = await uploadMerchantImage(file, "square"); // { id, url, path }
|
|
190
|
+
await updateMerchant({ imageIds: setMerchantImage(merchant?.imageIds, "square", image.id) });
|
|
191
|
+
getMerchantImageUrl(saved.imageIds, "square"); // HTTPS URL for <img src>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The shapes below are the ones adminV1 saves and adminV2 reads, so all three admins share one record (confirmed on the live API with merchant 5644, 30 Sep 2026):
|
|
195
|
+
|
|
196
|
+
| Field | Shape |
|
|
197
|
+
|---|---|
|
|
198
|
+
| `imageIds` | `{ logo: { receipt, square }, feature: [ … ] }` — **file names** on write (the uploader's `fileName`), **full `http://static.merchantstack.com/…` URLs** on read |
|
|
199
|
+
| `colours` | `{ primary: { main, highlight, contrast } }` (hex strings; adminV2 uses `primary.main` behind the logo) |
|
|
200
|
+
| `links` | `{ website: { home }, social: { twitter, facebook, instagram } }` |
|
|
201
|
+
|
|
202
|
+
- **Merchant fields** (`GET`/`POST /merchant`): `id`, `name`, `description`, `companyNumber`, `vatNumber`, `categoryId`, `estAnnualRevenue`, `note`, and the three objects above. The API returns `{}` for an empty object.
|
|
203
|
+
- **What the live API does on `POST /merchant`** (checked 30 Sep 2026): it replaces the record. A scalar field left out (`companyNumber`, `vatNumber`, `note`, …) becomes `null`; an object field left out (`colours`, `links`, `imageIds`) is kept, and one that is sent replaces the stored one (`{}` clears it). It answers **205 with an empty body**. It silently cuts `companyNumber` to 11 characters and `vatNumber` to 9 (a UK VAT number without `GB`).
|
|
204
|
+
- **`updateMerchant(patch, { merge })`**: `merge` (default `true`) reads first and merges the scalar fields, so nothing is wiped. Object fields (`MERCHANT_OBJECT_FIELDS`) are sent only when the patch has them, and then replace the stored one — to change one image, pass the whole `imageIds` (`setMerchantImage` does that). `imageIds` is always sent as file names (`toStoredImageIds`), so you can pass back what you read. It refuses a different merchant `id`, never sends `createdDate`/`modifiedDate`, rejects values longer than `MERCHANT_FIELD_MAX_LENGTH` (`validateMerchant(m)` lists them) instead of letting the API cut them, and returns the merchant **read back after the save**, so you see exactly what was kept. `merge: false` sends only the patch (which must include `id`, and clears the scalar fields you leave out).
|
|
205
|
+
- **Merchant image slots** (`MERCHANT_IMAGE_SLOTS`): the API doesn't resize, and adminV1 accepts only these exact sizes.
|
|
206
|
+
|
|
207
|
+
| Slot | Stored at | Upload kind | Size |
|
|
208
|
+
|---|---|---|---|
|
|
209
|
+
| `receipt` | `imageIds.logo.receipt` | `merchant` | 576 × 325 |
|
|
210
|
+
| `square` | `imageIds.logo.square` | `merchant` | 1000 × 1000 |
|
|
211
|
+
| `featured` | `imageIds.feature[0]` | `merchantFeature` | 1200 × 600 |
|
|
212
|
+
|
|
213
|
+
The API builds each image URL from its key, not from where the file was uploaded (`logo.*` → `…/merchant/<id>/logo/<file>`, `feature` → `…/feature/<file>`), so the featured image must be uploaded as `merchantFeature`. `POST /merchant` stores all three objects with the SDK's `{ merchant: … }` body; adminV1's `{ merchants: { "0": … } }` form isn't needed.
|
|
214
|
+
|
|
215
|
+
`uploadMerchantImage(file, slot, { fit = true })` scales and centre-crops the image to the slot's size in the browser (`fitImageToSize`, `coverCrop`) and uploads it with the slot's kind; `fit: false` sends the file as-is. `getMerchantImage` / `getMerchantImageUrl` / `setMerchantImage` read and write a slot; `imageFileName(url)` gives the file name in a URL. Hook: `useUploadMerchantImage()`.
|
|
216
|
+
- **Links:** `merchantLinksToForm(links)` gives flat fields (`website`, `twitter`, `facebook`, `instagram`) from the stored shape, flat keys or the old array form; `merchantLinksFromForm(fields)` builds the stored shape; `normalizeMerchantLinks(links)` converts any of them to the stored shape. Colour keys: `MERCHANT_COLOUR_KEYS`.
|
|
217
|
+
- **User** (`GET /user`): the live API returns `id`, `firstName`, `lastName`, `email` and `employee: { merchantId, roleId, stores }` — no `createdDate` and **no `merchant`**, although the reference lists them. There's no option to include it (adminV1 also reads `GET /merchant` separately and attaches it to its session). Read the merchant with `fetchMerchant()`.
|
|
218
|
+
- **`uploadImage(file, { kind, fileName? })`** posts the file to `/api/upload`, which `PUT`s it to Shopwave's `/uploader` (multipart `file`, header `contentType: <kind>`). Kinds: `merchant`, `merchantFeature`, `product`, `user`, `applicationLogo`, `applicationImages` (the last two sent as the API's own spelling, `applicaionLogo`/`applicaionImages`). Live: 201 `{ fileName, path }`. Returns `{ id, url, path }`: `id` is the generated file name (what records store), `path` the URL as the API gave it (`http://static.merchantstack.com/images/…`, which has no HTTPS endpoint), and `url` the same file over HTTPS via `getImageUrl(path)` (`SHOPWAVE_IMAGE_BASE_URL`, the S3 bucket behind the CDN). The upload alone changes no record.
|
|
219
|
+
|
|
220
|
+
`SHOPWAVE_RESOURCES` (merchant, user) and `SHOPWAVE_UPLOAD` are shared with the server routes, like `SHOPWAVE_ENTITIES`.
|
|
221
|
+
|
|
168
222
|
### Available `core` functions
|
|
169
223
|
|
|
170
224
|
| Domain | Function(s) |
|
|
@@ -172,16 +226,19 @@ The generic forms are `saveEntity(kind, item)`, `deleteEntityById(kind, id)` and
|
|
|
172
226
|
| category | `fetchCategories`, `fetchCategory`, `saveCategory`, `deleteCategory` |
|
|
173
227
|
| consumer | `fetchConsumers`, `fetchConsumer` (read-only) |
|
|
174
228
|
| employee | `fetchEmployees`, `fetchEmployee`, `saveEmployee`, `deleteEmployee` |
|
|
175
|
-
| product | `fetchProducts`, `fetchProductsMap` (batched, keyed), `fetchProduct`, `saveProduct`, `deleteProduct` |
|
|
229
|
+
| product | `fetchProducts`, `fetchProductsMap` (batched, keyed), `fetchProduct`, `saveProduct`, `deleteProduct`; prices `toProductInstancesForSave`, `findRepricedInstances`, `isSameInstancePrice` |
|
|
176
230
|
| promotion | `fetchPromotions`, `fetchPromotion`, `savePromotion`, `deletePromotion` |
|
|
177
231
|
| store | `fetchStores` (now with `storeIds`), `fetchStore`, `saveStore`, `deleteStore` |
|
|
178
232
|
| report | `fetchReport` |
|
|
233
|
+
| merchant | `fetchMerchant`, `updateMerchant`, `validateMerchant`; links `merchantLinksToForm`, `merchantLinksFromForm`, `normalizeMerchantLinks`; images `MERCHANT_IMAGE_SLOTS`, `uploadMerchantImage`, `getMerchantImage`, `getMerchantImageUrl`, `setMerchantImage`, `toStoredImageIds`, `imageFileName` |
|
|
234
|
+
| user | `fetchUser` |
|
|
235
|
+
| upload | `uploadImage`, `getImageUrl`, `fitImageToSize`, `readImageSize`, `coverCrop` |
|
|
179
236
|
| session | `fetchSession`, `loginPath`, `logoutPath`, `logout` |
|
|
180
237
|
| entity | `saveEntity`, `saveEntities`, `deleteEntityById`, `fetchEntityById`; low-level `submitEntity` / `deleteEntity` (raw endpoint) |
|
|
181
238
|
| errors | `ShopwaveApiError`, `getApiErrorMap`, `assertNoApiErrors` |
|
|
182
239
|
| basket | `buildBasketReportQuery`, `parseBasketReportData`, `combineBasketRows`, `computeBasketSummary`, … (pure transforms) |
|
|
183
240
|
|
|
184
|
-
All types/interfaces (`Product`, `Store`, `Category`, `Consumer`, `Employee`, `ReportQueryMap`, `Basket*`, `apiResponse`, …) are exported from `core` too.
|
|
241
|
+
All types/interfaces (`Product`, `Store`, `Category`, `Consumer`, `Employee`, `Merchant`, `User`, `UploadedImage`, `ReportQueryMap`, `Basket*`, `apiResponse`, …) are exported from `core` too.
|
|
185
242
|
|
|
186
243
|
---
|
|
187
244
|
|
|
@@ -224,14 +281,14 @@ const { data } = useProduct({ storeId }, { baseUrl: "https://api.merchantstack.c
|
|
|
224
281
|
|
|
225
282
|
### Auto-fetch hooks (`useEffect`-based)
|
|
226
283
|
|
|
227
|
-
`useCategory`, `useConsumer`, `useEmployee`, `useProduct`, `usePromotion`, `useStore`, `useReport`, `useSession
|
|
284
|
+
`useCategory`, `useConsumer`, `useEmployee`, `useProduct`, `usePromotion`, `useStore`, `useReport`, `useSession`, `useMerchant`, `useUser` (`useUser({ enabled: false })` stays idle).
|
|
228
285
|
They fetch on mount and re-run when their arguments change. `useConsumer` and `useReport` stay idle until you pass ids / a query (they return `loading: false`, `data: null` until then). `refetch()` replaces the old `reloadFlag` argument.
|
|
229
286
|
|
|
230
287
|
### Manually-triggered hooks (`useCallback`-based)
|
|
231
288
|
|
|
232
289
|
For saves and deletes, prefer calling the typed `core` functions (`saveProduct`, `deleteStore`, …) from your event handlers — they return the saved entity with its id.
|
|
233
290
|
|
|
234
|
-
`useDelete`, `useSubmit`, `useLogout` return `{ mutate, data, loading, error, errorStatus }` — nothing fires until you call `mutate`:
|
|
291
|
+
`useUpdateMerchant`, `useUploadImage`, `useUploadMerchantImage`, `useDelete`, `useSubmit`, `useLogout` return `{ mutate, data, loading, error, errorStatus }` — nothing fires until you call `mutate`:
|
|
235
292
|
|
|
236
293
|
```tsx
|
|
237
294
|
import { useDelete, useSubmit } from "react-shopwave-connect/hooks";
|
|
@@ -401,8 +458,20 @@ export const { GET, PUT, DELETE } = shopwave.item("product");
|
|
|
401
458
|
// app/api/report/route.ts — any other Shopwave path, read-only
|
|
402
459
|
import { shopwave } from "@/lib/shopwave";
|
|
403
460
|
export const { GET } = shopwave.passthrough("report");
|
|
461
|
+
|
|
462
|
+
// app/api/merchant/route.ts — read / update the merchant
|
|
463
|
+
export const { GET, PUT } = shopwave.resource("merchant");
|
|
464
|
+
|
|
465
|
+
// app/api/user/route.ts — the logged-in user (read-only)
|
|
466
|
+
export const { GET } = shopwave.resource("user");
|
|
467
|
+
|
|
468
|
+
// app/api/upload/route.ts — image upload (uploadImage)
|
|
469
|
+
export const { POST } = shopwave.upload(); // or upload({ maxBytes, accept })
|
|
404
470
|
```
|
|
405
471
|
|
|
472
|
+
- `resource("merchant")`: `PUT`/`POST` take `{ merchant: {…} }` or the bare object and send every field on as `postBody`. The `id` is required (400 without it), because a POST without one would create a second merchant. `resource("user")` answers 405 to writes.
|
|
473
|
+
- `upload()`: takes multipart `file` + `kind`, checks the size (default 10 MB → 413) and type (default `image/*` → 415), and `PUT`s only the file to `/uploader` with the `contentType` header. `extras` are ignored on uploads. Shopwave's reply (201 `{ fileName, path }`) is passed through.
|
|
474
|
+
|
|
406
475
|
Entities: `product`, `category`, `store`, `promotion`, `employee` (DELETE retires via `exitDate`), `promotion` (DELETE ends it via `endDate`), `consumer` (mounted at `/api/consumer`; GET only, writes answer 405). For anything else, `shopwave.forward(request, { method, path, headers, postBody })` does the same token/refresh/encoding work for a custom handler.
|
|
407
476
|
|
|
408
477
|
Every handler:
|