@shoppexio/storefront-react 0.1.3 → 0.1.5

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.d.ts CHANGED
@@ -826,10 +826,16 @@ interface ShoppexSdk {
826
826
  data?: SdkPage[];
827
827
  message?: string;
828
828
  }>;
829
+ /**
830
+ * On a failure, `status` is the HTTP status (404 for a page that does not
831
+ * exist) and is absent when the request never reached a server. SDK 1.0.87
832
+ * and later; earlier builds send no `status` at all.
833
+ */
829
834
  getPage(slug: string): Promise<{
830
835
  success: boolean;
831
836
  data?: SdkPage;
832
837
  message?: string;
838
+ status?: number;
833
839
  }>;
834
840
  /** Merchant-configured navigation menus, keyed by slot (header/footer/…). */
835
841
  getMenus(): Promise<{
@@ -896,7 +902,9 @@ interface ShoppexSdk {
896
902
  * One page of the same feed, newest first. `cursor` is the previous page's
897
903
  * `pagination.next_cursor`; omit it for the first page.
898
904
  */
899
- getShopReviewsPage(cursor?: string | null): Promise<{
905
+ getShopReviewsPage(cursor?: string | null, options?: {
906
+ product?: string | null;
907
+ }): Promise<{
900
908
  success: boolean;
901
909
  data?: {
902
910
  feedback: SdkShopReview[];
@@ -904,7 +912,16 @@ interface ShoppexSdk {
904
912
  next_cursor: string | null;
905
913
  has_more: boolean;
906
914
  };
915
+ /** Counts over the whole list, or over one product's reviews when filtered. */
916
+ summary?: SdkShopReviewsSummary;
917
+ /**
918
+ * The product filter the API applied, echoed back. Absent from an API that
919
+ * predates the filter and ignored it; see `loadShopReviewsPage`.
920
+ */
921
+ product?: string | null;
907
922
  };
923
+ message?: string;
924
+ status?: number;
908
925
  }>;
909
926
  /** Starts an in-page sign-in. Sends the email and nothing else. */
910
927
  requestOtp(email: string): Promise<SdkResponse<unknown>>;
@@ -1054,6 +1071,14 @@ interface ShoppexSdk {
1054
1071
  /** Lists the buyer's reseller API keys without plaintext secrets. */
1055
1072
  resellerApiKeys(): Promise<SdkResponse<unknown>>;
1056
1073
  }
1074
+ interface SdkShopReviewsSummary {
1075
+ total: number;
1076
+ positive: number;
1077
+ neutral: number;
1078
+ negative: number;
1079
+ /** 1-5 average of the scores (5/3/1), null for an empty list. */
1080
+ average_score: number | null;
1081
+ }
1057
1082
  interface SdkShopReview {
1058
1083
  id: string;
1059
1084
  /** 1-5, normalized by the SDK from the shop-feedback score (5/3/1). */
@@ -2534,6 +2559,14 @@ interface GroupCardModel {
2534
2559
  price: number;
2535
2560
  }
2536
2561
  type CatalogCardModel = ProductCardModel | GroupCardModel;
2562
+ /**
2563
+ * A card is sold out when nothing on it can be ordered: no stock at all, or no
2564
+ * option whose effective quantity range is non-empty (`quantity_min` 2 against
2565
+ * one unit left). PAYMENT PATH: the card's quick add goes through `add()`,
2566
+ * which refuses exactly these selections, so an active button here would be a
2567
+ * control that silently does nothing.
2568
+ */
2569
+ declare function isCardSoldOut(product: StorefrontProduct): boolean;
2537
2570
  interface StorefrontCatalog {
2538
2571
  cards: CatalogCardModel[];
2539
2572
  categories: StorefrontCategory[];
@@ -2880,6 +2913,8 @@ interface ShopReview {
2880
2913
  productTitle: string | null;
2881
2914
  /** True when the review is tied to a real completed purchase. */
2882
2915
  verified: boolean;
2916
+ /** Left by the shop's review automation, not written by the buyer. */
2917
+ automated: boolean;
2883
2918
  }
2884
2919
  type ShopReviewsState = {
2885
2920
  status: 'loading';
@@ -2917,14 +2952,62 @@ declare function toShopReviews(rows: readonly SdkShopReview[], labels: ShopRevie
2917
2952
  * derive a number from them.
2918
2953
  */
2919
2954
  declare function reviewsForProduct(reviews: readonly ShopReview[], productTitle: string): ShopReview[];
2955
+ /** One page of the shop's reviews, newest first. */
2956
+ interface ShopReviewsPageResult {
2957
+ reviews: ShopReview[];
2958
+ /** Pass to the next call; null on the last page. */
2959
+ nextCursor: string | null;
2960
+ /** Counts over the whole list (or the product's reviews), null when the API sent none. */
2961
+ summary: SdkShopReviewsSummary | null;
2962
+ }
2963
+ /**
2964
+ * One page of reviews from the feed, optionally for one product (its catalog
2965
+ * `id`). Throws when the request fails.
2966
+ *
2967
+ * A product page must never show another product's reviews, so a filtered
2968
+ * request also requires the API to echo the filter back: an API that predates
2969
+ * `product` ignores it and would answer with the whole shop's list.
2970
+ */
2971
+ declare function loadShopReviewsPage(options: {
2972
+ cursor?: string | null;
2973
+ product?: string | null;
2974
+ labels: ShopReviewLabels;
2975
+ }): Promise<ShopReviewsPageResult>;
2920
2976
  /**
2921
2977
  * The newest WRITTEN reviews, at most `limit`, for a sample such as a home
2922
- * section. Pages through the feed only until it has enough: a shop with a
2923
- * thousand reviews costs one request, not ten. Automated entries and ratings
2924
- * without words are skipped, since a sample of quotes needs quotes.
2925
- * Resolves null when a page request fails.
2978
+ * section or a product's review tab. Pages through the feed only until it has
2979
+ * enough. Automated entries and ratings without words are skipped, since a
2980
+ * sample of quotes needs quotes. Resolves null when a page request fails.
2981
+ */
2982
+ declare function loadLatestWrittenReviews(limit: number, labels: ShopReviewLabels, options?: {
2983
+ product?: string | null;
2984
+ }): Promise<ShopReview[] | null>;
2985
+ type PagedShopReviewsState = {
2986
+ status: 'loading';
2987
+ } | {
2988
+ status: 'error';
2989
+ } | {
2990
+ status: 'ready';
2991
+ reviews: ShopReview[];
2992
+ summary: SdkShopReviewsSummary | null;
2993
+ hasMore: boolean;
2994
+ /** True while the next page is on its way. */
2995
+ loadingMore: boolean;
2996
+ /** Set when the NEXT page failed; the pages already shown stay. */
2997
+ moreFailed: boolean;
2998
+ };
2999
+ /**
3000
+ * The shop's reviews, one page at a time: the first page on mount (or when
3001
+ * `product` changes), the next only when `loadMore` is called. The summary is
3002
+ * the API's, so the score and breakdown describe the whole list, not the pages
3003
+ * loaded so far.
2926
3004
  */
2927
- declare function loadLatestWrittenReviews(limit: number, labels: ShopReviewLabels): Promise<ShopReview[] | null>;
3005
+ declare function usePagedShopReviews(options: {
3006
+ product?: string | null;
3007
+ labels: ShopReviewLabels;
3008
+ }): PagedShopReviewsState & {
3009
+ loadMore: () => void;
3010
+ };
2928
3011
  declare function averageRating(reviews: readonly ShopReview[]): number | null;
2929
3012
  declare function useShopReviews(labels: ShopReviewLabels): ShopReviewsState;
2930
3013
 
@@ -3025,16 +3108,24 @@ type ShopPageState = {
3025
3108
  status: 'ready';
3026
3109
  page: ShopPageModel;
3027
3110
  }
3028
- /** The slug resolved to nothing, or the request failed. Same screen either way. */
3111
+ /** The API answered that no such page exists (404), or the page has no name. */
3029
3112
  | {
3030
3113
  status: 'missing';
3114
+ }
3115
+ /**
3116
+ * The page could not be loaded: the API failed or could not be reached, or
3117
+ * the page's body could not be rendered. Not "missing": the page may exist.
3118
+ */
3119
+ | {
3120
+ status: 'error';
3031
3121
  };
3032
3122
  /**
3033
3123
  * Loads one CMS page by slug.
3034
3124
  *
3035
- * A slug the API does not know, a page without a name, and a failed request all
3036
- * end in `missing`: there is no page to show, and inventing a title from the
3037
- * slug would publish a heading the merchant never wrote.
3125
+ * `missing` only when the API says so (404) or the page it returns has no name
3126
+ * to show; inventing a title from the slug would publish a heading the merchant
3127
+ * never wrote. Any other failure is `error`, so an outage never tells a buyer a
3128
+ * real page does not exist.
3038
3129
  */
3039
3130
  declare function loadShopPage(slug: string): Promise<ShopPageState>;
3040
3131
 
@@ -3115,4 +3206,4 @@ declare function useOnlineUserCount(): number | null;
3115
3206
  */
3116
3207
  declare function resolveProductVideoEmbedSrc(value: unknown): string | null;
3117
3208
 
3118
- export { ACCOUNT_ARTIFACT_CAPABILITIES, ACCOUNT_PATH, ACCOUNT_PATH_ALIAS, ACCOUNT_ROUTE_MESSAGE_KEYS, ACCOUNT_STOREFRONT_ROUTES, ACCOUNT_TABS, type AccountRouteMessageKey, type AccountRouteParts, type AccountTab, type AccountView, type BuyBoxVariantModel, CATALOG_PAGE_SIZE, CATALOG_SORTS, CATALOG_SORT_LABELS, type CartBoundsHeal, CartProvider, type CartQuoteLineBounds, type CartTotals, type CartView, type CartViewLine, type CatalogCardModel, type CatalogPage, type CatalogSort, type CheckoutFieldGroup, type CheckoutFieldValues, type CheckoutFields, type ContactFormValues, type ContactSubmissionContext, DEFAULT_ACCOUNT_TAB, DEFAULT_LOW_STOCK_THRESHOLD, DELIVERY_TIME_UNITS, type DeliveryPromise, type DeliveryTimeUnit, type GroupCardModel, IMPLEMENTED_ACCOUNT_TABS, type ImplementedAccountTab, type LicenseStatement, type LicenseTerm, type LinePricing, MAX_PAY_WHAT_YOU_WANT_PRICE, type MenuNavLink, type MenuSlot, type NavLink, type NavigationLinks, PRODUCT_BADGE_ICON_NAMES, type ProductAddon, type ProductAutomaticDeal, type ProductBadge, type ProductBadgeIconName, type ProductBundle, type ProductBundleItem, type ProductCardModel, type ProductCategoryRef, type ProductContentTab, type ProductCustomField, type ProductDetailModel, type ProductFaq, type ProductRatingModel, type ProductSubscription, type ProductType, QUOTE_UNAVAILABLE_CHECKOUT_ERROR, type RatingStars, type ResellerProgramSummary, type RewardProgramItem, type RewardProgramSummary, SERVING_CONTEXT_HINT, SHOP_REVIEW_PAGE_SIZE, STOREFRONT_SORT_KEYS, type SdkCartAddOptions, type SdkCartAddon, type SdkCartAppliedDiscount, type SdkCartCodeSource, type SdkCartItem, type SdkCartQuote, type SdkCartQuoteLine, type SdkCheckoutResult, type SdkCustomerAffiliateLink, type SdkCustomerAffiliateSummary, type SdkCustomerDashboard, type SdkCustomerDashboardOrder, type SdkCustomerDeliverable, type SdkCustomerDeliveryStatus, type SdkCustomerDeliverySummary, type SdkCustomerLicense, type SdkCustomerLoyalty, type SdkCustomerLoyaltyRedeem, type SdkCustomerLoyaltyTransactionType, type SdkCustomerOrderDetail, type SdkCustomerOrderPage, type SdkCustomerOrderSummary, type SdkCustomerRewardActivity, type SdkCustomerRewards, type SdkCustomerSubscription, type SdkCustomerTicketSummary, type SdkCustomerWarranty, type SdkCustomerWarrantyClaim, type SdkCustomerWarrantyClaimStatus, type SdkCustomerWarrantyList, type SdkCustomerWarrantyStatus, type SdkEligibleCartDeal, type SdkInitOptions, type SdkMenu, type SdkMenuItem, type SdkPage, type SdkRecentSaleEntry, type SdkResponse, type SdkShopReview, type ShopPageFaqItem, type ShopPageModel, type ShopPageState, type ShopReview, type ShopReviewLabels, type ShopReviewsState, type ShoppexSdk, type SocialLink, type SocialNetwork, type StockChip, type StockTone, type StorefrontAddon, type StorefrontCatalog, type StorefrontCategory, type StorefrontData, type StorefrontGroup, type StorefrontListingItem, type StorefrontPriceVariant, type StorefrontProduct, StorefrontProvider, type StorefrontSettings, type StorefrontShop, type StorefrontShopStats, type StorefrontSortKey, type SubscriptionIntervalUnit, type SupportChannel, type VolumeDiscountRule, accountOrderHref, accountRouteMessage, accountTabHref, accountTabLabelKey, averageRating, badgesForCard, badgesForProductPage, buildCartTotals, buildCartView, buildCustomFieldPayload, buildInitialCheckoutFieldValues, buildProductDetail, buildStorefrontCatalog, calculateDisplayLineTotal, calculateLinePricing, canonicalAccountPath, checkoutFieldKey, checkoutFieldWriteRepriced, checkoutNeedsDetails, clampQuantity, clearRecoveredQuoteCheckoutError, filterCatalogCards, findStorefrontProduct, firstOptionalString, flattenMerchantHtmlToText, formatCompactCount, formatDeliveryTime, formatLicenseTerm, formatMoney, formatSubscriptionCadence, formatSubscriptionTrial, getAuthoredContent, getBuyerCurrency, getProductGroups, getProducts, getShoppex, getStorefrontAddons, getStorefrontCategories, getStorefrontListing, getStorefrontSettings, getStorefrontShop, hasInjectedStorefrontData, isAccountTab, isBlankMerchantHtml, isCheckboxValueChecked, isImplementedAccountTab, isProductSoldOut, isRecord, isSelectionSoldOut, loadLatestWrittenReviews, loadShopPage, missingRequiredCheckoutValues, optionalBoolean, optionalString, optionalStringWithDefault, paginateCatalogCards, parseAccountPath, parseCartQuoteLines, parseEnum, parseFiniteNumber, parseProductBadges, parseStorefrontSortKey, readCheckoutNeedsDetails, readStorefrontData, requireArray, requireBoolean, requireNumber, requireRecord, requireString, requireStringFromKeys, requireUnknownRecord, resolveAccountView, resolveBadgeForeground, resolveCartBoundsHeals, resolveCategorySelection, resolveCheckoutFields, resolveDeliveryPromise, resolveDisplayStock, resolveHideStockCounter, resolveLicenseTerm, resolveMenuLinksForSlot, resolvePayWhatYouWantMinimum, resolveProductVideoEmbedSrc, resolveQuantityBounds, resolveSupportChannels, resolveUnsellableLineIds, resolveVariantStock, reviewsForProduct, roundCurrency, sanitizeMerchantHtml, sortCatalogCards, startCartMutation, stripMerchantHtmlImages, submitContactTicket, toShopReviews, toStarCount, useCart, useNavigationLinks, useOnlineUserCount, useShopReviews, useStorefront, validateCheckoutFieldValue, validateCheckoutFields, validateContactSubmission, withRequiredAddons };
3209
+ export { ACCOUNT_ARTIFACT_CAPABILITIES, ACCOUNT_PATH, ACCOUNT_PATH_ALIAS, ACCOUNT_ROUTE_MESSAGE_KEYS, ACCOUNT_STOREFRONT_ROUTES, ACCOUNT_TABS, type AccountRouteMessageKey, type AccountRouteParts, type AccountTab, type AccountView, type BuyBoxVariantModel, CATALOG_PAGE_SIZE, CATALOG_SORTS, CATALOG_SORT_LABELS, type CartBoundsHeal, CartProvider, type CartQuoteLineBounds, type CartTotals, type CartView, type CartViewLine, type CatalogCardModel, type CatalogPage, type CatalogSort, type CheckoutFieldGroup, type CheckoutFieldValues, type CheckoutFields, type ContactFormValues, type ContactSubmissionContext, DEFAULT_ACCOUNT_TAB, DEFAULT_LOW_STOCK_THRESHOLD, DELIVERY_TIME_UNITS, type DeliveryPromise, type DeliveryTimeUnit, type GroupCardModel, IMPLEMENTED_ACCOUNT_TABS, type ImplementedAccountTab, type LicenseStatement, type LicenseTerm, type LinePricing, MAX_PAY_WHAT_YOU_WANT_PRICE, type MenuNavLink, type MenuSlot, type NavLink, type NavigationLinks, PRODUCT_BADGE_ICON_NAMES, type PagedShopReviewsState, type ProductAddon, type ProductAutomaticDeal, type ProductBadge, type ProductBadgeIconName, type ProductBundle, type ProductBundleItem, type ProductCardModel, type ProductCategoryRef, type ProductContentTab, type ProductCustomField, type ProductDetailModel, type ProductFaq, type ProductRatingModel, type ProductSubscription, type ProductType, QUOTE_UNAVAILABLE_CHECKOUT_ERROR, type RatingStars, type ResellerProgramSummary, type RewardProgramItem, type RewardProgramSummary, SERVING_CONTEXT_HINT, SHOP_REVIEW_PAGE_SIZE, STOREFRONT_SORT_KEYS, type SdkCartAddOptions, type SdkCartAddon, type SdkCartAppliedDiscount, type SdkCartCodeSource, type SdkCartItem, type SdkCartQuote, type SdkCartQuoteLine, type SdkCheckoutResult, type SdkCustomerAffiliateLink, type SdkCustomerAffiliateSummary, type SdkCustomerDashboard, type SdkCustomerDashboardOrder, type SdkCustomerDeliverable, type SdkCustomerDeliveryStatus, type SdkCustomerDeliverySummary, type SdkCustomerLicense, type SdkCustomerLoyalty, type SdkCustomerLoyaltyRedeem, type SdkCustomerLoyaltyTransactionType, type SdkCustomerOrderDetail, type SdkCustomerOrderPage, type SdkCustomerOrderSummary, type SdkCustomerRewardActivity, type SdkCustomerRewards, type SdkCustomerSubscription, type SdkCustomerTicketSummary, type SdkCustomerWarranty, type SdkCustomerWarrantyClaim, type SdkCustomerWarrantyClaimStatus, type SdkCustomerWarrantyList, type SdkCustomerWarrantyStatus, type SdkEligibleCartDeal, type SdkInitOptions, type SdkMenu, type SdkMenuItem, type SdkPage, type SdkRecentSaleEntry, type SdkResponse, type SdkShopReview, type SdkShopReviewsSummary, type ShopPageFaqItem, type ShopPageModel, type ShopPageState, type ShopReview, type ShopReviewLabels, type ShopReviewsPageResult, type ShopReviewsState, type ShoppexSdk, type SocialLink, type SocialNetwork, type StockChip, type StockTone, type StorefrontAddon, type StorefrontCatalog, type StorefrontCategory, type StorefrontData, type StorefrontGroup, type StorefrontListingItem, type StorefrontPriceVariant, type StorefrontProduct, StorefrontProvider, type StorefrontSettings, type StorefrontShop, type StorefrontShopStats, type StorefrontSortKey, type SubscriptionIntervalUnit, type SupportChannel, type VolumeDiscountRule, accountOrderHref, accountRouteMessage, accountTabHref, accountTabLabelKey, averageRating, badgesForCard, badgesForProductPage, buildCartTotals, buildCartView, buildCustomFieldPayload, buildInitialCheckoutFieldValues, buildProductDetail, buildStorefrontCatalog, calculateDisplayLineTotal, calculateLinePricing, canonicalAccountPath, checkoutFieldKey, checkoutFieldWriteRepriced, checkoutNeedsDetails, clampQuantity, clearRecoveredQuoteCheckoutError, filterCatalogCards, findStorefrontProduct, firstOptionalString, flattenMerchantHtmlToText, formatCompactCount, formatDeliveryTime, formatLicenseTerm, formatMoney, formatSubscriptionCadence, formatSubscriptionTrial, getAuthoredContent, getBuyerCurrency, getProductGroups, getProducts, getShoppex, getStorefrontAddons, getStorefrontCategories, getStorefrontListing, getStorefrontSettings, getStorefrontShop, hasInjectedStorefrontData, isAccountTab, isBlankMerchantHtml, isCardSoldOut, isCheckboxValueChecked, isImplementedAccountTab, isProductSoldOut, isRecord, isSelectionSoldOut, loadLatestWrittenReviews, loadShopPage, loadShopReviewsPage, missingRequiredCheckoutValues, optionalBoolean, optionalString, optionalStringWithDefault, paginateCatalogCards, parseAccountPath, parseCartQuoteLines, parseEnum, parseFiniteNumber, parseProductBadges, parseStorefrontSortKey, readCheckoutNeedsDetails, readStorefrontData, requireArray, requireBoolean, requireNumber, requireRecord, requireString, requireStringFromKeys, requireUnknownRecord, resolveAccountView, resolveBadgeForeground, resolveCartBoundsHeals, resolveCategorySelection, resolveCheckoutFields, resolveDeliveryPromise, resolveDisplayStock, resolveHideStockCounter, resolveLicenseTerm, resolveMenuLinksForSlot, resolvePayWhatYouWantMinimum, resolveProductVideoEmbedSrc, resolveQuantityBounds, resolveSupportChannels, resolveUnsellableLineIds, resolveVariantStock, reviewsForProduct, roundCurrency, sanitizeMerchantHtml, sortCatalogCards, startCartMutation, stripMerchantHtmlImages, submitContactTicket, toShopReviews, toStarCount, useCart, useNavigationLinks, useOnlineUserCount, usePagedShopReviews, useShopReviews, useStorefront, validateCheckoutFieldValue, validateCheckoutFields, validateContactSubmission, withRequiredAddons };
package/dist/index.js CHANGED
@@ -1473,8 +1473,12 @@ function resolvePriceLabel(product) {
1473
1473
  }
1474
1474
  return formatMoney(product.price, product.currency);
1475
1475
  }
1476
+ function isCardSoldOut(product) {
1477
+ if (isProductSoldOut(product)) return true;
1478
+ return product.priceVariants.length > 0 ? product.priceVariants.every((variant) => isSelectionSoldOut(product, variant)) : isSelectionSoldOut(product, null);
1479
+ }
1476
1480
  function buildProductCard(product, settings, sortPriority) {
1477
- const soldOut = isProductSoldOut(product);
1481
+ const soldOut = isCardSoldOut(product);
1478
1482
  const isInfoCard = product.productType === "INFO_CARD";
1479
1483
  const stock = resolveDisplayStock(product);
1480
1484
  const hideCounter = resolveHideStockCounter(product, settings);
@@ -2538,7 +2542,7 @@ function useNavigationLinks(slot, themeLinks, authored) {
2538
2542
  }
2539
2543
 
2540
2544
  // src/reviews.ts
2541
- import { useEffect as useEffect3, useState as useState3 } from "react";
2545
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
2542
2546
  var SHOP_REVIEW_PAGE_SIZE = 12;
2543
2547
  function toTimestamp(value) {
2544
2548
  if (typeof value === "number" && Number.isFinite(value)) {
@@ -2578,7 +2582,8 @@ function toShopReviews(rows, labels) {
2578
2582
  dateLabel: formatDateLabel(timestamp),
2579
2583
  timestamp,
2580
2584
  productTitle: row.product_title?.trim() || null,
2581
- verified: row.verified === true
2585
+ verified: row.verified === true,
2586
+ automated: row.is_automated === true
2582
2587
  };
2583
2588
  }).sort((a, b) => b.timestamp - a.timestamp);
2584
2589
  }
@@ -2587,20 +2592,83 @@ function reviewsForProduct(reviews, productTitle) {
2587
2592
  if (!title) return [];
2588
2593
  return reviews.filter((review) => review.productTitle === title);
2589
2594
  }
2590
- async function loadLatestWrittenReviews(limit, labels) {
2595
+ async function loadShopReviewsPage(options) {
2596
+ const product = options.product?.trim() || null;
2597
+ const result = await getShoppex().getShopReviewsPage(options.cursor ?? null, product ? { product } : void 0);
2598
+ if (!result.success || !result.data) {
2599
+ throw new Error(result.message?.trim() || "Reviews could not be loaded.");
2600
+ }
2601
+ if (product !== null && result.data.product !== product) {
2602
+ throw new Error("This store does not filter reviews by product yet.");
2603
+ }
2604
+ const { pagination } = result.data;
2605
+ return {
2606
+ reviews: toShopReviews(result.data.feedback, options.labels),
2607
+ nextCursor: pagination.has_more ? pagination.next_cursor : null,
2608
+ summary: result.data.summary ?? null
2609
+ };
2610
+ }
2611
+ async function loadLatestWrittenReviews(limit, labels, options = {}) {
2591
2612
  const written = [];
2592
2613
  const seen = /* @__PURE__ */ new Set();
2593
2614
  let cursor = null;
2594
- while (written.length < limit) {
2595
- const result = await getShoppex().getShopReviewsPage(cursor);
2596
- if (!result.success || !result.data) return null;
2597
- written.push(...result.data.feedback.filter((row) => !row.is_automated && (row.comment ?? row.message ?? "").trim() !== ""));
2598
- const next = result.data.pagination.has_more ? result.data.pagination.next_cursor : null;
2599
- if (!next || seen.has(next)) break;
2600
- seen.add(next);
2601
- cursor = next;
2615
+ try {
2616
+ while (written.length < limit) {
2617
+ const page = await loadShopReviewsPage({ cursor, product: options.product, labels: { anonymousAuthor: labels.anonymousAuthor, emptyComment: "" } });
2618
+ written.push(...page.reviews.filter((review) => !review.automated && review.comment !== ""));
2619
+ if (!page.nextCursor || seen.has(page.nextCursor)) break;
2620
+ seen.add(page.nextCursor);
2621
+ cursor = page.nextCursor;
2622
+ }
2623
+ } catch {
2624
+ return null;
2602
2625
  }
2603
- return toShopReviews(written, labels).slice(0, limit);
2626
+ return written.slice(0, limit).map((review) => ({ ...review, comment: review.comment || labels.emptyComment }));
2627
+ }
2628
+ function usePagedShopReviews(options) {
2629
+ const product = options.product?.trim() || null;
2630
+ const { anonymousAuthor, emptyComment } = options.labels;
2631
+ const [state, setState] = useState3({ status: "loading" });
2632
+ const [cursor, setCursor] = useState3(null);
2633
+ const generation = useRef2(0);
2634
+ useEffect3(() => {
2635
+ const current = generation.current += 1;
2636
+ setState({ status: "loading" });
2637
+ setCursor(null);
2638
+ loadShopReviewsPage({ product, labels: { anonymousAuthor, emptyComment } }).then((page) => {
2639
+ if (generation.current !== current) return;
2640
+ setCursor(page.nextCursor);
2641
+ setState({
2642
+ status: "ready",
2643
+ reviews: page.reviews,
2644
+ summary: page.summary,
2645
+ hasMore: page.nextCursor !== null,
2646
+ loadingMore: false,
2647
+ moreFailed: false
2648
+ });
2649
+ }).catch(() => {
2650
+ if (generation.current === current) setState({ status: "error" });
2651
+ });
2652
+ }, [product, anonymousAuthor, emptyComment]);
2653
+ const loadMore = useCallback2(() => {
2654
+ if (state.status !== "ready" || state.loadingMore || cursor === null) return;
2655
+ const current = generation.current;
2656
+ setState({ ...state, loadingMore: true, moreFailed: false });
2657
+ loadShopReviewsPage({ cursor, product, labels: { anonymousAuthor, emptyComment } }).then((page) => {
2658
+ if (generation.current !== current) return;
2659
+ setCursor(page.nextCursor);
2660
+ setState((previous) => previous.status !== "ready" ? previous : {
2661
+ ...previous,
2662
+ reviews: [...previous.reviews, ...page.reviews],
2663
+ hasMore: page.nextCursor !== null,
2664
+ loadingMore: false
2665
+ });
2666
+ }).catch(() => {
2667
+ if (generation.current !== current) return;
2668
+ setState((previous) => previous.status !== "ready" ? previous : { ...previous, loadingMore: false, moreFailed: true });
2669
+ });
2670
+ }, [state, cursor, product, anonymousAuthor, emptyComment]);
2671
+ return { ...state, loadMore };
2604
2672
  }
2605
2673
  function averageRating(reviews) {
2606
2674
  if (reviews.length === 0) return null;
@@ -2659,15 +2727,19 @@ async function buildShopPageModel(page) {
2659
2727
  };
2660
2728
  }
2661
2729
  async function loadShopPage(slug) {
2730
+ let result;
2731
+ try {
2732
+ result = await getShoppex().getPage(slug);
2733
+ } catch {
2734
+ return { status: "error" };
2735
+ }
2736
+ if (!result.success) return result.status === 404 ? { status: "missing" } : { status: "error" };
2737
+ const page = result.data;
2738
+ if (!page || typeof page.name !== "string" || !page.name.trim()) return { status: "missing" };
2662
2739
  try {
2663
- const result = await getShoppex().getPage(slug);
2664
- const page = result.success ? result.data : void 0;
2665
- if (!page || typeof page.name !== "string" || !page.name.trim()) {
2666
- return { status: "missing" };
2667
- }
2668
2740
  return { status: "ready", page: await buildShopPageModel(page) };
2669
2741
  } catch {
2670
- return { status: "missing" };
2742
+ return { status: "error" };
2671
2743
  }
2672
2744
  }
2673
2745
 
@@ -2769,6 +2841,7 @@ export {
2769
2841
  hasInjectedStorefrontData,
2770
2842
  isAccountTab,
2771
2843
  isBlankMerchantHtml,
2844
+ isCardSoldOut,
2772
2845
  isCheckboxValueChecked,
2773
2846
  isImplementedAccountTab,
2774
2847
  isProductSoldOut,
@@ -2776,6 +2849,7 @@ export {
2776
2849
  isSelectionSoldOut,
2777
2850
  loadLatestWrittenReviews,
2778
2851
  loadShopPage,
2852
+ loadShopReviewsPage,
2779
2853
  missingRequiredCheckoutValues,
2780
2854
  optionalBoolean,
2781
2855
  optionalString,
@@ -2824,6 +2898,7 @@ export {
2824
2898
  useCart,
2825
2899
  useNavigationLinks,
2826
2900
  useOnlineUserCount,
2901
+ usePagedShopReviews,
2827
2902
  useShopReviews,
2828
2903
  useStorefront,
2829
2904
  validateCheckoutFieldValue,