@unbox-plus/cli 0.20.4
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/LICENSE.md +46 -0
- package/README.md +175 -0
- package/bin/cli.js +439 -0
- package/package.json +43 -0
- package/src/colors.js +100 -0
- package/src/presets.js +241 -0
- package/src/theme.js +253 -0
- package/template/.claude/agents/assets-marca.md +40 -0
- package/template/.claude/agents/avaliador-visual.md +48 -0
- package/template/.claude/agents/branding-briefing.md +401 -0
- package/template/.claude/agents/conteudo-secao.md +46 -0
- package/template/.claude/agents/paginas-legais.md +44 -0
- package/template/.claude/settings.json +3 -0
- package/template/.claude/skills/frontend-design/ATTRIBUTION.md +3 -0
- package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/template/.claude/skills/frontend-design/SKILL.md +55 -0
- package/template/.claude/skills/web-design-guidelines/ATTRIBUTION.md +3 -0
- package/template/.claude/skills/web-design-guidelines/SKILL.md +50 -0
- package/template/.claude/skills/web-design-guidelines/guidelines-snapshot.md +180 -0
- package/template/.env.example +158 -0
- package/template/.mcp.json.example +14 -0
- package/template/.nvmrc +1 -0
- package/template/CLAUDE.md +333 -0
- package/template/COVERAGE.md +80 -0
- package/template/DEPLOY.md +179 -0
- package/template/QA.md +108 -0
- package/template/README.md +152 -0
- package/template/agents/CONSTRUCAO.md +207 -0
- package/template/agents/MANAGER.md +192 -0
- package/template/agents/PADROES.md +255 -0
- package/template/agents/definitions/00-scaffold.md +56 -0
- package/template/agents/definitions/01-layout.md +47 -0
- package/template/agents/definitions/02-homepage.md +70 -0
- package/template/agents/definitions/03-catalog.md +86 -0
- package/template/agents/definitions/04-pdp.md +97 -0
- package/template/agents/definitions/05-cart.md +116 -0
- package/template/agents/definitions/06-checkout.md +146 -0
- package/template/agents/definitions/07-auth.md +130 -0
- package/template/agents/definitions/08-customer.md +55 -0
- package/template/agents/definitions/09-promotions.md +62 -0
- package/template/agents/definitions/10-feedback.md +128 -0
- package/template/agents/definitions/11-seo-infra.md +159 -0
- package/template/agents/definitions/12-deploy.md +173 -0
- package/template/agents/definitions/13-cro.md +275 -0
- package/template/agents/definitions/14-seo.md +388 -0
- package/template/agents/definitions/15-branding.md +26 -0
- package/template/agents/definitions/16-qa-visual.md +138 -0
- package/template/agents/definitions/17-aeo.md +257 -0
- package/template/app/(loja)/busca/page.tsx +72 -0
- package/template/app/(loja)/carrinho/oferta/page.tsx +57 -0
- package/template/app/(loja)/carrinho/page.tsx +160 -0
- package/template/app/(loja)/categoria/[tagSlug]/layout.tsx +31 -0
- package/template/app/(loja)/categoria/[tagSlug]/loading.tsx +17 -0
- package/template/app/(loja)/categoria/[tagSlug]/page.tsx +49 -0
- package/template/app/(loja)/checkout/page.tsx +80 -0
- package/template/app/(loja)/checkout/pix/[ref]/page.tsx +133 -0
- package/template/app/(loja)/conta/assinaturas/[referenceId]/page.tsx +112 -0
- package/template/app/(loja)/conta/assinaturas/page.tsx +51 -0
- package/template/app/(loja)/conta/enderecos/page.tsx +20 -0
- package/template/app/(loja)/conta/entrar/page.tsx +119 -0
- package/template/app/(loja)/conta/page.tsx +45 -0
- package/template/app/(loja)/conta/pedidos/[referenceId]/page.tsx +49 -0
- package/template/app/(loja)/conta/pedidos/page.tsx +104 -0
- package/template/app/(loja)/conta/preferencias/page.tsx +24 -0
- package/template/app/(loja)/devolucoes/page.tsx +36 -0
- package/template/app/(loja)/layout.tsx +22 -0
- package/template/app/(loja)/oferta/page.tsx +57 -0
- package/template/app/(loja)/page.tsx +86 -0
- package/template/app/(loja)/pedido/[referenceId]/page.tsx +78 -0
- package/template/app/(loja)/privacidade/page.tsx +113 -0
- package/template/app/(loja)/produto/[productSlug]/layout.tsx +23 -0
- package/template/app/(loja)/produto/[productSlug]/loading.tsx +20 -0
- package/template/app/(loja)/produto/[productSlug]/page.tsx +372 -0
- package/template/app/(loja)/produtos/loading.tsx +16 -0
- package/template/app/(loja)/produtos/page.tsx +33 -0
- package/template/app/(loja)/termos/page.tsx +104 -0
- package/template/app/acesso/page.tsx +129 -0
- package/template/app/api/account/addresses/route.ts +39 -0
- package/template/app/api/account/exists/route.ts +21 -0
- package/template/app/api/account/me/route.ts +25 -0
- package/template/app/api/account/otp/route.ts +26 -0
- package/template/app/api/account/preferences/route.ts +19 -0
- package/template/app/api/account/signin/route.ts +27 -0
- package/template/app/api/account/signout/route.ts +9 -0
- package/template/app/api/acesso/route.ts +171 -0
- package/template/app/api/capi/route.ts +34 -0
- package/template/app/api/cart/coupon/route.ts +49 -0
- package/template/app/api/cart/items/route.ts +45 -0
- package/template/app/api/cart/link/route.ts +69 -0
- package/template/app/api/cart/route.ts +113 -0
- package/template/app/api/cart/share/route.ts +21 -0
- package/template/app/api/cep/[code]/route.ts +18 -0
- package/template/app/api/checkout/address/route.ts +24 -0
- package/template/app/api/checkout/email/route.ts +41 -0
- package/template/app/api/checkout/installments/route.ts +16 -0
- package/template/app/api/checkout/route.ts +110 -0
- package/template/app/api/checkout/shipping/route.ts +65 -0
- package/template/app/api/checkout-destination/route.ts +31 -0
- package/template/app/api/order/[ref]/route.ts +17 -0
- package/template/app/api/payment-link/route.ts +28 -0
- package/template/app/api/revalidate/route.ts +52 -0
- package/template/app/api/shipping/quote/route.ts +29 -0
- package/template/app/api/subscriptions/[id]/route.ts +52 -0
- package/template/app/api/track/route.ts +28 -0
- package/template/app/api/unbox/catalogo/route.ts +136 -0
- package/template/app/api/unbox/paginas/route.ts +167 -0
- package/template/app/api/unbox/vitrine/route.ts +84 -0
- package/template/app/api/webhooks/unbox/route.ts +97 -0
- package/template/app/apple-icon.svg +5 -0
- package/template/app/error.tsx +15 -0
- package/template/app/globals.css +457 -0
- package/template/app/icon.svg +4 -0
- package/template/app/layout.tsx +90 -0
- package/template/app/llms.txt/route.ts +43 -0
- package/template/app/manifest.ts +21 -0
- package/template/app/not-found.tsx +18 -0
- package/template/app/opengraph-image.tsx +35 -0
- package/template/app/robots.ts +31 -0
- package/template/app/sitemap.ts +42 -0
- package/template/bootstrap.sh +87 -0
- package/template/components/account/account-shell.tsx +47 -0
- package/template/components/account/address-book.tsx +147 -0
- package/template/components/account/preferences-form.tsx +77 -0
- package/template/components/account/reorder-button.tsx +47 -0
- package/template/components/account/signout-button.tsx +20 -0
- package/template/components/account/subscription-actions.tsx +204 -0
- package/template/components/account-nav.tsx +41 -0
- package/template/components/address-fields.tsx +102 -0
- package/template/components/analytics/data-layer-ready.tsx +16 -0
- package/template/components/analytics/purchase-tracker.tsx +32 -0
- package/template/components/brand-search.tsx +34 -0
- package/template/components/cart/cart-provider.tsx +340 -0
- package/template/components/cart/mini-cart.tsx +228 -0
- package/template/components/cart-button.tsx +35 -0
- package/template/components/catalog/catalog-client.tsx +523 -0
- package/template/components/catalog/grid-skeleton.tsx +19 -0
- package/template/components/catalog/pager.tsx +66 -0
- package/template/components/catalog/product-grid-card.tsx +52 -0
- package/template/components/catalog/product-grid.tsx +27 -0
- package/template/components/checkout/checkout-client.tsx +1333 -0
- package/template/components/chrome/announce-bar.tsx +57 -0
- package/template/components/chrome/chrome-recipe.ts +13 -0
- package/template/components/chrome/footers/captura-botao.tsx +28 -0
- package/template/components/chrome/footers/colunas.tsx +110 -0
- package/template/components/chrome/footers/conversao.tsx +90 -0
- package/template/components/chrome/footers/editorial.tsx +98 -0
- package/template/components/chrome/footers/minimal.tsx +55 -0
- package/template/components/chrome/header-bar-mobile.tsx +64 -0
- package/template/components/chrome/headers/centralizado.tsx +71 -0
- package/template/components/chrome/headers/classico.tsx +64 -0
- package/template/components/chrome/headers/compacto.tsx +66 -0
- package/template/components/chrome/headers/equilibrado.tsx +73 -0
- package/template/components/chrome/headers/imersivo.tsx +62 -0
- package/template/components/chrome/registry.ts +70 -0
- package/template/components/chrome/solid-on-scroll.tsx +39 -0
- package/template/components/empty-state.tsx +22 -0
- package/template/components/home/combo-card-compact.tsx +67 -0
- package/template/components/home/combos-home.tsx +64 -0
- package/template/components/home/combos-section.tsx +177 -0
- package/template/components/home/home-recipe.ts +41 -0
- package/template/components/home/sections/attributes-marquee.tsx +178 -0
- package/template/components/home/sections/benefits.tsx +80 -0
- package/template/components/home/sections/bloco-html.tsx +47 -0
- package/template/components/home/sections/catalogo.ts +165 -0
- package/template/components/home/sections/category-pills.tsx +32 -0
- package/template/components/home/sections/combos-carousel.tsx +141 -0
- package/template/components/home/sections/comparison.tsx +73 -0
- package/template/components/home/sections/founder-story.tsx +40 -0
- package/template/components/home/sections/hero.tsx +187 -0
- package/template/components/home/sections/kits.tsx +11 -0
- package/template/components/home/sections/media-cards.tsx +70 -0
- package/template/components/home/sections/newsletter.tsx +62 -0
- package/template/components/home/sections/product-showcase.tsx +175 -0
- package/template/components/home/sections/purchase-hero.tsx +187 -0
- package/template/components/home/sections/quote-banner.tsx +50 -0
- package/template/components/home/sections/registry.ts +186 -0
- package/template/components/home/sections/reviews-carousel.tsx +66 -0
- package/template/components/home/sections/reviews.tsx +68 -0
- package/template/components/home/sections/ritual.tsx +96 -0
- package/template/components/home/sections/savings.tsx +47 -0
- package/template/components/home/sections/social-row.tsx +65 -0
- package/template/components/home/sections/spec-table.tsx +117 -0
- package/template/components/home/sections/stars.tsx +12 -0
- package/template/components/home/sections/stats-grid.tsx +54 -0
- package/template/components/home/sections/trust-bar.tsx +56 -0
- package/template/components/home/sections/trust-strip.tsx +40 -0
- package/template/components/home/sections/video-wall.tsx +59 -0
- package/template/components/landing/landing-recipe.ts +20 -0
- package/template/components/landing/oferta-sections.tsx +40 -0
- package/template/components/landing/product-picker.tsx +257 -0
- package/template/components/mobile-nav.tsx +71 -0
- package/template/components/order-status.tsx +163 -0
- package/template/components/powered-by-unbox.tsx +27 -0
- package/template/components/product/pdp/buy-box.tsx +604 -0
- package/template/components/product/pdp/catalog-grid.tsx +113 -0
- package/template/components/product/pdp/faq-modelo.ts +86 -0
- package/template/components/product/pdp/gallery.tsx +87 -0
- package/template/components/product/pdp/interactive.tsx +200 -0
- package/template/components/product/pdp/newsletter.tsx +64 -0
- package/template/components/product/pdp/payment-chips.tsx +22 -0
- package/template/components/product/pdp/pdp-view.tsx +227 -0
- package/template/components/product/pdp/recommendations.tsx +144 -0
- package/template/components/product/pdp/sections.tsx +173 -0
- package/template/components/quantity-stepper.tsx +36 -0
- package/template/components/search-box.tsx +37 -0
- package/template/components/site-footer.tsx +59 -0
- package/template/components/site-header.tsx +64 -0
- package/template/components/ui/accordion.tsx +72 -0
- package/template/components/ui/alert-dialog.tsx +187 -0
- package/template/components/ui/alert.tsx +76 -0
- package/template/components/ui/aspect-ratio.tsx +22 -0
- package/template/components/ui/avatar.tsx +109 -0
- package/template/components/ui/badge.tsx +52 -0
- package/template/components/ui/breadcrumb.tsx +125 -0
- package/template/components/ui/button.tsx +58 -0
- package/template/components/ui/card.tsx +103 -0
- package/template/components/ui/carousel.tsx +242 -0
- package/template/components/ui/command.tsx +196 -0
- package/template/components/ui/dialog.tsx +160 -0
- package/template/components/ui/dropdown-menu.tsx +268 -0
- package/template/components/ui/foto.tsx +54 -0
- package/template/components/ui/input-group.tsx +158 -0
- package/template/components/ui/input-otp.tsx +87 -0
- package/template/components/ui/input.tsx +20 -0
- package/template/components/ui/label.tsx +20 -0
- package/template/components/ui/navigation-menu.tsx +168 -0
- package/template/components/ui/pagination.tsx +132 -0
- package/template/components/ui/popover.tsx +90 -0
- package/template/components/ui/progress.tsx +83 -0
- package/template/components/ui/radio-group.tsx +38 -0
- package/template/components/ui/scroll-area.tsx +55 -0
- package/template/components/ui/select.tsx +201 -0
- package/template/components/ui/separator.tsx +25 -0
- package/template/components/ui/sheet.tsx +138 -0
- package/template/components/ui/skeleton.tsx +13 -0
- package/template/components/ui/sonner.tsx +44 -0
- package/template/components/ui/switch.tsx +32 -0
- package/template/components/ui/table.tsx +116 -0
- package/template/components/ui/tabs.tsx +82 -0
- package/template/components/ui/textarea.tsx +18 -0
- package/template/components/ui/toggle-group.tsx +89 -0
- package/template/components/ui/toggle.tsx +45 -0
- package/template/components/ui/tooltip.tsx +66 -0
- package/template/components.json +25 -0
- package/template/eslint.config.mjs +36 -0
- package/template/gitignore +21 -0
- package/template/lib/analytics.ts +369 -0
- package/template/lib/api.ts +60 -0
- package/template/lib/capi.ts +87 -0
- package/template/lib/cart-link.ts +51 -0
- package/template/lib/cart-normalize.ts +142 -0
- package/template/lib/cart-recovery.ts +23 -0
- package/template/lib/cart-response.ts +48 -0
- package/template/lib/catalog-map.ts +54 -0
- package/template/lib/catalog.ts +3 -0
- package/template/lib/checkout-lock.ts +21 -0
- package/template/lib/checkout-nav.ts +22 -0
- package/template/lib/config.ts +51 -0
- package/template/lib/crm.ts +40 -0
- package/template/lib/customer-session.ts +21 -0
- package/template/lib/dataloader.ts +23 -0
- package/template/lib/editable/config.ts +11 -0
- package/template/lib/editable/document.ts +1762 -0
- package/template/lib/editable/index.ts +3 -0
- package/template/lib/editable/primitives.tsx +821 -0
- package/template/lib/editable/provider.tsx +861 -0
- package/template/lib/editable/rastreio-navegacao.tsx +65 -0
- package/template/lib/editable/rastreio.tsx +168 -0
- package/template/lib/editable/server.ts +189 -0
- package/template/lib/editable/tokens.ts +22 -0
- package/template/lib/editable/verify.ts +24 -0
- package/template/lib/enrichment/combos.ts +184 -0
- package/template/lib/enrichment/index.ts +188 -0
- package/template/lib/enrichment/products.json +1 -0
- package/template/lib/env-check.ts +47 -0
- package/template/lib/format.ts +130 -0
- package/template/lib/icons.ts +67 -0
- package/template/lib/json-ld.ts +9 -0
- package/template/lib/llms-txt.ts +75 -0
- package/template/lib/mockup.ts +24 -0
- package/template/lib/newsletter.ts +12 -0
- package/template/lib/orders.ts +160 -0
- package/template/lib/queries.ts +66 -0
- package/template/lib/ratelimit.ts +47 -0
- package/template/lib/rotas-editaveis.ts +192 -0
- package/template/lib/sanitize.ts +36 -0
- package/template/lib/schemas.ts +78 -0
- package/template/lib/session.ts +98 -0
- package/template/lib/store-config.ts +67 -0
- package/template/lib/unbox/client.ts +829 -0
- package/template/lib/unbox/customer.ts +224 -0
- package/template/lib/unbox/errors.ts +97 -0
- package/template/lib/unbox/index.ts +10 -0
- package/template/lib/unbox/store.ts +113 -0
- package/template/lib/unbox/types.ts +195 -0
- package/template/lib/unbox/webhooks.ts +66 -0
- package/template/lib/utils.ts +6 -0
- package/template/lib/vitrine.ts +242 -0
- package/template/lib/webhook-store.ts +22 -0
- package/template/middleware.ts +178 -0
- package/template/next.config.ts +63 -0
- package/template/package.json +62 -0
- package/template/postcss.config.mjs +5 -0
- package/template/public/brand/coll/default.png +0 -0
- package/template/public/brand/hero-desktop.svg +12 -0
- package/template/public/brand/hero-mobile.svg +12 -0
- package/template/public/brand/heros/boutique-desktop.svg +11 -0
- package/template/public/brand/heros/boutique-mobile.svg +11 -0
- package/template/public/brand/heros/editorial-desktop.svg +11 -0
- package/template/public/brand/heros/editorial-mobile.svg +11 -0
- package/template/public/brand/heros/essencial-desktop.svg +12 -0
- package/template/public/brand/heros/essencial-mobile.svg +12 -0
- package/template/public/brand/heros/promocional-desktop.svg +12 -0
- package/template/public/brand/heros/promocional-mobile.svg +12 -0
- package/template/public/brand/logo-chrome.svg +6 -0
- package/template/public/brand/logo-white.svg +6 -0
- package/template/public/brand/logo.svg +7 -0
- package/template/public/brand/pay-amex.webp +0 -0
- package/template/public/brand/pay-elo.webp +0 -0
- package/template/public/brand/pay-mastercard.webp +0 -0
- package/template/public/brand/pay-pix.webp +0 -0
- package/template/public/brand/pay-visa.webp +0 -0
- package/template/public/brand/ph/avatar-a.svg +5 -0
- package/template/public/brand/ph/avatar-b.svg +5 -0
- package/template/public/brand/ph/avatar-c.svg +5 -0
- package/template/public/brand/ph/avatar-d.svg +5 -0
- package/template/public/brand/ph/photo-a.svg +7 -0
- package/template/public/brand/ph/photo-b.svg +6 -0
- package/template/public/brand/ph/photo-c.svg +6 -0
- package/template/public/brand/ph/poster-a.svg +6 -0
- package/template/public/brand/ph/poster-b.svg +6 -0
- package/template/public/brand/ph/poster-c.svg +6 -0
- package/template/public/unbox/powered-by-fundo-preto.png +0 -0
- package/template/public/unbox/powered-by-transparente.png +0 -0
- package/template/public/unbox/powered-by.png +0 -0
- package/template/scripts/abandoned-cart.ts +82 -0
- package/template/scripts/check-editable.mjs +467 -0
- package/template/scripts/check-honestidade.mjs +111 -0
- package/template/scripts/check-placeholder.mjs +200 -0
- package/template/scripts/check-recipe.mjs +76 -0
- package/template/scripts/check-unbox-brand.mjs +298 -0
- package/template/scripts/contraste.js +177 -0
- package/template/scripts/dump-catalog.ts +95 -0
- package/template/scripts/load-env.ts +13 -0
- package/template/scripts/medir-sistema.mjs +56 -0
- package/template/scripts/place-order-pix.ts +61 -0
- package/template/scripts/qa-screenshots.mjs +150 -0
- package/template/scripts/sistema.py +231 -0
- package/template/scripts/subscribe-webhook.ts +36 -0
- package/template/scripts/test-live.ts +186 -0
- package/template/scripts/vocabulario.py +264 -0
- package/template/tsconfig.json +27 -0
- package/tools/LEIA-ME.md +39 -0
- package/tools/check-template-neutro.mjs +111 -0
- package/tools/notion-doc.mjs +191 -0
- package/tools/tokenize-neutrals.mjs +0 -0
- package/tools/tokenize-radius.mjs +52 -0
- package/tools/workflow-storefront.legado.js +1207 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Normaliza o `cart` cru da Unbox (createCart.cart / anonymousCartByCartId) numa forma única
|
|
2
|
+
// e estável para a UI. Mantém o preço/total SEMPRE como displayAmount do servidor (autoritativo).
|
|
3
|
+
|
|
4
|
+
export interface UiCartItem {
|
|
5
|
+
id: string; // cartItemId (node._id) — usado em update/remove
|
|
6
|
+
productId: string;
|
|
7
|
+
variantId: string;
|
|
8
|
+
title: string;
|
|
9
|
+
variantTitle?: string;
|
|
10
|
+
quantity: number;
|
|
11
|
+
unitPrice: number;
|
|
12
|
+
displayUnitPrice: string;
|
|
13
|
+
thumbnail?: string;
|
|
14
|
+
isRecurring?: boolean;
|
|
15
|
+
isBonus: boolean; // isDiscountedBonusItem (brinde) — R$0, excluído do placeOrder
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UiShippingAddress {
|
|
19
|
+
fullName: string;
|
|
20
|
+
taxPayerId: string;
|
|
21
|
+
phone: string;
|
|
22
|
+
address1: string;
|
|
23
|
+
address2?: string;
|
|
24
|
+
number: string;
|
|
25
|
+
neighborhood: string;
|
|
26
|
+
city: string;
|
|
27
|
+
region: string;
|
|
28
|
+
postal: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Placeholders gravados pela cotação de frete antes do endereço estar completo (ver
|
|
32
|
+
// quoteAddress() em checkout-client.tsx) — nunca são endereço real, filtrar ao ler de volta.
|
|
33
|
+
const QUOTE_PLACEHOLDERS = new Set(["Cotação", "—", "0", "00000000000"]);
|
|
34
|
+
|
|
35
|
+
function cleanAddressField(v: unknown): string {
|
|
36
|
+
const s = typeof v === "string" ? v.trim() : "";
|
|
37
|
+
return QUOTE_PLACEHOLDERS.has(s) ? "" : s;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Endereço de entrega já gravado no carrinho (se houver), com placeholders de cotação removidos. */
|
|
41
|
+
export function extractShippingAddress(cart: any): UiShippingAddress | null {
|
|
42
|
+
const fg = cart?.checkout?.fulfillmentGroups ?? [];
|
|
43
|
+
const raw = fg.find((g: any) => g.data?.shippingAddress)?.data?.shippingAddress;
|
|
44
|
+
if (!raw) return null;
|
|
45
|
+
const addr: UiShippingAddress = {
|
|
46
|
+
fullName: cleanAddressField(raw.fullName),
|
|
47
|
+
taxPayerId: cleanAddressField(raw.taxPayerId),
|
|
48
|
+
phone: cleanAddressField(raw.phone),
|
|
49
|
+
address1: cleanAddressField(raw.address1),
|
|
50
|
+
address2: cleanAddressField(raw.address2),
|
|
51
|
+
number: cleanAddressField(raw.number),
|
|
52
|
+
neighborhood: cleanAddressField(raw.neighborhood),
|
|
53
|
+
city: cleanAddressField(raw.city),
|
|
54
|
+
region: cleanAddressField(raw.region),
|
|
55
|
+
postal: cleanAddressField(raw.postal),
|
|
56
|
+
};
|
|
57
|
+
// Se sobrou só CEP/cidade/UF (da cotação) e nada do resto, não é um endereço "salvo" de verdade.
|
|
58
|
+
const hasSubstance = addr.address1 || addr.fullName || addr.number;
|
|
59
|
+
return hasSubstance ? addr : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface UiCart {
|
|
63
|
+
cartId: string;
|
|
64
|
+
totalItemQuantity: number;
|
|
65
|
+
expiresAt?: string;
|
|
66
|
+
email?: string;
|
|
67
|
+
shippingAddress?: UiShippingAddress | null;
|
|
68
|
+
recurringItemsFrequencyId?: string | null;
|
|
69
|
+
hasRecurring: boolean;
|
|
70
|
+
/** Frequência de assinatura escolhida (resolvida do cookie + política da loja). */
|
|
71
|
+
recurringFrequencyId?: string;
|
|
72
|
+
recurringFrequencyTitle?: string;
|
|
73
|
+
items: UiCartItem[];
|
|
74
|
+
discounts: Array<{ id: string; code?: string; displayAmount?: string }>;
|
|
75
|
+
fulfillmentGroupIds: string[];
|
|
76
|
+
selectedShipping?: { methodId: string; displayName?: string; displayAmount?: string } | null;
|
|
77
|
+
summary: {
|
|
78
|
+
itemTotal?: string;
|
|
79
|
+
discountTotal?: string;
|
|
80
|
+
shippingTotal?: string;
|
|
81
|
+
taxTotal?: string;
|
|
82
|
+
total?: string;
|
|
83
|
+
totalAmount?: number;
|
|
84
|
+
};
|
|
85
|
+
cartEvents: Array<{ type: string; data?: any }>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function normalizeCart(cart: any, extraEvents: any[] = []): UiCart | null {
|
|
89
|
+
if (!cart) return null;
|
|
90
|
+
const edges = cart.items?.edges ?? [];
|
|
91
|
+
const items: UiCartItem[] = edges.map((e: any) => {
|
|
92
|
+
const n = e.node;
|
|
93
|
+
return {
|
|
94
|
+
id: n._id,
|
|
95
|
+
productId: n.productConfiguration?.productId,
|
|
96
|
+
variantId: n.productConfiguration?.productVariantId,
|
|
97
|
+
title: n.title,
|
|
98
|
+
variantTitle: n.variantTitle,
|
|
99
|
+
quantity: n.quantity,
|
|
100
|
+
unitPrice: n.price?.amount ?? 0,
|
|
101
|
+
displayUnitPrice: n.price?.displayAmount ?? "—",
|
|
102
|
+
thumbnail: n.thumbnail || undefined,
|
|
103
|
+
isRecurring: n.isRecurring ?? false,
|
|
104
|
+
isBonus: n.isDiscountedBonusItem === true,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const fg = cart.checkout?.fulfillmentGroups ?? [];
|
|
109
|
+
const selected = fg.find((g: any) => g.selectedFulfillmentOption)?.selectedFulfillmentOption;
|
|
110
|
+
const summary = cart.checkout?.summary ?? {};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
cartId: cart._id,
|
|
114
|
+
totalItemQuantity: cart.totalItemQuantity ?? items.reduce((s, i) => s + i.quantity, 0),
|
|
115
|
+
expiresAt: cart.expiresAt,
|
|
116
|
+
email: cart.email || undefined,
|
|
117
|
+
shippingAddress: extractShippingAddress(cart),
|
|
118
|
+
recurringItemsFrequencyId: cart.recurringItemsFrequencyId ?? null,
|
|
119
|
+
hasRecurring: items.some((i) => i.isRecurring),
|
|
120
|
+
items,
|
|
121
|
+
discounts: (cart.appliedDiscounts ?? cart.discounts ?? []).map((d: any) => ({
|
|
122
|
+
id: d._id, code: d.code, displayAmount: d.amount?.displayAmount,
|
|
123
|
+
})),
|
|
124
|
+
fulfillmentGroupIds: fg.map((g: any) => g._id),
|
|
125
|
+
selectedShipping: selected
|
|
126
|
+
? {
|
|
127
|
+
methodId: selected.fulfillmentMethod?._id,
|
|
128
|
+
displayName: selected.fulfillmentMethod?.displayName ?? selected.fulfillmentMethod?.name,
|
|
129
|
+
displayAmount: selected.price?.displayAmount,
|
|
130
|
+
}
|
|
131
|
+
: null,
|
|
132
|
+
summary: {
|
|
133
|
+
itemTotal: summary.itemTotal?.displayAmount,
|
|
134
|
+
discountTotal: summary.discountTotal?.displayAmount,
|
|
135
|
+
shippingTotal: summary.fulfillmentTotal?.displayAmount,
|
|
136
|
+
taxTotal: summary.taxTotal?.displayAmount,
|
|
137
|
+
total: summary.total?.displayAmount,
|
|
138
|
+
totalAmount: summary.total?.amount,
|
|
139
|
+
},
|
|
140
|
+
cartEvents: [...(cart.cartEvents ?? []), ...extraEvents],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Link de recuperação de carrinho — formato ÚNICO usado em todo o projeto.
|
|
2
|
+
// Abrir o link em qualquer browser restaura o carrinho: o CartProvider lê ?id=&token=
|
|
3
|
+
// e chama /api/cart/link, que regrava o cookie e reidrata (mesma convenção do link
|
|
4
|
+
// nativo da Unbox, ex.: finalizar-pedido?...&step=1).
|
|
5
|
+
//
|
|
6
|
+
// Sem "server-only" de propósito: o cron de abandono (scripts/abandoned-cart.ts) roda
|
|
7
|
+
// fora do Next e precisa montar o MESMO link.
|
|
8
|
+
|
|
9
|
+
export function buildRecoveryUrl(
|
|
10
|
+
siteUrl: string,
|
|
11
|
+
cartId: string,
|
|
12
|
+
cartToken: string,
|
|
13
|
+
/** Carrinho de assinatura: a frequência vive só em cookie — sem ela no link, o cliente
|
|
14
|
+
* reabre o checkout sem frequência e não consegue finalizar. */
|
|
15
|
+
recurringItemsFrequencyId?: string | null,
|
|
16
|
+
): string {
|
|
17
|
+
// Checkout hospedado (UNBOX_HOSTED_CHECKOUT_URL): o /checkout deste projeto nem existe —
|
|
18
|
+
// o link tem que cair no checkout da Unbox, com o mesmo ?id=&token=.
|
|
19
|
+
const base = process.env.UNBOX_HOSTED_CHECKOUT_URL || `${siteUrl.replace(/\/+$/, "")}/checkout`;
|
|
20
|
+
let url = `${base}?id=${encodeURIComponent(cartId)}&token=${encodeURIComponent(cartToken)}`;
|
|
21
|
+
if (recurringItemsFrequencyId) url += `&freq=${encodeURIComponent(recurringItemsFrequencyId)}`;
|
|
22
|
+
return url + "&step=1";
|
|
23
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Monta a resposta de carrinho para o BFF: normaliza + anexa a FREQUÊNCIA de assinatura
|
|
2
|
+
// (a frequência não vive no carrinho da Unbox — está no cookie; o título vem da política da loja).
|
|
3
|
+
import "server-only";
|
|
4
|
+
import { normalizeCart, type UiCart } from "./cart-normalize";
|
|
5
|
+
import { getRecurFreq } from "./session";
|
|
6
|
+
import { getShopData, getCatalog } from "./queries";
|
|
7
|
+
|
|
8
|
+
// Brindes/bônus liberados automaticamente pela Unbox (ex.: item grátis ao atingir um valor no
|
|
9
|
+
// carrinho) não vêm com `thumbnail` na linha do carrinho, mesmo o produto tendo foto real
|
|
10
|
+
// cadastrada — completa buscando no catálogo por productId (já cacheado por getCatalog).
|
|
11
|
+
async function fillMissingThumbnails(cart: UiCart): Promise<void> {
|
|
12
|
+
const missing = cart.items.filter((i) => !i.thumbnail);
|
|
13
|
+
if (!missing.length) return;
|
|
14
|
+
try {
|
|
15
|
+
const catalog = await getCatalog({ first: 100 });
|
|
16
|
+
const imageByProductId = new Map<string, string>();
|
|
17
|
+
for (const n of catalog.nodes ?? []) {
|
|
18
|
+
const p = n.product ?? n;
|
|
19
|
+
if (p?.productId && p?.imageUrls?.[0]) imageByProductId.set(p.productId, p.imageUrls[0]);
|
|
20
|
+
}
|
|
21
|
+
for (const item of missing) {
|
|
22
|
+
const url = imageByProductId.get(item.productId);
|
|
23
|
+
if (url) item.thumbnail = url;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
/* foto é cosmética — nunca quebra a resposta do carrinho */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function cartResponse(rawCart: any, extraEvents: any[] = []): Promise<UiCart | null> {
|
|
31
|
+
const cart = normalizeCart(rawCart, extraEvents);
|
|
32
|
+
if (!cart) return null;
|
|
33
|
+
await fillMissingThumbnails(cart);
|
|
34
|
+
if (cart.hasRecurring) {
|
|
35
|
+
const freqId = await getRecurFreq();
|
|
36
|
+
if (freqId) {
|
|
37
|
+
cart.recurringFrequencyId = freqId;
|
|
38
|
+
try {
|
|
39
|
+
const shop = await getShopData();
|
|
40
|
+
const f = (shop?.recurringOrdersPolicy?.allowedFrequencies ?? []).find((x: any) => x._id === freqId);
|
|
41
|
+
if (f?.title) cart.recurringFrequencyTitle = f.title;
|
|
42
|
+
} catch {
|
|
43
|
+
/* sem título não quebra a UI */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return cart;
|
|
48
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Mapeia o catálogo cru da Unbox para os itens do catálogo da loja (client-side filtering).
|
|
2
|
+
import { resolveProductPrice, parseBRL } from "@/lib/format";
|
|
3
|
+
import type { CatalogProductItem, CatalogCategory } from "@/components/catalog/catalog-client";
|
|
4
|
+
|
|
5
|
+
export function buildTagMap(tags: any[]): Map<string, string> {
|
|
6
|
+
const m = new Map<string, string>();
|
|
7
|
+
for (const t of tags ?? []) if (t?.isVisible !== false) m.set(t._id, t.displayTitle || t.name);
|
|
8
|
+
return m;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildCategories(tags: any[]): CatalogCategory[] {
|
|
12
|
+
return (tags ?? [])
|
|
13
|
+
.filter((t: any) => t?.isVisible !== false)
|
|
14
|
+
.sort((a: any, b: any) => (a.position ?? 0) - (b.position ?? 0))
|
|
15
|
+
.map((t: any): CatalogCategory => ({ name: t.displayTitle || t.name, slug: t.slug }));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function mapCatalogItems(nodes: any[], tagMap: Map<string, string>): CatalogProductItem[] {
|
|
19
|
+
return (nodes ?? [])
|
|
20
|
+
.map((n: any) => n.product ?? n)
|
|
21
|
+
.filter((p: any) => p?.slug)
|
|
22
|
+
.map((p: any): CatalogProductItem => {
|
|
23
|
+
const v0 = p.variants?.[0];
|
|
24
|
+
const rp = resolveProductPrice(p);
|
|
25
|
+
const price = v0?.pricing?.[0]?.price ?? rp.price ?? 0;
|
|
26
|
+
const displayOld = v0?.pricing?.[0]?.compareAtPrice?.displayAmount ?? rp.compareAt ?? null;
|
|
27
|
+
const oldPrice = parseBRL(displayOld);
|
|
28
|
+
const hasOld = oldPrice != null && oldPrice > price;
|
|
29
|
+
const offPct = hasOld && oldPrice ? Math.round((1 - price / oldPrice) * 100) : null;
|
|
30
|
+
const categories = ((p.tagIds ?? []) as string[]).map((id) => tagMap.get(id)).filter(Boolean) as string[];
|
|
31
|
+
return {
|
|
32
|
+
slug: p.slug,
|
|
33
|
+
title: p.title,
|
|
34
|
+
weight: v0?.title ?? "",
|
|
35
|
+
categories,
|
|
36
|
+
price,
|
|
37
|
+
displayPrice: rp.displayPrice,
|
|
38
|
+
oldPrice: hasOld ? oldPrice : null,
|
|
39
|
+
displayOld: hasOld ? displayOld : null,
|
|
40
|
+
offPct,
|
|
41
|
+
imageUrl: p.imageUrls?.[0] ?? null,
|
|
42
|
+
productId: p.productId,
|
|
43
|
+
variantId: v0?._id ?? null,
|
|
44
|
+
badge: p.isSoldOut
|
|
45
|
+
? { label: "ESGOTADO", bg: "var(--store-surface-2)", fg: "var(--store-muted)" }
|
|
46
|
+
: hasOld
|
|
47
|
+
? { label: "OFERTA", bg: "var(--store-sale-soft)", fg: "var(--store-sale)" }
|
|
48
|
+
: p.isLowQuantity
|
|
49
|
+
? { label: "ÚLTIMAS", bg: "var(--store-cta-soft)", fg: "var(--store-cta-dark)" }
|
|
50
|
+
: null,
|
|
51
|
+
soldOut: !!p.isSoldOut,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Lock de checkout por cartId (doc 11): placeOrder NÃO é idempotente e cobra na hora.
|
|
2
|
+
// Evita que dois cliques / duas requisições concorrentes do mesmo carrinho gerem DOIS pedidos.
|
|
3
|
+
//
|
|
4
|
+
// PROD: trocar por lock distribuído (Vercel KV SET NX com TTL / Redis). Em dev é em memória.
|
|
5
|
+
import "server-only";
|
|
6
|
+
|
|
7
|
+
const locks = new Map<string, number>(); // cartId -> expiresAt
|
|
8
|
+
const TTL_MS = 60 * 1000;
|
|
9
|
+
|
|
10
|
+
/** Tenta adquirir o lock. Retorna false se já há um placeOrder em voo para este cartId. */
|
|
11
|
+
export function acquireCheckoutLock(cartId: string): boolean {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const exp = locks.get(cartId);
|
|
14
|
+
if (exp && exp > now) return false;
|
|
15
|
+
locks.set(cartId, now + TTL_MS);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function releaseCheckoutLock(cartId: string): void {
|
|
20
|
+
locks.delete(cartId);
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Navega pro checkout — decidido pelo SERVIDOR (nunca no client), porque a rota real depende de
|
|
4
|
+
// UNBOX_HOSTED_CHECKOUT_URL (checkout padrão hospedado pela Unbox, doc 06) e essa variável não é
|
|
5
|
+
// NEXT_PUBLIC_. Se configurada, o destino é externo (mesmo domínio da loja, path
|
|
6
|
+
// /carrinho/finalizar-pedido, com ?id=&token=&freq=&step= — o servidor monta a URL completa,
|
|
7
|
+
// nunca o client, porque cartToken é httpOnly e não pode ser lido em JS). Sem a variável, cai no
|
|
8
|
+
// /checkout interno de sempre.
|
|
9
|
+
export async function goToCheckout(router: { push: (href: string) => void }): Promise<void> {
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch("/api/checkout-destination");
|
|
12
|
+
const data = await res.json().catch(() => ({}));
|
|
13
|
+
const url: string = data?.url || "/checkout";
|
|
14
|
+
if (/^https?:\/\//i.test(url)) {
|
|
15
|
+
window.location.href = url; // destino externo — precisa de navegação completa, não router.push
|
|
16
|
+
} else {
|
|
17
|
+
router.push(url);
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
router.push("/checkout");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Configuração central — lê variáveis de ambiente (server-only) e expõe os identificadores
|
|
2
|
+
// públicos da loja. NUNCA exporte segredos para o cliente daqui.
|
|
3
|
+
import "server-only";
|
|
4
|
+
import { checkEnv } from "./env-check";
|
|
5
|
+
|
|
6
|
+
// UNBOX_API_KEY/USER/PASS são lidos sem lançar erro aqui — esse módulo é importado
|
|
7
|
+
// pelo layout raiz (toda página), então validar no import quebraria o app inteiro
|
|
8
|
+
// sem credenciais. A validação acontece em lib/unbox/store.ts, só quando uma página
|
|
9
|
+
// de fato tenta buscar dados da API (permite "modo mockup" sem credenciais).
|
|
10
|
+
export const serverEnv = {
|
|
11
|
+
apiKey: process.env.UNBOX_API_KEY ?? "",
|
|
12
|
+
user: process.env.UNBOX_USER ?? "",
|
|
13
|
+
pass: process.env.UNBOX_PASS ?? "",
|
|
14
|
+
shopId: process.env.UNBOX_SHOP_ID ?? "",
|
|
15
|
+
shopSlug: process.env.UNBOX_SHOP_SLUG ?? "",
|
|
16
|
+
authUrl: process.env.UNBOX_AUTH_URL ?? "https://api.unbox.com.br",
|
|
17
|
+
gqlUrl: process.env.UNBOX_GRAPHQL_URL ?? "https://core.unbox.com.br/graphql",
|
|
18
|
+
// API pública de PARCEIROS (nova): uma api key única por parceiro, independente do nº de
|
|
19
|
+
// lojas. Setar UNBOX_PARTNER_API_KEY liga o roteamento: signIn + leituras com paridade
|
|
20
|
+
// passam por partners.unbox.com.br; o restante (carrinho/checkout/cliente) segue no core
|
|
21
|
+
// até a Unbox liberar as escritas na API de parceiros. Ver lib/unbox/client.ts.
|
|
22
|
+
partnerApiKey: process.env.UNBOX_PARTNER_API_KEY ?? "",
|
|
23
|
+
partnerGqlUrl: process.env.UNBOX_PARTNER_GRAPHQL_URL ?? "https://partners.unbox.com.br/graphql",
|
|
24
|
+
captchaBypass: process.env.UNBOX_CAPTCHA_BYPASS ?? "",
|
|
25
|
+
webhookSecret: process.env.UNBOX_WEBHOOK_SECRET ?? "",
|
|
26
|
+
sessionSecret: process.env.SESSION_SECRET ?? "dev-insecure-session-secret-change-me",
|
|
27
|
+
crmWebhookUrl: process.env.CRM_WEBHOOK_URL ?? "",
|
|
28
|
+
revalidateSecret: process.env.REVALIDATE_SECRET ?? "",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// No modelo antigo a loja tem api key própria; no novo, a key é do PARCEIRO. Qualquer uma
|
|
32
|
+
// das duas + user/pass da loja habilita o modo real (sem nenhuma → modo mockup).
|
|
33
|
+
export const hasUnboxCredentials = Boolean(
|
|
34
|
+
(serverEnv.apiKey || serverEnv.partnerApiKey) && serverEnv.user && serverEnv.pass,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/** Decodifica os claims do JWT (sem validar — só para extrair shopId/shopSlug). */
|
|
38
|
+
export function decodeJwtClaims(jwt: string): Record<string, any> {
|
|
39
|
+
try {
|
|
40
|
+
const payload = jwt.split(".")[1];
|
|
41
|
+
const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8");
|
|
42
|
+
return JSON.parse(json);
|
|
43
|
+
} catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const SHOP_ID_CLAIM = "arn:unbox:shopId";
|
|
49
|
+
export const SHOP_SLUG_CLAIM = "arn:unbox:shopSlug";
|
|
50
|
+
|
|
51
|
+
checkEnv();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Integração de CRM (carrinho abandonado / e-mails transacionais) — doc 10/11.
|
|
2
|
+
// A recuperação de carrinho é responsabilidade do storefront: ele detecta o abandono e
|
|
3
|
+
// dispara pelo CRM da loja (Revi/Klaviyo). Aqui fazemos um POST real para CRM_WEBHOOK_URL
|
|
4
|
+
// quando configurada.
|
|
5
|
+
import "server-only";
|
|
6
|
+
import { serverEnv } from "./config";
|
|
7
|
+
|
|
8
|
+
export interface CrmEvent {
|
|
9
|
+
/** `checkout_started` sai na hora em que o e-mail é gravado no carrinho e SEMPRE carrega
|
|
10
|
+
* `recoveryUrl` — é ele que arma o fluxo de abandono no CRM (espere N min; se não chegar
|
|
11
|
+
* `order_created` com o mesmo cartId/email, envie o e-mail com o recoveryUrl). */
|
|
12
|
+
type: "checkout_started" | "abandoned_cart" | "order_created" | "order_paid";
|
|
13
|
+
email?: string;
|
|
14
|
+
cartId?: string;
|
|
15
|
+
orderId?: string;
|
|
16
|
+
referenceId?: string;
|
|
17
|
+
/** Link pronto pra restaurar o carrinho (/checkout?id=&token=&freq=&step=) — top-level pra
|
|
18
|
+
* quem consumir o webhook (Klaviyo/Revi) achar fácil, sem precisar abrir o payload. */
|
|
19
|
+
recoveryUrl?: string;
|
|
20
|
+
payload?: Record<string, any>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Dispara um evento para o CRM. No-op (apenas log) se CRM_WEBHOOK_URL não estiver configurada. */
|
|
24
|
+
export async function dispatchCrm(event: CrmEvent): Promise<{ ok: boolean; skipped?: boolean }> {
|
|
25
|
+
if (!serverEnv.crmWebhookUrl) {
|
|
26
|
+
console.info("[crm] CRM_WEBHOOK_URL não configurada — evento ignorado:", event.type);
|
|
27
|
+
return { ok: true, skipped: true };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(serverEnv.crmWebhookUrl, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
body: JSON.stringify({ source: "unbox-storefront", ...event, at: new Date().toISOString() }),
|
|
34
|
+
});
|
|
35
|
+
return { ok: res.ok };
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error("[crm] falha ao despachar evento", e);
|
|
38
|
+
return { ok: false };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Factory do UnboxCustomerClient a partir do cookie httpOnly do cliente (área "minha conta").
|
|
2
|
+
// Lança 401 implícito devolvendo null quando não há token.
|
|
3
|
+
import "server-only";
|
|
4
|
+
import { UnboxCustomerClient } from "./unbox/customer";
|
|
5
|
+
import { getCustomerToken } from "./session";
|
|
6
|
+
import { getShopContext } from "./unbox/store";
|
|
7
|
+
|
|
8
|
+
/** Retorna um UnboxCustomerClient autenticado, ou null se o cliente não estiver logado. */
|
|
9
|
+
export async function getCustomerClient(): Promise<UnboxCustomerClient | null> {
|
|
10
|
+
const token = await getCustomerToken();
|
|
11
|
+
if (!token) return null;
|
|
12
|
+
const { shopId } = await getShopContext();
|
|
13
|
+
return new UnboxCustomerClient({ token, shopId });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Igual ao acima, mas lança um Error 401-like se não logado (para Route Handlers). */
|
|
17
|
+
export async function requireCustomerClient(): Promise<UnboxCustomerClient> {
|
|
18
|
+
const c = await getCustomerClient();
|
|
19
|
+
if (!c) throw new Error("UNAUTHENTICATED");
|
|
20
|
+
return c;
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Paginação completa do catálogo por offset (doc 11): NÃO confiar num `first` gigante (há teto
|
|
2
|
+
// de servidor que trunca em silêncio). Itera em lotes até pageInfo.hasNextPage === false.
|
|
3
|
+
import "server-only";
|
|
4
|
+
import { withStoreClient } from "./unbox/store";
|
|
5
|
+
|
|
6
|
+
const PAGE = 100;
|
|
7
|
+
|
|
8
|
+
/** Carrega TODOS os itens visíveis do catálogo (para sitemap e listagens completas). */
|
|
9
|
+
export async function loadAllCatalogItems(opts: { tagIds?: string[]; searchText?: string } = {}): Promise<any[]> {
|
|
10
|
+
const all: any[] = [];
|
|
11
|
+
let offset = 0;
|
|
12
|
+
// teto de segurança para não loopar infinito caso a API mude o contrato
|
|
13
|
+
for (let guard = 0; guard < 100; guard++) {
|
|
14
|
+
const page = await withStoreClient((c) =>
|
|
15
|
+
c.getCatalog({ first: PAGE, offset, tagIds: opts.tagIds, searchText: opts.searchText }),
|
|
16
|
+
);
|
|
17
|
+
const nodes = page.nodes ?? [];
|
|
18
|
+
all.push(...nodes);
|
|
19
|
+
if (!page.pageInfo?.hasNextPage || nodes.length === 0) break;
|
|
20
|
+
offset += PAGE;
|
|
21
|
+
}
|
|
22
|
+
return all;
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Identidade desta loja PARA O EDITOR. O CLI carimba o literal no scaffold; EDITOR_URL vem do
|
|
2
|
+
// ambiente (sem ele, o editor fica desligado).
|
|
3
|
+
//
|
|
4
|
+
// `UNBOX_EDITOR_SHOP` é variável PRÓPRIA, de propósito: não reuse `UNBOX_SHOP_SLUG`, que é o slug
|
|
5
|
+
// da loja NA PLATAFORMA (sai do JWT). Os dois costumam coincidir e no dia em que divergirem o
|
|
6
|
+
// editor deixa de achar a loja sem dizer por quê. Este nome tem de bater com o `slug` da entrada
|
|
7
|
+
// dela no shops.json do editor.
|
|
8
|
+
export const STORE_SLUG = process.env.UNBOX_EDITOR_SHOP || "__STORE_SLUG__";
|
|
9
|
+
export const EDITOR_URL = (process.env.EDITOR_URL || "").replace(/\/+$/, "");
|
|
10
|
+
/** Origem do editor vista pelo navegador (frame-ancestors + postMessage). */
|
|
11
|
+
export const EDITOR_ORIGIN = process.env.NEXT_PUBLIC_EDITOR_ORIGIN || "";
|