@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.
Files changed (358) hide show
  1. package/LICENSE.md +46 -0
  2. package/README.md +175 -0
  3. package/bin/cli.js +439 -0
  4. package/package.json +43 -0
  5. package/src/colors.js +100 -0
  6. package/src/presets.js +241 -0
  7. package/src/theme.js +253 -0
  8. package/template/.claude/agents/assets-marca.md +40 -0
  9. package/template/.claude/agents/avaliador-visual.md +48 -0
  10. package/template/.claude/agents/branding-briefing.md +401 -0
  11. package/template/.claude/agents/conteudo-secao.md +46 -0
  12. package/template/.claude/agents/paginas-legais.md +44 -0
  13. package/template/.claude/settings.json +3 -0
  14. package/template/.claude/skills/frontend-design/ATTRIBUTION.md +3 -0
  15. package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
  16. package/template/.claude/skills/frontend-design/SKILL.md +55 -0
  17. package/template/.claude/skills/web-design-guidelines/ATTRIBUTION.md +3 -0
  18. package/template/.claude/skills/web-design-guidelines/SKILL.md +50 -0
  19. package/template/.claude/skills/web-design-guidelines/guidelines-snapshot.md +180 -0
  20. package/template/.env.example +158 -0
  21. package/template/.mcp.json.example +14 -0
  22. package/template/.nvmrc +1 -0
  23. package/template/CLAUDE.md +333 -0
  24. package/template/COVERAGE.md +80 -0
  25. package/template/DEPLOY.md +179 -0
  26. package/template/QA.md +108 -0
  27. package/template/README.md +152 -0
  28. package/template/agents/CONSTRUCAO.md +207 -0
  29. package/template/agents/MANAGER.md +192 -0
  30. package/template/agents/PADROES.md +255 -0
  31. package/template/agents/definitions/00-scaffold.md +56 -0
  32. package/template/agents/definitions/01-layout.md +47 -0
  33. package/template/agents/definitions/02-homepage.md +70 -0
  34. package/template/agents/definitions/03-catalog.md +86 -0
  35. package/template/agents/definitions/04-pdp.md +97 -0
  36. package/template/agents/definitions/05-cart.md +116 -0
  37. package/template/agents/definitions/06-checkout.md +146 -0
  38. package/template/agents/definitions/07-auth.md +130 -0
  39. package/template/agents/definitions/08-customer.md +55 -0
  40. package/template/agents/definitions/09-promotions.md +62 -0
  41. package/template/agents/definitions/10-feedback.md +128 -0
  42. package/template/agents/definitions/11-seo-infra.md +159 -0
  43. package/template/agents/definitions/12-deploy.md +173 -0
  44. package/template/agents/definitions/13-cro.md +275 -0
  45. package/template/agents/definitions/14-seo.md +388 -0
  46. package/template/agents/definitions/15-branding.md +26 -0
  47. package/template/agents/definitions/16-qa-visual.md +138 -0
  48. package/template/agents/definitions/17-aeo.md +257 -0
  49. package/template/app/(loja)/busca/page.tsx +72 -0
  50. package/template/app/(loja)/carrinho/oferta/page.tsx +57 -0
  51. package/template/app/(loja)/carrinho/page.tsx +160 -0
  52. package/template/app/(loja)/categoria/[tagSlug]/layout.tsx +31 -0
  53. package/template/app/(loja)/categoria/[tagSlug]/loading.tsx +17 -0
  54. package/template/app/(loja)/categoria/[tagSlug]/page.tsx +49 -0
  55. package/template/app/(loja)/checkout/page.tsx +80 -0
  56. package/template/app/(loja)/checkout/pix/[ref]/page.tsx +133 -0
  57. package/template/app/(loja)/conta/assinaturas/[referenceId]/page.tsx +112 -0
  58. package/template/app/(loja)/conta/assinaturas/page.tsx +51 -0
  59. package/template/app/(loja)/conta/enderecos/page.tsx +20 -0
  60. package/template/app/(loja)/conta/entrar/page.tsx +119 -0
  61. package/template/app/(loja)/conta/page.tsx +45 -0
  62. package/template/app/(loja)/conta/pedidos/[referenceId]/page.tsx +49 -0
  63. package/template/app/(loja)/conta/pedidos/page.tsx +104 -0
  64. package/template/app/(loja)/conta/preferencias/page.tsx +24 -0
  65. package/template/app/(loja)/devolucoes/page.tsx +36 -0
  66. package/template/app/(loja)/layout.tsx +22 -0
  67. package/template/app/(loja)/oferta/page.tsx +57 -0
  68. package/template/app/(loja)/page.tsx +86 -0
  69. package/template/app/(loja)/pedido/[referenceId]/page.tsx +78 -0
  70. package/template/app/(loja)/privacidade/page.tsx +113 -0
  71. package/template/app/(loja)/produto/[productSlug]/layout.tsx +23 -0
  72. package/template/app/(loja)/produto/[productSlug]/loading.tsx +20 -0
  73. package/template/app/(loja)/produto/[productSlug]/page.tsx +372 -0
  74. package/template/app/(loja)/produtos/loading.tsx +16 -0
  75. package/template/app/(loja)/produtos/page.tsx +33 -0
  76. package/template/app/(loja)/termos/page.tsx +104 -0
  77. package/template/app/acesso/page.tsx +129 -0
  78. package/template/app/api/account/addresses/route.ts +39 -0
  79. package/template/app/api/account/exists/route.ts +21 -0
  80. package/template/app/api/account/me/route.ts +25 -0
  81. package/template/app/api/account/otp/route.ts +26 -0
  82. package/template/app/api/account/preferences/route.ts +19 -0
  83. package/template/app/api/account/signin/route.ts +27 -0
  84. package/template/app/api/account/signout/route.ts +9 -0
  85. package/template/app/api/acesso/route.ts +171 -0
  86. package/template/app/api/capi/route.ts +34 -0
  87. package/template/app/api/cart/coupon/route.ts +49 -0
  88. package/template/app/api/cart/items/route.ts +45 -0
  89. package/template/app/api/cart/link/route.ts +69 -0
  90. package/template/app/api/cart/route.ts +113 -0
  91. package/template/app/api/cart/share/route.ts +21 -0
  92. package/template/app/api/cep/[code]/route.ts +18 -0
  93. package/template/app/api/checkout/address/route.ts +24 -0
  94. package/template/app/api/checkout/email/route.ts +41 -0
  95. package/template/app/api/checkout/installments/route.ts +16 -0
  96. package/template/app/api/checkout/route.ts +110 -0
  97. package/template/app/api/checkout/shipping/route.ts +65 -0
  98. package/template/app/api/checkout-destination/route.ts +31 -0
  99. package/template/app/api/order/[ref]/route.ts +17 -0
  100. package/template/app/api/payment-link/route.ts +28 -0
  101. package/template/app/api/revalidate/route.ts +52 -0
  102. package/template/app/api/shipping/quote/route.ts +29 -0
  103. package/template/app/api/subscriptions/[id]/route.ts +52 -0
  104. package/template/app/api/track/route.ts +28 -0
  105. package/template/app/api/unbox/catalogo/route.ts +136 -0
  106. package/template/app/api/unbox/paginas/route.ts +167 -0
  107. package/template/app/api/unbox/vitrine/route.ts +84 -0
  108. package/template/app/api/webhooks/unbox/route.ts +97 -0
  109. package/template/app/apple-icon.svg +5 -0
  110. package/template/app/error.tsx +15 -0
  111. package/template/app/globals.css +457 -0
  112. package/template/app/icon.svg +4 -0
  113. package/template/app/layout.tsx +90 -0
  114. package/template/app/llms.txt/route.ts +43 -0
  115. package/template/app/manifest.ts +21 -0
  116. package/template/app/not-found.tsx +18 -0
  117. package/template/app/opengraph-image.tsx +35 -0
  118. package/template/app/robots.ts +31 -0
  119. package/template/app/sitemap.ts +42 -0
  120. package/template/bootstrap.sh +87 -0
  121. package/template/components/account/account-shell.tsx +47 -0
  122. package/template/components/account/address-book.tsx +147 -0
  123. package/template/components/account/preferences-form.tsx +77 -0
  124. package/template/components/account/reorder-button.tsx +47 -0
  125. package/template/components/account/signout-button.tsx +20 -0
  126. package/template/components/account/subscription-actions.tsx +204 -0
  127. package/template/components/account-nav.tsx +41 -0
  128. package/template/components/address-fields.tsx +102 -0
  129. package/template/components/analytics/data-layer-ready.tsx +16 -0
  130. package/template/components/analytics/purchase-tracker.tsx +32 -0
  131. package/template/components/brand-search.tsx +34 -0
  132. package/template/components/cart/cart-provider.tsx +340 -0
  133. package/template/components/cart/mini-cart.tsx +228 -0
  134. package/template/components/cart-button.tsx +35 -0
  135. package/template/components/catalog/catalog-client.tsx +523 -0
  136. package/template/components/catalog/grid-skeleton.tsx +19 -0
  137. package/template/components/catalog/pager.tsx +66 -0
  138. package/template/components/catalog/product-grid-card.tsx +52 -0
  139. package/template/components/catalog/product-grid.tsx +27 -0
  140. package/template/components/checkout/checkout-client.tsx +1333 -0
  141. package/template/components/chrome/announce-bar.tsx +57 -0
  142. package/template/components/chrome/chrome-recipe.ts +13 -0
  143. package/template/components/chrome/footers/captura-botao.tsx +28 -0
  144. package/template/components/chrome/footers/colunas.tsx +110 -0
  145. package/template/components/chrome/footers/conversao.tsx +90 -0
  146. package/template/components/chrome/footers/editorial.tsx +98 -0
  147. package/template/components/chrome/footers/minimal.tsx +55 -0
  148. package/template/components/chrome/header-bar-mobile.tsx +64 -0
  149. package/template/components/chrome/headers/centralizado.tsx +71 -0
  150. package/template/components/chrome/headers/classico.tsx +64 -0
  151. package/template/components/chrome/headers/compacto.tsx +66 -0
  152. package/template/components/chrome/headers/equilibrado.tsx +73 -0
  153. package/template/components/chrome/headers/imersivo.tsx +62 -0
  154. package/template/components/chrome/registry.ts +70 -0
  155. package/template/components/chrome/solid-on-scroll.tsx +39 -0
  156. package/template/components/empty-state.tsx +22 -0
  157. package/template/components/home/combo-card-compact.tsx +67 -0
  158. package/template/components/home/combos-home.tsx +64 -0
  159. package/template/components/home/combos-section.tsx +177 -0
  160. package/template/components/home/home-recipe.ts +41 -0
  161. package/template/components/home/sections/attributes-marquee.tsx +178 -0
  162. package/template/components/home/sections/benefits.tsx +80 -0
  163. package/template/components/home/sections/bloco-html.tsx +47 -0
  164. package/template/components/home/sections/catalogo.ts +165 -0
  165. package/template/components/home/sections/category-pills.tsx +32 -0
  166. package/template/components/home/sections/combos-carousel.tsx +141 -0
  167. package/template/components/home/sections/comparison.tsx +73 -0
  168. package/template/components/home/sections/founder-story.tsx +40 -0
  169. package/template/components/home/sections/hero.tsx +187 -0
  170. package/template/components/home/sections/kits.tsx +11 -0
  171. package/template/components/home/sections/media-cards.tsx +70 -0
  172. package/template/components/home/sections/newsletter.tsx +62 -0
  173. package/template/components/home/sections/product-showcase.tsx +175 -0
  174. package/template/components/home/sections/purchase-hero.tsx +187 -0
  175. package/template/components/home/sections/quote-banner.tsx +50 -0
  176. package/template/components/home/sections/registry.ts +186 -0
  177. package/template/components/home/sections/reviews-carousel.tsx +66 -0
  178. package/template/components/home/sections/reviews.tsx +68 -0
  179. package/template/components/home/sections/ritual.tsx +96 -0
  180. package/template/components/home/sections/savings.tsx +47 -0
  181. package/template/components/home/sections/social-row.tsx +65 -0
  182. package/template/components/home/sections/spec-table.tsx +117 -0
  183. package/template/components/home/sections/stars.tsx +12 -0
  184. package/template/components/home/sections/stats-grid.tsx +54 -0
  185. package/template/components/home/sections/trust-bar.tsx +56 -0
  186. package/template/components/home/sections/trust-strip.tsx +40 -0
  187. package/template/components/home/sections/video-wall.tsx +59 -0
  188. package/template/components/landing/landing-recipe.ts +20 -0
  189. package/template/components/landing/oferta-sections.tsx +40 -0
  190. package/template/components/landing/product-picker.tsx +257 -0
  191. package/template/components/mobile-nav.tsx +71 -0
  192. package/template/components/order-status.tsx +163 -0
  193. package/template/components/powered-by-unbox.tsx +27 -0
  194. package/template/components/product/pdp/buy-box.tsx +604 -0
  195. package/template/components/product/pdp/catalog-grid.tsx +113 -0
  196. package/template/components/product/pdp/faq-modelo.ts +86 -0
  197. package/template/components/product/pdp/gallery.tsx +87 -0
  198. package/template/components/product/pdp/interactive.tsx +200 -0
  199. package/template/components/product/pdp/newsletter.tsx +64 -0
  200. package/template/components/product/pdp/payment-chips.tsx +22 -0
  201. package/template/components/product/pdp/pdp-view.tsx +227 -0
  202. package/template/components/product/pdp/recommendations.tsx +144 -0
  203. package/template/components/product/pdp/sections.tsx +173 -0
  204. package/template/components/quantity-stepper.tsx +36 -0
  205. package/template/components/search-box.tsx +37 -0
  206. package/template/components/site-footer.tsx +59 -0
  207. package/template/components/site-header.tsx +64 -0
  208. package/template/components/ui/accordion.tsx +72 -0
  209. package/template/components/ui/alert-dialog.tsx +187 -0
  210. package/template/components/ui/alert.tsx +76 -0
  211. package/template/components/ui/aspect-ratio.tsx +22 -0
  212. package/template/components/ui/avatar.tsx +109 -0
  213. package/template/components/ui/badge.tsx +52 -0
  214. package/template/components/ui/breadcrumb.tsx +125 -0
  215. package/template/components/ui/button.tsx +58 -0
  216. package/template/components/ui/card.tsx +103 -0
  217. package/template/components/ui/carousel.tsx +242 -0
  218. package/template/components/ui/command.tsx +196 -0
  219. package/template/components/ui/dialog.tsx +160 -0
  220. package/template/components/ui/dropdown-menu.tsx +268 -0
  221. package/template/components/ui/foto.tsx +54 -0
  222. package/template/components/ui/input-group.tsx +158 -0
  223. package/template/components/ui/input-otp.tsx +87 -0
  224. package/template/components/ui/input.tsx +20 -0
  225. package/template/components/ui/label.tsx +20 -0
  226. package/template/components/ui/navigation-menu.tsx +168 -0
  227. package/template/components/ui/pagination.tsx +132 -0
  228. package/template/components/ui/popover.tsx +90 -0
  229. package/template/components/ui/progress.tsx +83 -0
  230. package/template/components/ui/radio-group.tsx +38 -0
  231. package/template/components/ui/scroll-area.tsx +55 -0
  232. package/template/components/ui/select.tsx +201 -0
  233. package/template/components/ui/separator.tsx +25 -0
  234. package/template/components/ui/sheet.tsx +138 -0
  235. package/template/components/ui/skeleton.tsx +13 -0
  236. package/template/components/ui/sonner.tsx +44 -0
  237. package/template/components/ui/switch.tsx +32 -0
  238. package/template/components/ui/table.tsx +116 -0
  239. package/template/components/ui/tabs.tsx +82 -0
  240. package/template/components/ui/textarea.tsx +18 -0
  241. package/template/components/ui/toggle-group.tsx +89 -0
  242. package/template/components/ui/toggle.tsx +45 -0
  243. package/template/components/ui/tooltip.tsx +66 -0
  244. package/template/components.json +25 -0
  245. package/template/eslint.config.mjs +36 -0
  246. package/template/gitignore +21 -0
  247. package/template/lib/analytics.ts +369 -0
  248. package/template/lib/api.ts +60 -0
  249. package/template/lib/capi.ts +87 -0
  250. package/template/lib/cart-link.ts +51 -0
  251. package/template/lib/cart-normalize.ts +142 -0
  252. package/template/lib/cart-recovery.ts +23 -0
  253. package/template/lib/cart-response.ts +48 -0
  254. package/template/lib/catalog-map.ts +54 -0
  255. package/template/lib/catalog.ts +3 -0
  256. package/template/lib/checkout-lock.ts +21 -0
  257. package/template/lib/checkout-nav.ts +22 -0
  258. package/template/lib/config.ts +51 -0
  259. package/template/lib/crm.ts +40 -0
  260. package/template/lib/customer-session.ts +21 -0
  261. package/template/lib/dataloader.ts +23 -0
  262. package/template/lib/editable/config.ts +11 -0
  263. package/template/lib/editable/document.ts +1762 -0
  264. package/template/lib/editable/index.ts +3 -0
  265. package/template/lib/editable/primitives.tsx +821 -0
  266. package/template/lib/editable/provider.tsx +861 -0
  267. package/template/lib/editable/rastreio-navegacao.tsx +65 -0
  268. package/template/lib/editable/rastreio.tsx +168 -0
  269. package/template/lib/editable/server.ts +189 -0
  270. package/template/lib/editable/tokens.ts +22 -0
  271. package/template/lib/editable/verify.ts +24 -0
  272. package/template/lib/enrichment/combos.ts +184 -0
  273. package/template/lib/enrichment/index.ts +188 -0
  274. package/template/lib/enrichment/products.json +1 -0
  275. package/template/lib/env-check.ts +47 -0
  276. package/template/lib/format.ts +130 -0
  277. package/template/lib/icons.ts +67 -0
  278. package/template/lib/json-ld.ts +9 -0
  279. package/template/lib/llms-txt.ts +75 -0
  280. package/template/lib/mockup.ts +24 -0
  281. package/template/lib/newsletter.ts +12 -0
  282. package/template/lib/orders.ts +160 -0
  283. package/template/lib/queries.ts +66 -0
  284. package/template/lib/ratelimit.ts +47 -0
  285. package/template/lib/rotas-editaveis.ts +192 -0
  286. package/template/lib/sanitize.ts +36 -0
  287. package/template/lib/schemas.ts +78 -0
  288. package/template/lib/session.ts +98 -0
  289. package/template/lib/store-config.ts +67 -0
  290. package/template/lib/unbox/client.ts +829 -0
  291. package/template/lib/unbox/customer.ts +224 -0
  292. package/template/lib/unbox/errors.ts +97 -0
  293. package/template/lib/unbox/index.ts +10 -0
  294. package/template/lib/unbox/store.ts +113 -0
  295. package/template/lib/unbox/types.ts +195 -0
  296. package/template/lib/unbox/webhooks.ts +66 -0
  297. package/template/lib/utils.ts +6 -0
  298. package/template/lib/vitrine.ts +242 -0
  299. package/template/lib/webhook-store.ts +22 -0
  300. package/template/middleware.ts +178 -0
  301. package/template/next.config.ts +63 -0
  302. package/template/package.json +62 -0
  303. package/template/postcss.config.mjs +5 -0
  304. package/template/public/brand/coll/default.png +0 -0
  305. package/template/public/brand/hero-desktop.svg +12 -0
  306. package/template/public/brand/hero-mobile.svg +12 -0
  307. package/template/public/brand/heros/boutique-desktop.svg +11 -0
  308. package/template/public/brand/heros/boutique-mobile.svg +11 -0
  309. package/template/public/brand/heros/editorial-desktop.svg +11 -0
  310. package/template/public/brand/heros/editorial-mobile.svg +11 -0
  311. package/template/public/brand/heros/essencial-desktop.svg +12 -0
  312. package/template/public/brand/heros/essencial-mobile.svg +12 -0
  313. package/template/public/brand/heros/promocional-desktop.svg +12 -0
  314. package/template/public/brand/heros/promocional-mobile.svg +12 -0
  315. package/template/public/brand/logo-chrome.svg +6 -0
  316. package/template/public/brand/logo-white.svg +6 -0
  317. package/template/public/brand/logo.svg +7 -0
  318. package/template/public/brand/pay-amex.webp +0 -0
  319. package/template/public/brand/pay-elo.webp +0 -0
  320. package/template/public/brand/pay-mastercard.webp +0 -0
  321. package/template/public/brand/pay-pix.webp +0 -0
  322. package/template/public/brand/pay-visa.webp +0 -0
  323. package/template/public/brand/ph/avatar-a.svg +5 -0
  324. package/template/public/brand/ph/avatar-b.svg +5 -0
  325. package/template/public/brand/ph/avatar-c.svg +5 -0
  326. package/template/public/brand/ph/avatar-d.svg +5 -0
  327. package/template/public/brand/ph/photo-a.svg +7 -0
  328. package/template/public/brand/ph/photo-b.svg +6 -0
  329. package/template/public/brand/ph/photo-c.svg +6 -0
  330. package/template/public/brand/ph/poster-a.svg +6 -0
  331. package/template/public/brand/ph/poster-b.svg +6 -0
  332. package/template/public/brand/ph/poster-c.svg +6 -0
  333. package/template/public/unbox/powered-by-fundo-preto.png +0 -0
  334. package/template/public/unbox/powered-by-transparente.png +0 -0
  335. package/template/public/unbox/powered-by.png +0 -0
  336. package/template/scripts/abandoned-cart.ts +82 -0
  337. package/template/scripts/check-editable.mjs +467 -0
  338. package/template/scripts/check-honestidade.mjs +111 -0
  339. package/template/scripts/check-placeholder.mjs +200 -0
  340. package/template/scripts/check-recipe.mjs +76 -0
  341. package/template/scripts/check-unbox-brand.mjs +298 -0
  342. package/template/scripts/contraste.js +177 -0
  343. package/template/scripts/dump-catalog.ts +95 -0
  344. package/template/scripts/load-env.ts +13 -0
  345. package/template/scripts/medir-sistema.mjs +56 -0
  346. package/template/scripts/place-order-pix.ts +61 -0
  347. package/template/scripts/qa-screenshots.mjs +150 -0
  348. package/template/scripts/sistema.py +231 -0
  349. package/template/scripts/subscribe-webhook.ts +36 -0
  350. package/template/scripts/test-live.ts +186 -0
  351. package/template/scripts/vocabulario.py +264 -0
  352. package/template/tsconfig.json +27 -0
  353. package/tools/LEIA-ME.md +39 -0
  354. package/tools/check-template-neutro.mjs +111 -0
  355. package/tools/notion-doc.mjs +191 -0
  356. package/tools/tokenize-neutrals.mjs +0 -0
  357. package/tools/tokenize-radius.mjs +52 -0
  358. package/tools/workflow-storefront.legado.js +1207 -0
@@ -0,0 +1,224 @@
1
+ // UnboxCustomerClient — operações da ÁREA DO CLIENTE final.
2
+ // Usa o token do cliente (obtido via UnboxClient.customerSignIn). NÃO precisa da API key
3
+ // da loja — pode rodar num BFF/Route Handler com o token vindo de um cookie httpOnly.
4
+
5
+ import { UnboxError, comEnderecoDoGrupo } from "./client";
6
+ import type { AddressInput } from "./types";
7
+
8
+ const RECURRING_FIELDS = `
9
+ _id referenceId shopId createdAt updatedAt customerAccountId unboxPayCustomerId
10
+ status{value createdAt} frequency{_id title periodicity interval}
11
+ pricingPolicy{type value}
12
+ cyclesInformation{cycleCount skipNextCycle lastCycleDate nextCycleDate}
13
+ items{productId variantId productERPCode variantERPCode quantity skipNextCycle}
14
+ unboxPayCustomerCreditCard{first4Digits last4Digits expirationMonth expirationYear holderName}
15
+ shippingAddressBook{_id fullName postal address1 number neighborhood city region}
16
+ discount{discountId code} totalAmount{amount displayAmount}`;
17
+
18
+ export class UnboxCustomerClient {
19
+ gqlUrl: string;
20
+ shopId: string;
21
+ token: string;
22
+ language: string;
23
+
24
+ constructor(opts: { token: string; shopId: string; gqlUrl?: string; language?: string }) {
25
+ this.token = opts.token;
26
+ this.shopId = opts.shopId;
27
+ this.gqlUrl = opts.gqlUrl ?? "https://core.unbox.com.br/graphql";
28
+ this.language = opts.language ?? "pt-BR";
29
+ }
30
+
31
+ async gql<T = any>(query: string, variables: Record<string, any> = {}): Promise<T> {
32
+ const res = await fetch(this.gqlUrl, {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.token}` },
35
+ body: JSON.stringify({ query, variables }),
36
+ });
37
+ const json = await res.json();
38
+ if (json.errors?.length) throw new UnboxError(json.errors.map((e: any) => e.message).join(" | "), json.errors);
39
+ return json.data as T;
40
+ }
41
+
42
+ // --------------------------------------------------------------------- conta
43
+ async me(): Promise<any> {
44
+ const q = `query($s:String!){ currentCustomerAccount(shopId:$s){
45
+ _id email isFirstAccess reuseDataBetweenShops metafields{receiveNewOrderEmail}
46
+ addressBooks{_id alias fullName postal address1 number neighborhood city region isShippingDefault isBillingDefault}
47
+ lastAddressUsed{_id postal address1 city region} }}`;
48
+ const d = await this.gql<{ currentCustomerAccount: any }>(q, { s: this.shopId });
49
+ return d.currentCustomerAccount;
50
+ }
51
+
52
+ /** Atualiza preferências da conta (ex.: receiveNewOrderEmail, reuseDataBetweenShops). */
53
+ async updateAccount(input: { receiveNewOrderEmail?: boolean; reuseDataBetweenShops?: boolean }): Promise<any> {
54
+ const q = `mutation($i:UpdateCustomerAccountInput!){ updateCustomerAccount(input:$i){
55
+ _id reuseDataBetweenShops metafields{receiveNewOrderEmail} }}`;
56
+ const d = await this.gql<{ updateCustomerAccount: any }>(q, { i: { shopId: this.shopId, ...input } });
57
+ return d.updateCustomerAccount;
58
+ }
59
+
60
+ // -------------------------------------------------------------------- pedidos
61
+ // ⚠️ NÃO pedimos displayStatus: o resolver da Unbox lança
62
+ // "Cannot read properties of undefined (reading 'status')" e, por ser non-null,
63
+ // derruba a query inteira. Use `status` (enum) + orderStatusLabel() para o rótulo.
64
+ async orders(opts: { first?: number; filters?: any } = {}): Promise<any> {
65
+ const q = (endereco: string) => `query($shopId:ID!,$first:ConnectionLimitInt,$filters:CustomerOrderFilterInput){
66
+ customerOrders(shopId:$shopId,first:$first,filters:$filters,sortBy:_id,sortOrder:desc){
67
+ totalCount shippingMethods pageInfo{hasNextPage endCursor}
68
+ nodes{_id referenceId status createdAt
69
+ recurringOrderId dispatched delivered isBoletoPaid invoiceIssued
70
+ summary{total{amount displayAmount}}
71
+ fulfillmentGroups{
72
+ ${endereco}
73
+ items{nodes{_id title quantity thumbnail price{amount displayAmount}}}
74
+ }} }}`;
75
+ // ⚠️ totalItemQuantity removido: dispara "reading 'shop'" no resolver de customerOrders (bug Unbox).
76
+ const d = await comEnderecoDoGrupo((endereco) => this.gql<{ customerOrders: any }>(q(endereco), { shopId: this.shopId, first: opts.first ?? 20, filters: opts.filters }));
77
+ return d.customerOrders;
78
+ }
79
+
80
+ async order(referenceId: string): Promise<any> {
81
+ // Seleção validada ao vivo. Notas do schema (customer context):
82
+ // payments.data → PaymentData (objeto) fica FORA. fulfillmentGroups.data é a união
83
+ // OrderFulfillmentGroupData: entra pelo comEnderecoDoGrupo, que repete a consulta sem o
84
+ // campo se o servidor não resolver o tipo. trackingUrl NÃO existe (use trackingCode).
85
+ const q = (endereco: string) => `query($referenceId:ID!,$shopId:ID!){
86
+ customerOrderByReferenceId(referenceId:$referenceId,shopId:$shopId){
87
+ _id referenceId status email createdAt
88
+ summary{total{amount displayAmount}}
89
+ discounts{code label discount discountMethod}
90
+ payments{displayName mode processor isCaptured cardBrand captureErrorMessage amount{amount displayAmount}}
91
+ fulfillmentGroups{
92
+ status type trackingCode
93
+ ${endereco}
94
+ items{nodes{_id title variantTitle quantity thumbnail productSlug price{amount displayAmount} subtotal{displayAmount} productConfiguration{productId productVariantId}}}
95
+ }
96
+ recurringOrderId generatedNewRecurringOrder }}`;
97
+ const d = await comEnderecoDoGrupo((endereco) => this.gql<{ customerOrderByReferenceId: any }>(q(endereco), { referenceId, shopId: this.shopId }));
98
+ return d.customerOrderByReferenceId;
99
+ }
100
+
101
+ // ----------------------------------------------------------------- assinaturas
102
+ async subscriptions(opts: { first?: number; status?: string[] } = {}): Promise<any> {
103
+ const q = `query($filters:CustomerRecurringOrdersFilterInput,$first:ConnectionLimitInt){
104
+ customerRecurringOrders(filters:$filters,first:$first){
105
+ totalCount nodes{_id referenceId shopId createdAt unboxPayCustomerId} }}`;
106
+ const d = await this.gql<{ customerRecurringOrders: any }>(q, { first: opts.first ?? 10, filters: { status: opts.status, shopIds: [this.shopId] } });
107
+ return d.customerRecurringOrders;
108
+ }
109
+
110
+ async subscription(referenceId: string): Promise<any> {
111
+ const q = `query($referenceId:String!){ customerRecurringOrderByReferenceId(referenceId:$referenceId){${RECURRING_FIELDS}} }`;
112
+ const d = await this.gql<{ customerRecurringOrderByReferenceId: any }>(q, { referenceId });
113
+ return d.customerRecurringOrderByReferenceId;
114
+ }
115
+
116
+ async subscriptionCycles(recurringOrderId: string, first = 20): Promise<any> {
117
+ const q = `query($filters:RecurringOrderCyclesFilterInput!,$first:ConnectionLimitInt){
118
+ customerRecurringOrderCycles(filters:$filters,first:$first){
119
+ totalCount nodes{_id cycleIndex completedAt skipped attemptingRetry manuallyRetried createdAt} }}`;
120
+ const d = await this.gql<{ customerRecurringOrderCycles: any }>(q, { filters: { recurringOrderId }, first });
121
+ return d.customerRecurringOrderCycles;
122
+ }
123
+
124
+ /** Pausar/retomar assinatura (toggle — valida ao vivo: customerTogglePauseRecurringOrder). */
125
+ pause(recurringOrderId: string) {
126
+ return this.gql(`mutation($id:String!){customerTogglePauseRecurringOrder(recurringOrderId:$id){${RECURRING_FIELDS}}}`, { id: recurringOrderId });
127
+ }
128
+ /** Adiar / pular o próximo ciclo. */
129
+ skipNextCycle(recurringOrderId: string) {
130
+ return this.gql(`mutation($id:String!){customerSkipNextRecurringOrderCycle(recurringOrderId:$id){${RECURRING_FIELDS}}}`, { id: recurringOrderId });
131
+ }
132
+ /** Cancelar assinatura. */
133
+ cancel(recurringOrderId: string) {
134
+ return this.gql(`mutation($id:String!){customerCancelRecurringOrder(recurringOrderId:$id){${RECURRING_FIELDS}}}`, { id: recurringOrderId });
135
+ }
136
+ /** Trocar itens/quantidade. */
137
+ updateItems(recurringOrderId: string, items: Array<{ productId: string; variantId: string; quantity: number; skipNextCycle?: boolean }>) {
138
+ const q = `mutation($i:UpdateRecurringOrderItemsInput){customerUpdateRecurringOrderItems(input:$i){${RECURRING_FIELDS}}}`;
139
+ return this.gql(q, { i: { recurringOrderId, recurringOrderItems: items.map((x) => ({ ...x, skipNextCycle: x.skipNextCycle ?? false })) } });
140
+ }
141
+ /** Trocar cartão da assinatura. */
142
+ updateCard(recurringOrderId: string, card: { holderName: string; cardNumber: string; expirationMonth: string; expirationYear: string; securityCode: string }) {
143
+ const q = `mutation($i:UpdateRecurringOrderCreditCardInput!){customerUpdateRecurringOrderCreditCard(input:$i){${RECURRING_FIELDS}}}`;
144
+ return this.gql(q, { i: { recurringOrderId, creditCardData: card } });
145
+ }
146
+ /** Trocar endereço de entrega da assinatura. */
147
+ updateAddress(recurringOrderId: string, shippingAddress: AddressInput) {
148
+ const q = `mutation($i:UpdateRecurringOrderShippingAddressInput!){customerUpdateRecurringOrderShippingAddress(input:$i){
149
+ _id postal address1 number neighborhood city region fullName }}`;
150
+ return this.gql(q, { i: { recurringOrderId, shippingAddress: { country: "BR", ...shippingAddress } } });
151
+ }
152
+
153
+ // ------------------------------------------------------------------ endereços
154
+ /**
155
+ * Busca endereços por ID. ⚠️ `customerAddressBooks` exige ao menos 1 id (não aceita lista vazia).
156
+ * Para a LISTA COMPLETA do cliente, use `me().addressBooks` (vem em currentCustomerAccount).
157
+ */
158
+ async addressBooks(ids: string[]): Promise<any[]> {
159
+ if (!ids?.length) throw new UnboxError("customerAddressBooks exige ao menos 1 id — use me().addressBooks para a lista completa");
160
+ const q = `query($i:AddressBooksInput!){ customerAddressBooks(input:$i){
161
+ _id alias fullName postal address1 address2 number neighborhood city region taxPayerId phone
162
+ isShippingDefault isBillingDefault }}`;
163
+ const d = await this.gql<{ customerAddressBooks: any[] }>(q, { i: { addressBooksIds: ids, shopId: this.shopId } });
164
+ return d.customerAddressBooks;
165
+ }
166
+
167
+ /** Cria/atualiza um endereço do cliente (upsert). Sem `_id` cria; com `_id` atualiza. */
168
+ async upsertAddress(address: AddressInput & { _id?: string; alias?: string; isShippingDefault?: boolean; isBillingDefault?: boolean }): Promise<any> {
169
+ const q = `mutation($i:UpsertCustomerAddressBookInput!){ upsertCustomerAddressBook(input:$i){
170
+ _id alias fullName postal address1 number neighborhood city region isShippingDefault isBillingDefault }}`;
171
+ const d = await this.gql<{ upsertCustomerAddressBook: any }>(q, { i: { shopId: this.shopId, addressBook: { country: "BR", ...address } } });
172
+ return d.upsertCustomerAddressBook;
173
+ }
174
+
175
+ /** Remove endereços do address book pelos ids. */
176
+ async deleteAddresses(ids: string[]): Promise<any> {
177
+ const q = `mutation($i:DeleteCustomerAddressBooksInput!){ deleteCustomerAddressBooks(input:$i){ _id } }`;
178
+ const d = await this.gql<{ deleteCustomerAddressBooks: any }>(q, { i: { shopId: this.shopId, addressBooksIds: ids } });
179
+ return d.deleteCustomerAddressBooks;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Rótulos PT-BR para Order.status (workaround do displayStatus quebrado na Unbox).
185
+ * Valores conforme OrderStatusEnum do schema: PENDING, PROCESSING, COMPLETED, CANCELED, FAILED, REFUNDED.
186
+ */
187
+ export const ORDER_STATUS_LABELS: Record<string, string> = {
188
+ PENDING: "Aguardando pagamento",
189
+ PROCESSING: "Em processamento",
190
+ COMPLETED: "Concluído",
191
+ CANCELED: "Cancelado",
192
+ FAILED: "Falhou",
193
+ REFUNDED: "Reembolsado",
194
+ };
195
+ export function orderStatusLabel(status: string): string {
196
+ return ORDER_STATUS_LABELS[status] ?? status;
197
+ }
198
+
199
+ /** Rótulos PT-BR para PaymentStatusEnum. */
200
+ export const PAYMENT_STATUS_LABELS: Record<string, string> = {
201
+ CREATED: "Criado",
202
+ PENDING: "Aguardando pagamento",
203
+ AUTHORIZED: "Autorizado",
204
+ CAPTURED: "Capturado",
205
+ PAID: "Pago",
206
+ CANCELED: "Cancelado",
207
+ REFUNDED: "Reembolsado",
208
+ EXPIRED: "Expirado",
209
+ CHARGED_BACK: "Estornado (chargeback)",
210
+ };
211
+ export function paymentStatusLabel(status: string): string {
212
+ return PAYMENT_STATUS_LABELS[status] ?? status;
213
+ }
214
+
215
+ /** Rótulos PT-BR para RecurringOrderStatusEnum (assinatura). */
216
+ export const SUBSCRIPTION_STATUS_LABELS: Record<string, string> = {
217
+ ACTIVE: "Ativa",
218
+ PAUSED: "Pausada",
219
+ CANCELED: "Cancelada",
220
+ ERROR: "Com erro",
221
+ };
222
+ export function subscriptionStatusLabel(status: string): string {
223
+ return SUBSCRIPTION_STATUS_LABELS[status] ?? status;
224
+ }
@@ -0,0 +1,97 @@
1
+ // errors.ts — tradução de erros/eventos da Unbox em FEEDBACK ao cliente final (PT-BR).
2
+ //
3
+ // A Unbox sinaliza problemas de 3 formas (ver docs 08-feedback-ao-cliente):
4
+ // 1) erro LANÇADO no GraphQL (json.errors[].message = um CÓDIGO, ex. DISCOUNT_CODE_NOT_FOUND_ERROR)
5
+ // 2) campos de FALHA no payload (min/maxOrderQuantityFailures). Obs.: incorrectPriceFailures
6
+ // NÃO é confiável — o servidor sobrescreve o preço e o campo vem vazio (validado ao vivo).
7
+ // 3) cartEvents[] (eventos informativos: brinde adicionado, cupom removido, etc.)
8
+ //
9
+ // Use friendlyError() no catch das chamadas e cartEventLabel() ao renderizar o carrinho.
10
+
11
+ import { UnboxError } from "./client";
12
+
13
+ /** Código de erro (json.errors[].message) → mensagem amigável ao cliente. */
14
+ export const ERROR_MESSAGES: Record<string, string> = {
15
+ // ---- carrinho / itens ----
16
+ MISSING_ITEMS_ERROR: "Um ou mais produtos não estão mais disponíveis.",
17
+ ITEM_ERROR: "Há um problema com um item do seu carrinho.",
18
+ CREATE_CART_ERROR: "Não foi possível criar o carrinho. Tente novamente.",
19
+ OUT_OF_STOCK: "Produto esgotado.",
20
+ // ---- cupom / desconto ----
21
+ DISCOUNT_CODE_NOT_FOUND_ERROR: "Cupom inválido ou expirado.",
22
+ DISCOUNT_AVAILABLE_FOR_NON_RECURRING_PRODUCTS_ONLY_ERROR:
23
+ "Este cupom não vale para itens de assinatura. Troque a assinatura por compra única para usá-lo.",
24
+ DISCOUNT_AVAILABLE_FOR_RECURRING_PRODUCTS_ONLY_ERROR:
25
+ "Este cupom só vale para itens de assinatura.",
26
+ DISCOUNT_NOT_APPLICABLE_ERROR: "Este cupom não se aplica aos itens do seu carrinho.",
27
+ DISCOUNT_EXPIRED_ERROR: "Cupom expirado.",
28
+ DISCOUNT_USAGE_LIMIT_REACHED_ERROR: "Este cupom atingiu o limite de uso.",
29
+ DISCOUNT_MINIMUM_ORDER_NOT_MET_ERROR: "Seu pedido ainda não atingiu o valor mínimo para este cupom.",
30
+ DISCOUNT_ALREADY_APPLIED_ERROR: "Este cupom já está aplicado ao carrinho.",
31
+ // ---- endereço / frete ----
32
+ ADDRESS_BOOK_ERROR: "Endereço inválido. Confira os dados.",
33
+ NO_FULFILLMENT_OPTIONS: "Não há opções de entrega para este CEP.",
34
+ // ---- pagamento (recusas do processador / UnboxPay-Zoop) ----
35
+ INSUFFICIENT_FUNDS_ERROR: "Pagamento recusado: saldo/limite insuficiente.",
36
+ PAYMENT_ERROR: "Não foi possível processar o pagamento. Tente outro método.",
37
+ PLACE_ORDER_ERROR: "Não foi possível concluir o pedido. Tente novamente.",
38
+ CARD_DECLINED_ERROR: "Cartão recusado. Verifique os dados ou use outro cartão.",
39
+ FULFILLMENT_GROUP_AND_CART_ITEMS_DO_NOT_MATCH_ERROR: "Há um item promocional inválido no pedido. Atualize o carrinho e tente novamente.",
40
+ // ---- conta do cliente ----
41
+ CUSTOMER_ACCOUNT_ERROR: "Não foi possível acessar sua conta.",
42
+ INVALID_CREDENTIALS_ERROR: "Código ou credenciais inválidos.",
43
+ UNBOX_PAY_CUSTOMER_ERROR: "Falha no cadastro de pagamento. Tente novamente.",
44
+ // ---- infra / genéricos ----
45
+ FORBIDDEN_ERROR: "Ação não autorizada. Recarregue a página e tente novamente.", // ex.: falta x-captcha-verification
46
+ VALIDATION_ERROR: "Alguns dados são inválidos. Revise o formulário.",
47
+ UNEXPECTED_ERROR: "Algo deu errado. Tente novamente em instantes.",
48
+ INTERNAL_SERVER_ERROR: "Algo deu errado. Tente novamente em instantes.",
49
+ };
50
+
51
+ /** O login sem senha (OTP) NÃO responde no formato dos outros erros: em vez de um CÓDIGO_ERRO,
52
+ * devolve um corpo OAuth (`{"error":"invalid_grant","error_description":"Wrong email or
53
+ * verification code."}`). Sem este dicionário caía no fallback, e quem digitou um código
54
+ * expirado lia "Algo deu errado", redigitava o MESMO código e desistia (caso real). */
55
+ const ERROS_OAUTH: [RegExp, string][] = [
56
+ [/expired_token|code has expired|c[oó]digo expirad/i, "Este código expirou. Peça um novo."],
57
+ [/invalid_grant|wrong email or verification code/i, "Código incorreto ou expirado. Peça um novo código."],
58
+ [/too many|rate.?limit/i, "Muitas tentativas. Aguarde um instante e tente de novo."],
59
+ [/invalid_client|unauthorized_client/i, "Não foi possível validar o acesso. Recarregue a página e tente de novo."],
60
+ ];
61
+
62
+ const FALLBACK = "Algo deu errado. Tente novamente.";
63
+
64
+ /**
65
+ * Converte um erro do SDK/Unbox em mensagem amigável ao cliente.
66
+ * Casa por código exato e por substring (a mensagem do servidor costuma SER o código).
67
+ */
68
+ export function friendlyError(err: unknown): string {
69
+ let raw: string;
70
+ if (err instanceof UnboxError) raw = err.errors?.[0]?.message ?? err.message;
71
+ else if (err instanceof Error) raw = err.message;
72
+ else if (err && typeof err === "object") {
73
+ const o = err as any;
74
+ raw = o.errors?.[0]?.message ?? o.message ?? String(err);
75
+ } else raw = String(err);
76
+ // O corpo OAuth do login por código não casa com nenhum CÓDIGO_ERRO: testar antes do dicionário.
77
+ for (const [rx, msg] of ERROS_OAUTH) if (rx.test(raw)) return msg;
78
+ if (ERROR_MESSAGES[raw]) return ERROR_MESSAGES[raw];
79
+ for (const code of Object.keys(ERROR_MESSAGES)) {
80
+ if (raw.includes(code)) return ERROR_MESSAGES[code];
81
+ }
82
+ // validação de variável GraphQL (ex.: campo obrigatório ausente)
83
+ if (/required type|got invalid value|Expected/.test(raw)) return ERROR_MESSAGES.VALIDATION_ERROR;
84
+ return FALLBACK;
85
+ }
86
+
87
+ /** CartEventEnum (cart.cartEvents[].type) → mensagem informativa ao cliente. */
88
+ export const CART_EVENT_LABELS: Record<string, string> = {
89
+ SALE_FREE_ITEM_ADDED: "🎁 Você ganhou um brinde!",
90
+ SALE_FREE_ITEM_CHANGED: "Seu brinde foi atualizado.",
91
+ SALE_FREE_ITEM_REMOVED: "O brinde foi removido (condição da promoção não atendida).",
92
+ DISCOUNT_CODE_REMOVED: "O cupom foi removido do carrinho.",
93
+ DISCOUNT_CODE_RECOVERED: "Seu cupom foi reaplicado.",
94
+ };
95
+ export function cartEventLabel(type: string): string {
96
+ return CART_EVENT_LABELS[type] ?? type;
97
+ }
@@ -0,0 +1,10 @@
1
+ export { UnboxClient, UnboxError } from "./client";
2
+ export {
3
+ UnboxCustomerClient,
4
+ orderStatusLabel, paymentStatusLabel, subscriptionStatusLabel,
5
+ ORDER_STATUS_LABELS, PAYMENT_STATUS_LABELS, SUBSCRIPTION_STATUS_LABELS,
6
+ } from "./customer";
7
+ export { friendlyError, cartEventLabel, ERROR_MESSAGES, CART_EVENT_LABELS } from "./errors";
8
+ export { verifyUnboxWebhook, parseUnboxWebhook } from "./webhooks";
9
+ export type { UnboxWebhookEnvelope, UnboxWebhookEventType, UnboxOrderWebhookData } from "./webhooks";
10
+ export type * from "./types";
@@ -0,0 +1,113 @@
1
+ // Factory do UnboxClient de LOJA (server-only) com cache de token em memória.
2
+ //
3
+ // Regra de ouro (doc 11): NÃO fazer signIn() a cada request. O JWT vale ~24h; cacheamos num
4
+ // singleton de módulo e renovamos com folga (~23h). Em produção serverless, troque este cache
5
+ // por Vercel KV / Edge Config compartilhado entre lambdas (ver pontos `// PROD:`).
6
+ import "server-only";
7
+ import { UnboxClient, UnboxError } from "./client";
8
+ import { serverEnv, hasUnboxCredentials, decodeJwtClaims, SHOP_ID_CLAIM, SHOP_SLUG_CLAIM } from "../config";
9
+
10
+ interface CachedToken {
11
+ token: string;
12
+ expMs: number; // epoch ms em que devemos renovar (exp - folga)
13
+ shopId: string;
14
+ shopSlug: string;
15
+ }
16
+
17
+ // PROD: substituir por KV. Em dev/single-instance, o módulo singleton basta.
18
+ let cache: CachedToken | null = null;
19
+ let inFlight: Promise<CachedToken> | null = null;
20
+
21
+ const RENEW_BUFFER_MS = 60 * 60 * 1000; // renova 1h antes de expirar
22
+
23
+ async function doSignIn(): Promise<CachedToken> {
24
+ if (!hasUnboxCredentials) {
25
+ throw new Error(
26
+ "[unbox] credenciais ausentes (UNBOX_API_KEY/UNBOX_USER/UNBOX_PASS) — preencha .env.local. Rodando em modo mockup."
27
+ );
28
+ }
29
+ const client = new UnboxClient({
30
+ apiKey: serverEnv.apiKey,
31
+ shopId: serverEnv.shopId,
32
+ apiBaseUrl: serverEnv.authUrl,
33
+ gqlUrl: serverEnv.gqlUrl,
34
+ partnerApiKey: serverEnv.partnerApiKey,
35
+ partnerGqlUrl: serverEnv.partnerGqlUrl,
36
+ captchaBypass: serverEnv.captchaBypass,
37
+ });
38
+ const token = await client.signIn(serverEnv.user, serverEnv.pass);
39
+ const claims = decodeJwtClaims(token);
40
+ const expSec = typeof claims.exp === "number" ? claims.exp : Math.floor(Date.now() / 1000) + 23 * 3600;
41
+ return {
42
+ token,
43
+ expMs: expSec * 1000 - RENEW_BUFFER_MS,
44
+ shopId: serverEnv.shopId || claims[SHOP_ID_CLAIM] || "",
45
+ shopSlug: serverEnv.shopSlug || claims[SHOP_SLUG_CLAIM] || "",
46
+ };
47
+ }
48
+
49
+ async function getCachedToken(force = false): Promise<CachedToken> {
50
+ if (!force && cache && Date.now() < cache.expMs) return cache;
51
+ if (inFlight) return inFlight;
52
+ inFlight = doSignIn()
53
+ .then((c) => {
54
+ cache = c;
55
+ return c;
56
+ })
57
+ .finally(() => {
58
+ inFlight = null;
59
+ });
60
+ return inFlight;
61
+ }
62
+
63
+ /** shopId/shopSlug resolvidos (env tem prioridade; senão extrai do JWT). */
64
+ export async function getShopContext(): Promise<{ shopId: string; shopSlug: string }> {
65
+ const c = await getCachedToken();
66
+ return { shopId: c.shopId, shopSlug: c.shopSlug };
67
+ }
68
+
69
+ /**
70
+ * Retorna um UnboxClient de loja já autenticado (token do cache). Re-signin automático
71
+ * em 401/ACCESS_DENIED (token expirado) é tratado por `withStoreClient`.
72
+ */
73
+ export async function getStoreClient(): Promise<UnboxClient> {
74
+ const c = await getCachedToken();
75
+ const client = new UnboxClient({
76
+ apiKey: serverEnv.apiKey,
77
+ shopId: c.shopId,
78
+ apiBaseUrl: serverEnv.authUrl,
79
+ gqlUrl: serverEnv.gqlUrl,
80
+ partnerApiKey: serverEnv.partnerApiKey,
81
+ partnerGqlUrl: serverEnv.partnerGqlUrl,
82
+ captchaBypass: serverEnv.captchaBypass,
83
+ });
84
+ client.setToken(c.token);
85
+ return client;
86
+ }
87
+
88
+ /**
89
+ * Executa uma operação com o client de loja; se falhar por token expirado
90
+ * (401/ACCESS_DENIED/FORBIDDEN de auth), força re-signin UMA vez e repete.
91
+ */
92
+ export async function withStoreClient<T>(fn: (c: UnboxClient) => Promise<T>): Promise<T> {
93
+ const client = await getStoreClient();
94
+ try {
95
+ return await fn(client);
96
+ } catch (e) {
97
+ if (isAuthExpired(e)) {
98
+ const c = await getCachedToken(true);
99
+ client.setToken(c.token);
100
+ return fn(client);
101
+ }
102
+ throw e;
103
+ }
104
+ }
105
+
106
+ function isAuthExpired(e: unknown): boolean {
107
+ if (e instanceof UnboxError) {
108
+ const msg = (e.errors?.[0]?.message ?? e.message ?? "").toUpperCase();
109
+ return /ACCESS_DENIED|UNAUTHENTICATED|TOKEN|EXPIRED|401/.test(msg);
110
+ }
111
+ if (e instanceof Error) return /401|ACCESS_DENIED/i.test(e.message);
112
+ return false;
113
+ }
@@ -0,0 +1,195 @@
1
+ // Tipos da API Unbox (headless commerce). Apenas tipos — apagados em runtime.
2
+
3
+ export interface Money {
4
+ amount: number;
5
+ displayAmount?: string;
6
+ currency?: { code: string };
7
+ }
8
+
9
+ export interface UnboxConfig {
10
+ /** API key da loja (modelo antigo). Usada no x-api-key (signin REST) e no
11
+ * x-captcha-verification (placeOrder, customerOTPRequest, customerPasswordlessSignIn).
12
+ * Server-only. Pode ficar vazia quando `partnerApiKey` está configurada. */
13
+ apiKey: string;
14
+ /** shopId da loja (ex.: "8EaeSDX99hifhyTQp"). */
15
+ shopId: string;
16
+ /** Base REST de auth. Default: https://api.unbox.com.br */
17
+ apiBaseUrl?: string;
18
+ /** Endpoint GraphQL do core. Default: https://core.unbox.com.br/graphql */
19
+ gqlUrl?: string;
20
+ /** API de PARCEIROS (nova, pública): api key ÚNICA do parceiro, vale para todas as lojas
21
+ * dele. Presente → signIn vira mutation GQL na API de parceiros e as leituras com
22
+ * paridade (tags, cupons, pedido, parcelas, inventário, webhook, cart template) são
23
+ * roteadas para lá. A loja específica é autenticada pelo user/senha no signIn. */
24
+ partnerApiKey?: string;
25
+ /** Endpoint GraphQL da API de parceiros. Default: https://partners.unbox.com.br/graphql */
26
+ partnerGqlUrl?: string;
27
+ /** x-captcha-verification do signIn na API de parceiros — OBRIGATÓRIO segundo a doc
28
+ * oficial (docs.unbox.com.br); fornecido pela Unbox. Sem ele o header é omitido e o
29
+ * signIn de parceiros tende a falhar. */
30
+ captchaBypass?: string;
31
+ /** Idioma de respostas/rótulos. Default: "pt-BR".
32
+ * ⚠️ displayStatus(language) tem resolver quebrado no live — use status cru + orderStatusLabel(). */
33
+ language?: string;
34
+ /** Timeout por request (ms). Default: 15000. Aplica a signin e a toda chamada GraphQL. */
35
+ timeoutMs?: number;
36
+ }
37
+
38
+ export interface ProductVariant {
39
+ _id: string;
40
+ title: string;
41
+ sku?: string | null;
42
+ pricing?: Array<{ price: number | null; displayPrice: string; compareAtPrice?: { displayAmount: string } | null }>;
43
+ }
44
+
45
+ export interface CatalogProduct {
46
+ _id: string;
47
+ productId: string;
48
+ title: string;
49
+ slug: string;
50
+ productType: string;
51
+ isVisible: boolean;
52
+ isSoldOut: boolean;
53
+ isBackorder?: boolean;
54
+ isLowQuantity?: boolean;
55
+ recurrenceAllowed?: boolean;
56
+ imageUrls?: string[];
57
+ videoUrls?: string[];
58
+ description?: string;
59
+ additionalInformation?: string;
60
+ pageTitle?: string;
61
+ metaDescription?: string;
62
+ tagIds?: string[];
63
+ minOrderQuantity?: number | null;
64
+ maxOrderQuantity?: number | null;
65
+ pricing?: Array<{ displayPrice: string; price: number | null; minPrice: number; maxPrice: number }>;
66
+ variants?: ProductVariant[];
67
+ }
68
+
69
+ export interface Connection<T> {
70
+ totalCount: number;
71
+ nodes: T[];
72
+ pageInfo?: { hasNextPage: boolean; endCursor?: string };
73
+ }
74
+
75
+ export interface AddressInput {
76
+ fullName: string;
77
+ taxPayerId: string;
78
+ postal: string;
79
+ address1: string;
80
+ number: string;
81
+ neighborhood: string;
82
+ city: string;
83
+ region: string;
84
+ phone: string; // OBRIGATÓRIO (String!) — validado ao vivo
85
+ country?: string; // default "BR" aplicado pelo SDK
86
+ address2?: string;
87
+ cityCode?: string;
88
+ isCommercial?: boolean;
89
+ trackByMobile?: boolean;
90
+ }
91
+
92
+ export interface CartItemInput {
93
+ productId: string;
94
+ /** ⚠️ É `productVariantId`, NÃO `variantId`. Errar o nome (em payload montado à mão, fora
95
+ * deste tipo) devolve um erro de GraphQL que só cita `productConfiguration`, sem dizer
96
+ * qual campo faltou — armadilha clássica ao escrever testes contra a API. */
97
+ productVariantId: string;
98
+ /** Preço da variante. ⚠️ O servidor SOBRESCREVE com o preço do catálogo (validado ao vivo:
99
+ * enviar 0.01 grava o preço real e `incorrectPriceFailures` vem vazio). Envie o preço real
100
+ * por correção/UX, mas a fonte da verdade é o catálogo. */
101
+ price: number;
102
+ quantity: number;
103
+ isRecurring?: boolean;
104
+ thumbnail?: string;
105
+ currencyCode?: string;
106
+ }
107
+
108
+ export interface FulfillmentOption {
109
+ fulfillmentMethod: { _id: string; name: string; displayName?: string; daysToDeliver?: number };
110
+ price: Money;
111
+ discountPrice?: { displayAmount?: string };
112
+ }
113
+
114
+ export interface CartResult {
115
+ cartId: string;
116
+ cartToken: string;
117
+ cart: any;
118
+ incorrectPriceFailures?: any[];
119
+ minOrderQuantityFailures?: Array<{ minOrderQuantity: number; quantity: number }>;
120
+ maxOrderQuantityFailures?: Array<{ maxOrderQuantity: number; quantity: number }>;
121
+ }
122
+
123
+ export type PaymentType = "pix" | "credit" | "boleto";
124
+
125
+ /** Fingerprint antifraude/3DS exigido pela Unbox no placeOrder. Vai no NÍVEL RAIZ do
126
+ * PlaceOrderInput (irmão de `order` e `payments`), como campo único com enum `type`:
127
+ * - BROWSER: dados reais do navegador do comprador (coletados no checkout-client).
128
+ * - API: fallback para pedidos originados no servidor (scripts), sem navegador. */
129
+ export type DeviceInput =
130
+ | {
131
+ type: "BROWSER";
132
+ colorDepth: number;
133
+ javaEnabled: boolean;
134
+ userAgent: string;
135
+ language: string;
136
+ screenHeight: number;
137
+ screenWidth: number;
138
+ /** ⚠️ Ponto aberto com a Unbox: enviamos MINUTOS (getTimezoneOffset(), BRT → 180),
139
+ * igual ao storefront de referência. A doc mostrava 3 (horas). Se a Unbox confirmar
140
+ * horas, dividir por 60 na coleta (checkout-client.tsx). Não quebra o pedido —
141
+ * afeta só o sinal de antifraude. */
142
+ timezoneOffset: number;
143
+ }
144
+ | { type: "API" };
145
+
146
+ export interface CardData {
147
+ cardHolder: string;
148
+ cardNumber: string;
149
+ expirationMonth: string;
150
+ expirationYear: string;
151
+ securityCode: string;
152
+ installments?: number;
153
+ }
154
+
155
+ export interface PlaceOrderParams {
156
+ cartId: string;
157
+ email: string;
158
+ address: AddressInput;
159
+ fulfillmentMethodId: string;
160
+ /** total final (cart.checkout.summary.total.amount após selecionar o frete). */
161
+ total: number;
162
+ /** itens finais do carrinho — TODOS (inclusive brindes), com os flags `isRecurring` e
163
+ * `isDiscountedBonusItem` (use buildOrderItems). Omitir os flags/brindes → 502 (ver buildOrderItems). */
164
+ items: Array<{ productConfiguration: { productId: string; productVariantId: string }; price: number; quantity: number; addedAt?: string; thumbnail?: string; isRecurring?: boolean; isDiscountedBonusItem?: boolean }>;
165
+ payment:
166
+ | { type: "pix" }
167
+ | { type: "card"; card: CardData };
168
+ /** assinatura: cria recurring order. */
169
+ recurrence?: { recurringItemsFrequencyId: string };
170
+ /** fingerprint antifraude — omitido, o client envia { type: "API" } como fallback. */
171
+ device?: DeviceInput;
172
+ }
173
+
174
+ /** Inventário de uma variante (API de parceiros — simpleInventory). */
175
+ export interface SimpleInventoryInfo {
176
+ _id: string;
177
+ canBackorder: boolean;
178
+ inventoryInStock: number;
179
+ inventoryReserved: number;
180
+ isEnabled: boolean;
181
+ lowInventoryWarningThreshold?: number | null;
182
+ productConfiguration: { productId: string; productVariantId: string };
183
+ }
184
+
185
+ export interface SubscriptionFrequency {
186
+ _id: string;
187
+ title: string;
188
+ periodicity: "DAY" | "WEEK" | "MONTH" | string;
189
+ interval: number;
190
+ }
191
+
192
+ export interface PaymentLinkConstraints {
193
+ expirationDate?: string;
194
+ usageLimit?: number;
195
+ }