@shoppexio/storefront-react 0.1.5 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ - Favorites follow the account session without a reload. Signing in on the
6
+ account page reloads the favorites store, so the hearts on cards and the
7
+ product page work at once instead of sending the buyer back to Account.
8
+ Signing out reloads it too, which clears the previous buyer's saved set
9
+ instead of leaving it on the page for whoever uses the browser next.
10
+ (`announceCustomerSessionChange()`, `signOutEndedSession()`.)
11
+
12
+ ## 0.2.0
13
+
14
+ Breaking:
15
+
16
+ - `settings.availableGateways` is gone. Use `settings.paymentMethods`: the
17
+ public payment methods the backend derives from the shop's live gateways,
18
+ each `{ id, label, kind }` with `kind` `card`, `wallet` or `crypto`,
19
+ already in display order. A theme no longer maps gateway codes itself.
20
+ - `resolveDeliveryPromise()` returns `{ kind: 'unspecified' }` when the
21
+ merchant set no delivery time. A missing delivery time used to read as
22
+ instant delivery; hosted checkout never promised that, and themes no longer
23
+ do. Handle the new kind in an exhaustive `switch`.
24
+
25
+ Added:
26
+
27
+ - Product meta: `shortDescription` (one plain-text line, at most 200
28
+ characters), the unit labels `unitLabelSingular` / `unitLabelPlural`, an
29
+ option's `description`, and `resolveYouReceive()` for a "You receive" line
30
+ that only states the real delivery type: keys for serial products, a
31
+ download for file products, nothing otherwise.
32
+ - Favorites: `<FavoritesProvider>` and `useFavorites()` for a heart on
33
+ cards and the product page. It shares one store with the account's
34
+ favorites tab (`FAVORITES_CHANGED_EVENT`) and reports `signed_out`
35
+ instead of failing when there is no customer session.
36
+ - Back-in-stock ("Notify me"): `canRequestStockAlert()` and
37
+ `requestStockAlert()`. `settings.stockAlertsEnabled` reflects the shop
38
+ toggle.
39
+ - Status page: `parseServiceStatus()` and `buildStatusPageModel()` for a
40
+ `/status` page. It lists only the products the merchant gave a status
41
+ (`operational`, `updating`, `detected`, `offline`).
42
+
43
+ Needs: a Shoppex backend that sends `payment_methods`,
44
+ `stock_alerts_enabled`, `short_description` and `service_status` in the
45
+ storefront payload. Parsing fails fast when a field is missing.
46
+
3
47
  ## 0.1.5
4
48
 
5
49
  - `loadShopPage()` returns `error` for a failed request or render and `missing`
package/README.md CHANGED
@@ -80,7 +80,10 @@ createRoot(document.getElementById('root')!).render(
80
80
 
81
81
  - Catalog: product and bundle cards, categories, sorting, filtering, paging.
82
82
  - Product pages: options, pricing per quantity, stock, badges, videos, the
83
- rating breakdown.
83
+ rating breakdown, a short description and a "You receive" line.
84
+ - Favorites on cards and product pages, "Notify me" for sold-out products,
85
+ and a `/status` page from the statuses the merchant sets.
86
+ - Payment method marks from the shop's live payment methods.
84
87
  - Cart: add, update, remove, coupons, quotes from the server, checkout
85
88
  hand-off, per-product checkout fields.
86
89
  - Reviews, shop pages, navigation menus and the contact form, from the SDK.
@@ -164,6 +167,9 @@ Most themes never need this.
164
167
  | Product | The product page model: variants, rating, pricing, quantity bounds, badges, videos | `buildProductDetail()`, `calculateLinePricing()`, `badgesForProductPage()` |
165
168
  | Cart | Cart state, quotes, coupons, checkout hand-off | `<CartProvider>`, `useCart()`, `buildCartView()`, `buildCartTotals()` |
166
169
  | Checkout fields | Per-product fields a buyer fills before checkout | `resolveCheckoutFields()`, `readCheckoutNeedsDetails()` |
170
+ | Favorites | The signed-in customer's saved products, shared with the account tab | `<FavoritesProvider>`, `useFavorites()` |
171
+ | Back in stock | "Notify me" on a sold-out product or option | `canRequestStockAlert()`, `requestStockAlert()` |
172
+ | Status page | The merchant-set status of each product | `buildStatusPageModel()`, `parseServiceStatus()` |
167
173
  | Reviews, pages, menus, contact | SDK-backed hooks | `usePagedShopReviews()`, `loadLatestWrittenReviews()`, `loadShopPage()`, `useNavigationLinks()`, `submitContactTicket()` |
168
174
  | Merchant HTML | Sanitizing and flattening merchant rich text | `sanitizeMerchantHtml()`, `flattenMerchantHtmlToText()` |
169
175
  | Account routing | Mounting the account and its lazy chunk | `ACCOUNT_PATH`, `ACCOUNT_PATH_ALIAS`, `accountRouteMessage()`, `ACCOUNT_STOREFRONT_ROUTES`, `ACCOUNT_ARTIFACT_CAPABILITIES` |
package/dist/account.js CHANGED
@@ -4,9 +4,13 @@ import {
4
4
  accountTabHref,
5
5
  accountTabLabelKey,
6
6
  canonicalAccountPath,
7
+ favoriteProductHref,
7
8
  getShoppex,
8
- resolveAccountView
9
- } from "./chunk-D6XKDVAV.js";
9
+ narrowFavorites,
10
+ notifyFavoritesChanged,
11
+ resolveAccountView,
12
+ withoutFavorite
13
+ } from "./chunk-ZEFZPUAW.js";
10
14
 
11
15
  // src/account/account-page.tsx
12
16
  import { useEffect as useEffect5, useState as useState11 } from "react";
@@ -8618,52 +8622,6 @@ function CodeRow({ code }) {
8618
8622
  // src/account/favorites.tsx
8619
8623
  import { useEffect as useEffect2, useState as useState3 } from "react";
8620
8624
  import { Link as Link3 } from "react-router-dom";
8621
-
8622
- // src/account/account-favorites.ts
8623
- function isRecord2(value) {
8624
- return typeof value === "object" && value !== null && !Array.isArray(value);
8625
- }
8626
- function readNullableString(record, key) {
8627
- const value = record[key];
8628
- if (value === null || typeof value === "string") return value;
8629
- return void 0;
8630
- }
8631
- function narrowFavorites(payload) {
8632
- if (!isRecord2(payload) || !Array.isArray(payload.favorites)) return { ok: false };
8633
- const favorites = [];
8634
- for (const entry of payload.favorites) {
8635
- if (!isRecord2(entry)) return { ok: false };
8636
- const productUniqid = entry.product_uniqid;
8637
- const title = entry.title;
8638
- const slug = readNullableString(entry, "slug");
8639
- const imageUrl = readNullableString(entry, "image_url");
8640
- const priceDisplay = entry.price_display;
8641
- const unavailable = entry.unavailable;
8642
- const createdAt = entry.created_at;
8643
- if (typeof productUniqid !== "string" || typeof title !== "string" || slug === void 0 || imageUrl === void 0 || typeof priceDisplay !== "number" || !Number.isFinite(priceDisplay) || typeof unavailable !== "boolean" || typeof createdAt !== "string") {
8644
- return { ok: false };
8645
- }
8646
- favorites.push({
8647
- product_uniqid: productUniqid,
8648
- title,
8649
- slug,
8650
- image_url: imageUrl,
8651
- price_display: priceDisplay,
8652
- unavailable,
8653
- created_at: createdAt
8654
- });
8655
- }
8656
- return { ok: true, favorites };
8657
- }
8658
- function favoriteProductHref(favorite) {
8659
- if (favorite.unavailable || !favorite.slug) return null;
8660
- return `/product/${encodeURIComponent(favorite.slug)}`;
8661
- }
8662
- function withoutFavorite(favorites, productUniqid) {
8663
- return favorites.filter((favorite) => favorite.product_uniqid !== productUniqid);
8664
- }
8665
-
8666
- // src/account/favorites.tsx
8667
8625
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
8668
8626
  function AccountFavorites() {
8669
8627
  const { state, reload } = useAccountResource("favorites", () => callCustomerApi((sdk) => sdk.favorites()));
@@ -8709,6 +8667,7 @@ function AccountFavorites() {
8709
8667
  setRemoveError(result.message?.trim() || t("favorites.removeFailed"));
8710
8668
  return;
8711
8669
  }
8670
+ notifyFavoritesChanged();
8712
8671
  setFavorites((current) => current ? withoutFavorite(current, favorite.product_uniqid) : current);
8713
8672
  };
8714
8673
  return /* @__PURE__ */ jsxs4("div", { children: [
@@ -11264,21 +11223,21 @@ function grantStatusLabel(status) {
11264
11223
  import { useEffect as useEffect4, useId as useId2, useRef as useRef4, useState as useState8 } from "react";
11265
11224
 
11266
11225
  // src/account/account-settings.ts
11267
- function isRecord3(value) {
11226
+ function isRecord2(value) {
11268
11227
  return typeof value === "object" && value !== null && !Array.isArray(value);
11269
11228
  }
11270
- function readNullableString2(record, key) {
11229
+ function readNullableString(record, key) {
11271
11230
  const value = record[key];
11272
11231
  if (value === null || typeof value === "string") return value;
11273
11232
  return void 0;
11274
11233
  }
11275
11234
  function narrowEmailPreferences(payload) {
11276
- if (!isRecord3(payload) || typeof payload.global_unsubscribed !== "boolean") return { ok: false };
11235
+ if (!isRecord2(payload) || typeof payload.global_unsubscribed !== "boolean") return { ok: false };
11277
11236
  if (!Array.isArray(payload.lists)) return { ok: false };
11278
11237
  const lists = [];
11279
11238
  for (const entry of payload.lists) {
11280
- if (!isRecord3(entry)) return { ok: false };
11281
- const description = readNullableString2(entry, "description");
11239
+ if (!isRecord2(entry)) return { ok: false };
11240
+ const description = readNullableString(entry, "description");
11282
11241
  if (typeof entry.id !== "string" || typeof entry.name !== "string" || description === void 0 || typeof entry.subscribed !== "boolean") {
11283
11242
  return { ok: false };
11284
11243
  }
@@ -11287,12 +11246,12 @@ function narrowEmailPreferences(payload) {
11287
11246
  return { ok: true, preferences: { global_unsubscribed: payload.global_unsubscribed, lists } };
11288
11247
  }
11289
11248
  function narrowCustomerSessions(payload) {
11290
- if (!isRecord3(payload) || !Array.isArray(payload.sessions)) return { ok: false };
11249
+ if (!isRecord2(payload) || !Array.isArray(payload.sessions)) return { ok: false };
11291
11250
  const sessions = [];
11292
11251
  for (const entry of payload.sessions) {
11293
- if (!isRecord3(entry)) return { ok: false };
11294
- const ipAddress = readNullableString2(entry, "ip_address");
11295
- const userAgent = readNullableString2(entry, "user_agent");
11252
+ if (!isRecord2(entry)) return { ok: false };
11253
+ const ipAddress = readNullableString(entry, "ip_address");
11254
+ const userAgent = readNullableString(entry, "user_agent");
11296
11255
  if (typeof entry.id !== "string" || typeof entry.created_at !== "string" || typeof entry.expires_at !== "string" || ipAddress === void 0 || userAgent === void 0 || typeof entry.current !== "boolean") {
11297
11256
  return { ok: false };
11298
11257
  }
@@ -11857,7 +11816,7 @@ function billingMessage(plan) {
11857
11816
  function cancelsAtPeriodEnd(gateway) {
11858
11817
  return gateway.trim().toUpperCase() !== "PAYPAL";
11859
11818
  }
11860
- function isRecord4(value) {
11819
+ function isRecord3(value) {
11861
11820
  return typeof value === "object" && value !== null && !Array.isArray(value);
11862
11821
  }
11863
11822
  function readString(record, key) {
@@ -11865,10 +11824,10 @@ function readString(record, key) {
11865
11824
  return typeof value === "string" ? value : null;
11866
11825
  }
11867
11826
  function narrowBillingHistory(payload) {
11868
- if (!isRecord4(payload) || !Array.isArray(payload.invoices)) return { ok: false };
11827
+ if (!isRecord3(payload) || !Array.isArray(payload.invoices)) return { ok: false };
11869
11828
  const invoices = [];
11870
11829
  for (const entry of payload.invoices) {
11871
- if (!isRecord4(entry)) return { ok: false };
11830
+ if (!isRecord3(entry)) return { ok: false };
11872
11831
  const uniqid = readString(entry, "uniqid");
11873
11832
  const total = readString(entry, "total");
11874
11833
  const currency = readString(entry, "currency");
@@ -12135,10 +12094,10 @@ function BillingHistoryList({ payload, reload }) {
12135
12094
  import { useId as useId3, useState as useState10 } from "react";
12136
12095
 
12137
12096
  // src/account/account-support.ts
12138
- function isRecord5(value) {
12097
+ function isRecord4(value) {
12139
12098
  return typeof value === "object" && value !== null && !Array.isArray(value);
12140
12099
  }
12141
- function readNullableString3(record, key) {
12100
+ function readNullableString2(record, key) {
12142
12101
  const value = record[key];
12143
12102
  if (value === null || typeof value === "string") return value;
12144
12103
  return void 0;
@@ -12147,12 +12106,12 @@ function isSupportMessageRole(value) {
12147
12106
  return value === "CUSTOMER" || value === "MERCHANT" || value === "SYSTEM";
12148
12107
  }
12149
12108
  function narrowSupportTicket(payload) {
12150
- if (!isRecord5(payload) || !isRecord5(payload.ticket)) return { ok: false };
12109
+ if (!isRecord4(payload) || !isRecord4(payload.ticket)) return { ok: false };
12151
12110
  const raw = payload.ticket;
12152
12111
  const uniqid = raw.uniqid;
12153
- const title = readNullableString3(raw, "title");
12112
+ const title = readNullableString2(raw, "title");
12154
12113
  const status = raw.status;
12155
- const invoiceId = readNullableString3(raw, "invoice_id");
12114
+ const invoiceId = readNullableString2(raw, "invoice_id");
12156
12115
  const createdAt = raw.created_at;
12157
12116
  const updatedAt = raw.updated_at;
12158
12117
  if (typeof uniqid !== "string" || title === void 0 || typeof status !== "string" || invoiceId === void 0 || typeof createdAt !== "string" || typeof updatedAt !== "string" || !Array.isArray(raw.messages)) {
@@ -12160,7 +12119,7 @@ function narrowSupportTicket(payload) {
12160
12119
  }
12161
12120
  const messages = [];
12162
12121
  for (const entry of raw.messages) {
12163
- if (!isRecord5(entry)) return { ok: false };
12122
+ if (!isRecord4(entry)) return { ok: false };
12164
12123
  const { id, role, message, created_at: messageCreatedAt } = entry;
12165
12124
  if (typeof id !== "string" || !isSupportMessageRole(role) || typeof message !== "string" || typeof messageCreatedAt !== "string") {
12166
12125
  return { ok: false };
@@ -12196,7 +12155,7 @@ function messageAuthorKey(role) {
12196
12155
  return "support.fromMerchant";
12197
12156
  }
12198
12157
  function readCreatedTicketUniqid(data) {
12199
- if (!isRecord5(data)) return null;
12158
+ if (!isRecord4(data)) return null;
12200
12159
  const uniqid = data.uniqid;
12201
12160
  return typeof uniqid === "string" && uniqid.length > 0 ? uniqid : null;
12202
12161
  }
@@ -12548,6 +12507,12 @@ function remainingSearch(search) {
12548
12507
  const query = kept.toString();
12549
12508
  return query ? `?${query}` : "";
12550
12509
  }
12510
+ function announceCustomerSessionChange() {
12511
+ notifyFavoritesChanged();
12512
+ }
12513
+ function signOutEndedSession(result) {
12514
+ return result.success || result.status !== void 0;
12515
+ }
12551
12516
  function AccountPage({ frame: Frame, notFound }) {
12552
12517
  const location = useLocation();
12553
12518
  const search = new URLSearchParams(location.search);
@@ -12586,11 +12551,12 @@ function AccountPage({ frame: Frame, notFound }) {
12586
12551
  setSignOutError(null);
12587
12552
  const result = await callCustomerApi((sdk) => sdk.logout());
12588
12553
  setSigningOut(false);
12589
- if (!result.success && result.status === void 0) {
12554
+ if (!signOutEndedSession(result)) {
12590
12555
  setSignOutError(result.message?.trim() || t("account.signOutFailed"));
12591
12556
  return;
12592
12557
  }
12593
12558
  setSession({ status: "signed-out" });
12559
+ announceCustomerSessionChange();
12594
12560
  };
12595
12561
  if (canonical !== null) return /* @__PURE__ */ jsx15(Navigate, { to: `${canonical}${remainingSearch(search)}`, replace: true });
12596
12562
  if (view.kind === "unknown-tab") return /* @__PURE__ */ jsx15(Fragment3, { children: notFound });
@@ -12626,6 +12592,7 @@ function AccountPage({ frame: Frame, notFound }) {
12626
12592
  onSignedIn: () => {
12627
12593
  setPrefill(spendSignInPrefill());
12628
12594
  setSession({ status: "signed-in" });
12595
+ announceCustomerSessionChange();
12629
12596
  }
12630
12597
  }
12631
12598
  ) : null,