Cliente TypeScript do Notification Service (envio M2M, inbox, anexos, templates, erros) e editor visual de templates de email.
Dois pacotes npm para o frontend:
| Pacote | Versão | Para quê |
|---|---|---|
@igrp/platform-notification-client-ts | 0.2.0-beta.4 | Cliente REST do Notification Service: envio, inbox, anexos, templates |
@igrp/notification-template-editor | 0.1.0-beta.5 | Editor visual de templates de email (React) |
Os pacotes estão no registry do iGRP:
# .npmrc
@igrp:registry=https://sonatype.nosi.cv/repository/igrp/O passo a passo de uma app Next.js completa (factories, Server Actions, sino) está em Envio — SDKs e API. Esta página é a referência do SDK.
@igrp/platform-notification-client-ts
pnpm add @igrp/platform-notification-client-tsNotificationClient agrupa um cliente por domínio da API, todos com a mesma configuração:
| Propriedade | Endpoint | Uso típico |
|---|---|---|
notifications | /api/v1/notifications | send, list, get_, resend |
inbox | /api/v1/inbox | Caixa de entrada IN_APP do utilizador |
attachments | /api/v1/attachments | upload / download de anexos |
templates | /api/v1/templates | list, create, update, publish, archive, preview |
bulk, batches | Envios em massa e o seu estado | |
recipients, groups | Destinatários e grupos guardados | |
recurrences | Envios recorrentes | |
deliveries, stats | Entregas por canal e estatísticas | |
settings, webhooks, admin | Configuração da aplicação e administração |
Todos os métodos devolvem ApiResponse<T>: o corpo está em .data (e há status e statusText).
Autenticação
O serviço aceita dois tipos de token, e cada um serve para operações diferentes:
| Token | Para quê | Como criar o cliente |
|---|---|---|
| Utilizador (sessão) | Inbox do próprio utilizador: o userId sai do token | headers.Authorization com o token da sessão |
M2M (client_credentials) | Enviar, anexos, templates, administração | m2mConfig com clientId / clientSecret / authBaseUrl |
Utilizador final
import { NotificationClient } from '@igrp/platform-notification-client-ts';
import { auth } from '@/lib/auth';
const token = await auth.getAccessToken(); // sessão do template iGRP
const client = NotificationClient.create({
baseUrl: process.env.NOTIFICATION_SERVICE_BASE_URL!,
timeout: 30000,
headers: { Authorization: `Bearer ${token?.accessToken}` },
});M2M
import { NotificationClient } from '@igrp/platform-notification-client-ts';
const client = NotificationClient.create(
{ baseUrl: process.env.NOTIFICATION_SERVICE_BASE_URL! },
{
clientId: process.env.IGRP_M2M_CLIENT_ID!,
clientSecret: process.env.IGRP_M2M_CLIENT_SECRET!,
authBaseUrl: process.env.IGRP_ACCESS_MANAGEMENT_API!, // Access Management, não o Notification Service
serviceId: process.env.IGRP_SERVICE_ID, // opcional: header X-Machine-Service-ID
},
);O SDK pede o token em POST {authBaseUrl}/oauth2/token, guarda-o em cache e renova-o 30 s antes de expirar. scope é opcional: só o passes se o cliente OAuth2 o restringir.
authBaseUrl é o Access Management
Se authBaseUrl faltar, o SDK pede o token ao baseUrl do Notification Service, que responde 401. Aponta-o para a base do Access Management (ou do IdP).
Cria o cliente M2M uma vez
Cada cliente de domínio tem a sua cache de token. Guarda o NotificationClient num módulo (singleton) em vez de o criar a cada pedido. Se precisares do token em bruto, usa ClientCredentialsTokenProvider (exportado pelo pacote) e passa-o em headers.Authorization.
Todas as variáveis de ambiente são lidas no servidor (Server Actions, Route Handlers). O clientSecret nunca pode chegar ao bundle do browser.
applicationCode
Cada pedido pertence a uma aplicação do Access Management. O serviço lê-a do claim application_code do token e, se o token não a tiver, do applicationCode do pedido. Os tokens client_credentials normalmente não trazem esse claim (o erro é APPLICATION_CODE_REQUIRED), por isso passa sempre o código da app (IGRP_APP_CODE):
| Operação | Onde vai |
|---|---|
notifications.send(request) | request.applicationCode |
templates.list(filters, pageable, applicationCode) | 3.º argumento |
attachments.upload(file, filename, applicationCode) | 3.º argumento |
notifications.list / get_ / resend, templates.get_ / publish / archive | último argumento |
Enviar com template
EMAIL precisa de SMTP configurado
Para enviar por EMAIL, o provider SMTP tem de estar configurado no Notification Service (NOTIFICATION_PROVIDERS_EMAILSMTP_*: servidor, porta, credenciais e remetente). Sem ele, o envio é recusado com 422 CHANNEL_NOT_ENABLED e nenhum email sai — ver Configurar os canais › EMAIL (SMTP). O canal também tem de estar ativo nas Definições da aplicação.
'use server';
import { Channel, ChannelStrategy } from '@igrp/platform-notification-client-ts';
import { getNotificationM2MClient } from '@/lib/notification-client';
export async function enviarConfirmacao(input: { email: string; nome: string; referencia: string }) {
const client = getNotificationM2MClient();
const { data } = await client.notifications.send({
applicationCode: process.env.IGRP_APP_CODE!,
channelStrategy: ChannelStrategy.SPECIFIC,
channels: [Channel.EMAIL],
recipients: [{ email: input.email }],
template: { code: 'pedido-confirmacao', locale: 'pt-CV' },
variables: { nome: input.nome, numeroPedido: input.referencia },
category: 'pedido-confirmacao',
businessKey: input.referencia,
idempotencyKey: `pedido-confirmacao-${input.referencia}`,
});
return data.id;
}Campos de SendNotificationRequest:
| Campo | Descrição |
|---|---|
channelStrategy | Obrigatório. SPECIFIC (só os channels indicados), ALL ou PREFERRED (preferências do destinatário). |
channels | EMAIL, SMS, WHATSAPP, PUSH, IN_APP, OFFLINE. |
recipients | { userId }, { email }, { phone } ou { pushToken } — endereços explícitos por canal. |
groups | Um de: groupId (grupo guardado), departmentCode, roleCode ou permissionName do Access Management, resolvidos na hora do envio. |
template | { code, locale? } — template publicado. |
inline | { subject?, title?, body? } — conteúdo sem template. Usa template ou inline. |
variables | Valores para as variáveis Handlebars do template ({{nome}}). |
priority | LOW, NORMAL, HIGH, URGENT. |
attachments | [{ filename, contentRef }] — ver Anexos. |
category, businessKey, correlationId | Agrupamento e rastreio (o businessKey aparece na inbox). |
idempotencyKey | Repetir o pedido com a mesma chave não duplica o envio. |
locale, scheduledAt, expiresAt, callbackUrl | Opcionais. |
Template ou conteúdo inline
Não há endpoint para obter um template pelo código. Para saber se está publicado, lista com filtro:
import { Channel, TemplateStatus } from '@igrp/platform-notification-client-ts';
const { data } = await client.templates.list(
{ code: 'pedido-confirmacao', status: TemplateStatus.PUBLISHED, locale: 'pt-CV', channel: Channel.EMAIL },
{ page: 0, size: 5 },
process.env.IGRP_APP_CODE,
);
const publicado = data.content?.[0];Um padrão útil é enviar com o template e, se ele não existir ou falhar a renderização, repetir com conteúdo inline:
import { ApiClientError, IgrpNotificationErrorCode } from '@igrp/platform-notification-client-ts';
try {
await client.notifications.send({ ...base, template: { code: 'pedido-confirmacao', locale: 'pt-CV' }, variables });
} catch (error) {
const semTemplate =
error instanceof ApiClientError &&
(error.is(IgrpNotificationErrorCode.TEMPLATE_NOT_FOUND) || error.is(IgrpNotificationErrorCode.TEMPLATE_ERROR));
if (!semTemplate) throw error;
await client.notifications.send({
...base,
inline: { subject: `Pedido ${referencia}`, title: 'Pedido recebido', body: `Olá ${nome}, recebemos o seu pedido.` },
});
}Anexos
O anexo é enviado primeiro e o envio refere-o pelo contentRef:
const pdf = new Blob([bytes], { type: 'application/pdf' }); // bytes: ArrayBuffer | Uint8Array
const { data: anexo } = await client.attachments.upload(pdf, 'recibo.pdf', process.env.IGRP_APP_CODE);
await client.notifications.send({
...base,
attachments: [{ filename: 'recibo.pdf', contentRef: anexo.contentRef }],
});upload recebe um Blob (ou File). Erros possíveis: ATTACHMENT_TOO_LARGE, ATTACHMENT_INVALID.
Inbox (IN_APP)
Com o cliente de utilizador:
| Método | Endpoint |
|---|---|
inbox.list({ unreadOnly?, includeArchived? }, { page, size }) | GET /api/v1/inbox |
inbox.unreadCount() | GET /api/v1/inbox/unread-count |
inbox.markRead(deliveryId) | POST /api/v1/inbox/{deliveryId}/read |
inbox.markAllRead() | POST /api/v1/inbox/read |
inbox.archive(deliveryId) | POST /api/v1/inbox/{deliveryId}/archive |
Cada item (InboxItemResponse) tem deliveryId, subject, title, body, category, businessKey, status, read, archived e createdAt. O body pode trazer HTML (templates de email): limpa-o antes de o mostrar como texto.
Sino da header
A header do framework mostra o que getHeaderData() devolver em notifications (IGRPNotificationArgs[], de @igrp/framework-next-types). Converte cada item da inbox:
// src/lib/map-inbox-notification.ts
import type { IGRPNotificationArgs } from '@igrp/framework-next-types';
import type { InboxItemResponse } from '@igrp/platform-notification-client-ts';
const texto = (html?: string) => (html ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
export function mapInboxItem(item: InboxItemResponse): IGRPNotificationArgs {
return {
id: item.deliveryId ?? '',
title: texto(item.subject || item.title) || 'Notificação',
message: texto(item.body) || item.businessKey || '',
type: 'info',
timestamp: item.createdAt ?? new Date().toISOString(),
isRead: Boolean(item.read),
};
}A ligação ao getHeaderData() está em Envio — SDKs e API.
Erros
Os erros HTTP chegam como ApiClientError:
| Campo | Conteúdo |
|---|---|
status | Código HTTP |
errorCode | Código do serviço (IgrpNotificationErrorCode), por exemplo TEMPLATE_NOT_FOUND, APPLICATION_CODE_REQUIRED, CHANNEL_NOT_ENABLED, QUOTA_EXCEEDED, VALIDATION_ERROR |
serverMessage | Mensagem do serviço (envelope { code, message }) |
details | Corpo da resposta |
is(code) | true se errorCode for esse código |
Server Actions perdem os detalhes
Uma Server Action só passa message ao browser: errorCode, serverMessage e details perdem-se. Para mostrar o motivo num toast, apanha o ApiClientError no servidor e lança um Error cuja mensagem já inclua serverMessage e errorCode.
Variáveis de ambiente
| Variável | Uso |
|---|---|
NOTIFICATION_SERVICE_BASE_URL | Base do Notification Service, sem /api/v1 (no workspace do Studio: http://<slug>:<porta>/gateway-api/igrp-notification-service) |
IGRP_APP_CODE | Código da aplicação → applicationCode |
IGRP_M2M_CLIENT_ID, IGRP_M2M_CLIENT_SECRET | Cliente OAuth2 M2M — só servidor |
IGRP_ACCESS_MANAGEMENT_API (ou IGRP_AUTH_ISSUER) | authBaseUrl do token M2M |
IGRP_SERVICE_ID | Opcional — header X-Machine-Service-ID |
NEXT_PUBLIC_IGRP_NOTIFICATION_URL | Página "ver todas" do sino (notificationsUrl) |
@igrp/notification-template-editor
Editor visual React para compor templates de email por linhas e colunas. É o editor usado na Notifications UI. Precisa de React 19 e do Design System (@igrp/igrp-framework-react-design-system ≥ 0.2.0-beta.3).
pnpm add @igrp/notification-template-editorEstilos, no CSS global (Tailwind v4):
@source "../../node_modules/@igrp/notification-template-editor/dist/**/*.{js,jsx,ts,tsx,mjs,cjs}";
@import "@igrp/notification-template-editor/styles";'use client';
import { EmailTemplateRowEditor, type EmailTemplate } from '@igrp/notification-template-editor';
export function EditorTemplate({ onSave }: { onSave: (html: string) => void }) {
return (
<EmailTemplateRowEditor
tagSyntax="handlebars"
layout="fill"
embedded
persistLocal={false}
onSave={(template: EmailTemplate, html: string) => onSave(html)}
/>
);
}| Prop | Descrição |
|---|---|
tagSyntax | "handlebars" ({{nome}}) ou "go" (por omissão). O Notification Service renderiza Handlebars: usa "handlebars". |
initialTemplate, initialDynamicTags | Estado inicial (EmailTemplate, DynamicTag[]). |
onChange, onSave | Recebem (template, html): o HTML gerado vai para o corpo do template no serviço. |
onDynamicTagsChange | Aviso quando as variáveis disponíveis mudam. |
embedded, layout | Embutir numa página (layout="fill" ocupa a altura do contentor). |
persistLocal | Guardar o rascunho no localStorage do browser. |
insertTokenRef | Ref para o host inserir uma variável no bloco selecionado. |
Também exporta generateEmailHtml(template) (gera o HTML fora do editor) e os dados iniciais INITIAL_TEMPLATE, DEFAULT_DYNAMIC_TAGS e SAMPLE_PRESETS.
Última atualização:
