@rock-encantech/vtex-faststore-sdk
v1.0.1
Published
SDK for Propz FastStore integration — offers context, price display, add-to-cart with manual price
Readme
@rockencantech/vtex-faststore-sdk
SDK para integração de ofertas personalizadas Rock Encantech com lojas VTEX FastStore.
Sumário
- Instalação
- Configuração
- Duas abordagens de uso
- Componentes
- Hooks
- Funções utilitárias
- Tipos
- Mecânicas
- Regras de negócio
- Arquitetura interna
- Troubleshooting
Instalação
npm install @rockencantech/vtex-faststore-sdkConfiguração
Variáveis de ambiente
NEXT_PUBLIC_ROCK_API_URL=https://sua-rock-api.com
NEXT_PUBLIC_ROCK_CLIENT_ID=seu-client-id
NEXT_PUBLIC_ROCK_CLIENT_SECRET=seu-client-secretAdicione no painel de secrets do FastStore (WebOps Settings).
Duas abordagens de uso
Abordagem 1: Plug-and-play (zero código)
O SDK faz tudo automaticamente. O dev só monta os componentes:
// src/components/index.tsx
import { RockProductDetails, RockShelf, RockProvider } from '../sdk/propz-sdk'
import { OverriddenDefaultProductDetails as ProductDetails } from 'src/components/sections/ProductDetails/OverriddenDefaultProductDetails'
import { OverriddenDefaultHero as Hero } from 'src/components/sections/Hero/OverriddenDefaultHero'
import React from 'react'
function ProductDetailsWithRock(props: any) {
return (
<>
<RockProductDetails />
<ProductDetails {...props} />
</>
)
}
ProductDetailsWithRock.$componentKey = 'ProductDetails'
function HeroWithRock(props: any) {
return (
<>
<RockProvider />
<Hero {...props} />
<RockShelf />
</>
)
}
HeroWithRock.$componentKey = 'Hero'
export default { ProductDetails: ProductDetailsWithRock, Hero: HeroWithRock }O que o plug-and-play entrega:
- PDP: preço riscado + preço promo + badge da mecânica (tudo via DOM)
- Carrinho: manual price aplicado automaticamente
- Checkout: interceptor garante preço correto
- Shelf: vitrine com cards, badges e slider
- Funciona com o botão padrão do FastStore (não precisa trocar)
Abordagem 2: Dados + layout próprio (controle total)
O dev usa hooks e funções para montar seu próprio layout:
import {
useOffers,
useProductOffer,
getOfferBadge,
calculateOfferPrice,
isOfferApplicable,
RockProvider
} from '@rockencantech/vtex-faststore-sdk'
// Monte o RockProvider em algum lugar global (ex: Hero, Navbar)
// para inicializar a busca de ofertas:
<RockProvider />
// Depois, em qualquer componente:
function MinhaPDP({ product }) {
const offer = useProductOffer(product.sku)
const badge = offer ? getOfferBadge(offer) : null
return (
<div>
{badge && <span className="minha-badge">{badge}</span>}
<h1>{product.name}</h1>
{offer ? (
<div>
<s>R${offer.listPrice}</s>
<strong>R${offer.sellingPrice}</strong>
</div>
) : (
<span>R${product.price}</span>
)}
</div>
)
}
// Shelf customizada com cards da loja:
function MinhaShelf() {
const { offers, isAuthenticated } = useOffers()
if (!isAuthenticated || !offers.length) return null
return (
<div className="minha-shelf">
{offers.map(offer => (
<div key={offer.skuId} className="meu-card">
<img src={offer.imageUrl} alt={offer.productName} />
<h3>{offer.itemName}</h3>
<span>{getOfferBadge(offer)}</span>
<p>R${offer.sellingPrice}</p>
</div>
))}
</div>
)
}Nota sobre o carrinho/checkout: Mesmo usando layout próprio, se quiser que o desconto seja aplicado no orderForm (carrinho + checkout), use <RockProductDetails /> junto. Ele é invisível e cuida da lógica de apply/restore em background sem afetar seu layout.
Componentes
RockProductDetails
Componente invisível que gerencia toda a lógica de ofertas na PDP e carrinho.
O que faz automaticamente:
- Busca ofertas se ainda não carregadas (self-contained)
- Aplica preço riscado + preço promo no DOM da PDP
- Injeta badge da mecânica acima do título do produto
- Escuta click no
[data-fs-buy-button]→ aplica manual price no orderForm - Polling (5s) para virtual_pack/get_and_pay → recalcula quando qty muda
- Restaura preço original quando qty cai abaixo do mínimo
- Intercepta checkout → aplica/recalcula todos items antes de redirecionar
- Detecta troca de variação (poll no
data-skudo DOM) - CPF cacheado (não re-busca a cada click)
Uso:
<RockProductDetails />Sem props. Sem configuração. Monta e esquece.
RockProvider
Componente invisível que inicializa a busca de ofertas no offersStore.
Quando usar: Em pages que não têm RockProductDetails (ex: Home). O RockProductDetails já faz isso internamente, então na PDP não precisa.
Uso:
<RockProvider />RockShelf
Vitrine de ofertas personalizadas. 5 cards por linha, slider com setas quando > 5.
Uso básico:
<RockShelf />Customizado:
<RockShelf
title="Suas ofertas exclusivas"
loadingText="Buscando promoções..."
loginImage="https://cdn.com/banner-login.png"
renderCard={(offer) => <MeuCard product={offer} />}
/>Props:
| Prop | Tipo | Padrão | Descrição |
|------|------|--------|-----------|
| title | string | "Ofertas para você" | Título (só aparece quando há ofertas) |
| loadingText | string | "Carregando ofertas..." | Texto durante loading |
| loginImage | string | "" | URL da imagem quando deslogado (vazio = não renderiza) |
| renderCard | (offer: RockOffer) => ReactNode | undefined | Card customizado da loja |
Comportamento:
- Deslogado + sem
loginImage→ não renderiza nada - Deslogado + com
loginImage→ exibe imagem - Logado sem ofertas → não renderiza nada
- Logado com ofertas → exibe vitrine
- Mais de 5 ofertas → slider horizontal com setas
RockBadge
Badge da mecânica como componente React. Para uso fora do plug-and-play (o RockProductDetails já injeta badge automaticamente).
Uso:
import { RockBadge } from '@rockencantech/vtex-faststore-sdk'
// Detecta SKU do DOM automaticamente:
<RockBadge />
// Com SKU específico:
<RockBadge skuId="8" />
// Estilizado:
<RockBadge style={{ background: '#ff6600', fontSize: '1rem' }} className="my-badge" />Props:
| Prop | Tipo | Descrição |
|------|------|-----------|
| skuId | string (opcional) | SKU da oferta. Se omitido, lê do DOM ([data-fs-buy-button][data-sku]) |
| style | CSSProperties | Override de estilo inline |
| className | string | Classe CSS adicional |
Comportamento:
- SKU sem oferta → não renderiza nada
- SKU com oferta → renderiza badge colorida com texto da mecânica
- Atualiza ao trocar variação (poll 500ms)
RockPrice
Componente React de preço. Self-contained (busca ofertas se necessário).
Uso:
import { RockPrice } from '@rockencantech/vtex-faststore-sdk'
<RockPrice skuId="3" basePrice={16.90} />Props:
| Prop | Tipo | Descrição |
|------|------|-----------|
| skuId | string | SKU do produto |
| basePrice | number | Preço original (sem desconto) |
| listPrice | number (opcional) | Preço "de" (se diferente de basePrice) |
| formatter | (price: number) => string | Formatador customizado |
Comportamento:
- Sem oferta → renderiza preço normal
- Com oferta → renderiza preço riscado + preço promo
RockAddToCart
Botão de adicionar ao carrinho com lógica de desconto embutida. Opcional — o botão padrão do FastStore funciona com RockProductDetails.
Uso:
import { RockAddToCart } from '@rockencantech/vtex-faststore-sdk'
<RockAddToCart skuId="3" quantity={1} onAddToCart={() => cartStore.addItem(item)}>
Comprar com desconto
</RockAddToCart>Hooks
useOffers()
Retorna todas as ofertas da sessão do usuário logado.
import { useOffers } from '@rockencantech/vtex-faststore-sdk'
function MeuComponente() {
const { offers, isLoading, isAuthenticated } = useOffers()
// offers: RockOffer[]
// isLoading: boolean
// isAuthenticated: boolean
}Exemplo completo:
function OfertasCustomizadas() {
const { offers, isLoading, isAuthenticated } = useOffers()
if (isLoading) return <p>Carregando...</p>
if (!isAuthenticated) return <p>Faça login para ver ofertas</p>
if (offers.length === 0) return null
return (
<ul>
{offers.map(offer => (
<li key={offer.skuId}>
<img src={offer.imageUrl} alt={offer.productName} />
<h3>{offer.itemName}</h3>
<p>De: R${offer.listPrice} | Por: R${offer.sellingPrice}</p>
<span>{getOfferBadge(offer)}</span>
</li>
))}
</ul>
)
}useProductOffer(skuId)
Retorna a oferta de um SKU específico ou null.
import { useProductOffer } from '@rockencantech/vtex-faststore-sdk'
function MeuPreco({ skuId }: { skuId: string }) {
const offer = useProductOffer(skuId)
if (!offer) return <span>Sem oferta</span>
return (
<div>
<s>R${offer.listPrice}</s>
<strong>R${offer.sellingPrice}</strong>
<small>{offer.mechanic}</small>
</div>
)
}Funções utilitárias
getOfferBadge(offer)
Retorna o texto da badge para a mecânica da oferta.
import { getOfferBadge } from '@rockencantech/vtex-faststore-sdk'
const badge = getOfferBadge(offer)Retornos:
| Mecânica | Badge gerada |
|----------|-------------|
| percent_off | "Desconto exclusivo" |
| money_off | "Desconto exclusivo" |
| reduced_price | "Preço exclusivo" |
| virtual_pack | "A cada 3 un. pague R$ 2,00/un." |
| get_and_pay | "A partir de 6 un. por R$ 5,00/un." |
Exemplo de uso customizado:
function MinhaBadge({ skuId }: { skuId: string }) {
const offer = useProductOffer(skuId)
if (!offer) return null
const text = getOfferBadge(offer)
if (!text) return null
return <div className="badge-customizada">{text}</div>
}calculateOfferPrice(basePrice, offer, quantity?)
Calcula o preço final por unidade considerando mecânica e quantidade.
import { calculateOfferPrice } from '@rockencantech/vtex-faststore-sdk'
// Mecânicas simples (qty não importa):
calculateOfferPrice(16.90, offer) // percent_off: 14.90
calculateOfferPrice(16.90, offer) // money_off: 14.90
calculateOfferPrice(16.90, offer) // reduced_price: 10.00
// virtual_pack (preço médio muda com qty):
calculateOfferPrice(5.00, offer, 3) // 2.00 (3 no pack)
calculateOfferPrice(5.00, offer, 5) // 3.20 (média: 3×2 + 2×5 / 5)
calculateOfferPrice(5.00, offer, 6) // 2.00 (2 packs completos)
// get_and_pay (todas com desconto quando qty >= min):
calculateOfferPrice(5.00, offer, 3) // 2.00
calculateOfferPrice(5.00, offer, 5) // 2.00isOfferApplicable(offer, quantity)
Verifica se a oferta pode ser aplicada para uma dada quantidade.
import { isOfferApplicable } from '@rockencantech/vtex-faststore-sdk'
isOfferApplicable(offer, 1) // false (virtual_pack com minQty=3)
isOfferApplicable(offer, 3) // true
isOfferApplicable(offer, 5) // trueTipos
RockOffer
interface RockOffer {
skuId: string // ID do SKU (itemId VTEX)
productId: string // ID do produto
productName: string // Nome do produto
linkText: string // Slug do produto
itemName: string // Nome completo da variação
itemDetailUrl: string // URL da PDP da variação
imageUrl: string // URL da imagem do produto
mechanic: OfferMechanic // Tipo de mecânica
discountPercent: number | null // % desconto (percent_off)
discountAmount: number | null // R$ desconto (money_off)
finalPrice: number | null // Preço fixo (reduced_price/virtual_pack/get_and_pay)
minQuantity: number // Qty mínima para ativar
remainingMaxPerCustomer: number | 'full-promotion' // Limite por cliente
sellingPrice: number // Preço com desconto
listPrice: number // Preço original
}
type OfferMechanic = 'percent_off' | 'money_off' | 'reduced_price' | 'virtual_pack' | 'get_and_pay'Mecânicas
percent_off / money_off / reduced_price
Desconto aplicado imediatamente para qualquer quantidade.
- PDP: preço riscado + preço promo + badge "Desconto exclusivo" ou "Preço exclusivo"
- Carrinho: manual price aplicado ao clicar "Comprar"
- Checkout: garantido pelo interceptor
virtual_pack — "A cada X pague Y por unidade"
Desconto aplicado em packs completos. Unidades restantes pagam preço cheio.
- PDP: não mostra desconto (exige qty mínima no carrinho)
- Shelf: badge "A cada 3 un. pague R$ 2,00/un."
- Carrinho: aplica preço médio por unidade
- Qty < min: restaura preço original automaticamente
| Qty | Cálculo (minQty=3, promo=R$2, base=R$5) | Total | Preço/un | |-----|------------------------------------------|-------|----------| | 2 | Sem desconto | R$10,00 | R$5,00 | | 3 | 1 pack (3×R$2) | R$6,00 | R$2,00 | | 5 | 1 pack (3×R$2) + 2 restantes (2×R$5) | R$16,00 | R$3,20 | | 6 | 2 packs (6×R$2) | R$12,00 | R$2,00 | | 7 | 2 packs (6×R$2) + 1 restante (1×R$5) | R$17,00 | R$2,43 |
get_and_pay — "A partir de X pague Y por unidade"
Desconto aplicado para todas as unidades quando qty >= min.
- PDP: não mostra desconto (exige qty mínima no carrinho)
- Shelf: badge "A partir de 6 un. por R$ 5,00/un."
- Carrinho: aplica finalPrice para todas as unidades
- Qty < min: restaura preço original automaticamente
| Qty | Cálculo (minQty=3, promo=R$2, base=R$5) | Total | Preço/un | |-----|------------------------------------------|-------|----------| | 2 | Sem desconto | R$10,00 | R$5,00 | | 3 | Todas a R$2 | R$6,00 | R$2,00 | | 5 | Todas a R$2 | R$10,00 | R$2,00 | | 6 | Todas a R$2 | R$12,00 | R$2,00 |
Regras de negócio
- Menor preço prevalece: preço Propz > preço VTEX → mantém VTEX, não mostra na shelf
- Restauração automática: qty < min + manual price existente → restaura preço base
- Interceptor de checkout: garante preço correto antes do redirect (anti-exploit)
- Limite por cliente: respeita
remainingMaxPerCustomerda Propz - Variações: cada SKU tem preço próprio (match por refId)
- Botão padrão: funciona com
[data-fs-buy-button]do FastStore (não precisa trocar)
Arquitetura interna
┌───────────────────────────────────────────────────────────────┐
│ Browser (FastStore) │
├───────────────────────────────────────────────────────────────┤
│ │
│ offersStore (singleton global) │
│ └── { offers: RockOffer[], isLoading, isAuthenticated } │
│ │
│ RockProductDetails (invisível, tudo automático) │
│ ├── ensureOffersLoaded → /api/vtexid → getPromotions │
│ ├── ensureCpfLoaded (cacheado) │
│ ├── applyPdpPrice → DOM [data-fs-price-variant] │
│ ├── injectBadge → DOM [data-fs-product-details-title] │
│ ├── skuPoll (500ms) → detecta troca variação │
│ ├── handleBuyClick → polling 300ms → /api/offers/apply │
│ │ └── fsCart.set → força re-validate │
│ ├── periodicCheck (5s) → virtual_pack/get_and_pay │
│ │ └── aplica ou restaura conforme qty vs minQty │
│ └── handleCheckoutClick → apply all → redirect │
│ │
│ RockShelf / RockBadge / RockPrice │
│ └── Lêem do offersStore via hooks │
│ │
└───────────────────────────────────────────────────────────────┘
│
│ HTTPS (X-Client-Id + X-Client-Secret)
▼
┌───────────────────────────────────────────────────────────────┐
│ Rock Encantech API (BFF) │
│ /api/promotion → ofertas Propz + catálogo VTEX │
│ /api/offers/apply → manual price + restore │
│ /api/user-document → email → CPF │
└───────────────────────────────────────────────────────────────┘Troubleshooting
| Problema | Verificar |
|----------|-----------|
| Preço não aparece na PDP | Env vars NEXT_PUBLIC_ROCK_* configuradas? Usuário logado? |
| Badge não aparece | O produto tem oferta na Propz? RockProductDetails montado? |
| Desconto não aplica no carrinho | Logs da API: /api/offers/apply chamado? appKey com permissão? |
| virtual_pack não recalcula | periodicCheck roda a cada 5s. Para garantia: vá ao checkout (interceptor) |
| Preço errado no checkout | Impossível — interceptor aplica/recalcula antes de redirecionar |
| Shelf não aparece | RockProvider montado? Usuário autenticado? loginImage definido? |
| Variações com preço errado | Cada variação precisa de promoção separada na Propz (refId individual) |
| Preço Propz > VTEX | Correto — produto não aparece na shelf (regra de menor preço) |
| Console: "Missing environment variables" | Env vars no build. Warning pode ser ignorado se funciona no browser |
