Um passo é satisfeito, nunca marcado
Três tipos, e nenhum deles aceita escrita. O que cada um é, e o que cada um garante, está mais abaixo — esta seção é sobre a regra que os três compartilham. Não existe caminho de escrita contra o progresso, e essa ausência é o desenho. Uma marcação que o cliente de alguém pode enviar é uma checklist que diz “você conectou seu domínio” para quem não conectou, porque uma requisição foi repetida ou um botão foi clicado por otimismo. Um passo satisfeito pelo fato que o satisfaz é um passo que não pode estar errado. Vale para o clique também. Um passo pode carregar um destino e alguém pode apertá-lo — e apertar não satisfaz nada. O link é a porta, não a prova de que alguém atravessou. O corolário: o que você quer que apareça numa checklist precisa ser um fato antes. Um passo nomeia um fato sobre uma PESSOA — um contato ou um vínculo, ou um evento seu. Um fato sobre um time ou uma assinatura teria que ser espalhado pelo quadro inteiro para ser atribuído, e “todo mundo completou configurar cobrança porque uma pessoa completou” não é como um funil é lido.Ela conta a partir do momento em que é ligada
Um fato publicado antes de a checklist existir não conta. O barramento não é um event store, então não há o que reproduzir — e uma regra que preenchesse alguns passos retroativamente e outros não produziria um funil cujos números não podem ser comparados entre si. Na prática: crie a checklist, ligue, e leia o funil contra a coorte que chegou depois. Uma checklist ligada hoje não conta o que os usuários do ano passado fizeram.active é um interruptor reversível e não um arquivamento, porque nada fora desta
API nomeia uma checklist por id.
Renderizando
position aceita inline (padrão), center, bottom-right, bottom-center ou
bottom-left — as
mesmas cinco que a pesquisa e o
quadro de sugestões aceitam.
Dois painéis flutuantes nunca dividem um canto. Um guia e um <HelpWidget /> no
mesmo canto se cobriam, e o único conselho era pôr um à esquerda — uma decisão de
layout tomada no seu lugar para contornar um bug nosso. Agora o canto comporta um
painel: abrir a ajuda pede ao guia que saia de lado, e fechar a ajuda o devolve.
O guia sai de lado saindo mesmo, e o que torna isso seguro é a volta: um guia que
sumisse quando alguém abre a ajuda leria como a ajuda ter fechado o onboarding da
pessoa, enquanto um que volta no instante em que a ajuda fecha lê como o canto sendo
compartilhado. Recolher era a alternativa óbvia e não resolve — um card no mesmo canto
continua no canto.
A ajuda não volta no sentido contrário: alguém a abriu, algo aberto depois tomou o
canto, e uma janela de suporte que reaparecesse sozinha seria uma janela que a pessoa
fechou.
Flutuando, o cabeçalho carrega dois gestos, e eles são de fato diferentes.
Recolher encolhe o guia para um card — o título, a barra de progresso e a única
coisa a fazer em seguida. Não é um widget menor no canto; é o mesmo widget dizendo
menos, e ele volta a abrir pelo próprio controle.
Fechar tira da tela e não deixa nada flutuando. Um painel que responde
“fechar” com um painel menor no mesmo canto não fechou. O caminho de volta é um
controle que você posiciona:
<OnboardingLauncher /> desenha o mesmo progresso do guia e o abre. Ele não desenha
nada até um <OnboardingChecklist /> na página dizer que há o que abrir, então nunca
vira um botão que não abre nada — e nada enquanto o guia flutuante já está aberto,
porque um painel sobre a página e um pill no header dizendo a mesma coisa é um a mais.
Passe whileOpen para mantê-lo nos dois estados, numa barra que reflowaria de forma
visível a cada aparição.
Um guia inline nunca o esconde: inline vive no fluxo da página e pode estar três
telas abaixo, então um pill que sumisse por causa dele não deixaria caminho de volta.
Do seu código, onboardingGuide.show() faz o mesmo.
O que dá para tirar do pill
count é para um header já denso, ou um onde o pill fica ao lado de outros números
e um segundo compete com eles. O número continua indo para o leitor de tela — quem
não vê o anel não fica sem a informação.
border={false} é para uma barra que já tem a própria moldura, onde mais um
contorno é uma caixa em volta de algo que ninguém pediu para separar. O padding
lateral sai junto, porque ele existia para afastar o texto de uma borda que não é
mais desenhada.
label é para quando o dicionário viraria dois dicionários: o mesmo produto
chamando de “Configuração” num header e “Primeiros passos” numa página de ajustes.
Se for o nome em toda parte, o lugar é localization no provider — ele alcança
todas as superfícies de uma vez.
Os gestos, e até onde eles vão
Minimizar e fechar são lembrados pela visita. Os dois ficam emsessionStorage, com a chave do contato: um F5 não desfaz — é a mesma visita — e
fechar a aba desfaz, porque quem volta na semana seguinte merece o guia e não um
card que minimizou e esqueceu. A chave leva o contato porque sessionStorage é por
origem e não por pessoa; sem isso, num computador compartilhado o gesto de um vira
o estado do outro.
São um gesto só em duas profundidades, e por um tempo só o menor sobreviveu ao F5 —
que é a metade mais estranha de explicar: um reload devolvia por cima da página o
card que a pessoa acabara de tirar dali. Reabrir pelo seu próprio botão esquece o
fechar na hora; a decisão mais recente é a que vale.
Ceder o canto não conta como fechar. Quando o painel de ajuda ocupa o mesmo
canto, o guia sai de cena e volta quando a ajuda fecha — um recuo nosso, não uma
resposta de quem está lendo, e um F5 no meio disso não custa o guia pelo resto da
visita.
Nenhum dos dois conta nada para a API. O fechar que persiste de verdade é
outro, e está logo abaixo.
O título do card é o próprio toggle — é onde a mão vai antes do chevron.
A linha do card faz o passo, não abre o guia. O card existe para nomear a única
coisa que falta; uma linha que nomeia e depois responde com uma lista está pedindo
que a pessoa procure ali o que acabou de ler. Com destino ela é um link de verdade
(clique do meio, copiar endereço); sem destino e sem onStepAction, aí sim ela
abre o guia, que é o que sobra a oferecer.
O fechar que persiste mora no menu de objetivos, atrás do Editar — “fechar tudo
por um tempo”. Ele dispensa cada lista do guia, cada uma pelo próprio snooze_days, e
elas voltam sozinhas. Está ali e não no cabeçalho porque é um tipo de ato diferente
dos dois ao lado, e porque aquele menu já é a tela sobre o que a pessoa quer no guia
dela. Do seu código é o userkit.dismissChecklist(id), uma lista por vez.
O que satisfaz um passo
Três tipos, e a diferença entre os dois de evento é quem publica.Da pessoa ou do time
Todo passo tem umscope, e o padrão é contact: uma pessoa, um passo. É o que
todo passo significava antes deste campo existir.
scope: "customer" é para o trabalho que é da conta, não de ninguém em
particular — conectar a plataforma de pagamento, importar um catálogo, configurar
um domínio. Ele fica satisfeito para todo mundo do time assim que qualquer
membro faz:
entitlement, um tipo ao lado. É por isso que o argumento contra
espalhar um fato por um quadro inteiro continua de pé: nada é espalhado.
Duas consequências que valem saber:
- O funil conta TIMES num passo de escopo de time, e pessoas nos outros. O
scopevolta junto do número na leitura de conclusão, porque 40 e 40 em unidades diferentes é uma tabela onde duas respostas parecem concordar. - A lista inteira passa a ter
completed: null, como já acontece com um passoentitlement— e pela mesma razão. Quem escreve a marca de “terminou a lista” é um consumidor, que não tem requisição e portanto não tem time; materializar uma significaria escolher um time pela pessoa e estar errado em todos os outros de que ela participa. O passo continua com o número dele; a lista responde “não dá para dizer” em vez de contradizer o widget. - Sem time resolvido, cai para a própria pessoa. Um contato sem time não é um contato cujo time não fez nada — não há time. É também o estado de todo mundo antes de espelhar as contas.
scope é congelado depois de criado, como kind: virá-lo com gente segurando
progresso mudaria o que as linhas delas significam. Num passo entitlement ele é
recusado — esse tipo já é uma pergunta sobre o time por construção.
O passo product_event é como se expressa aquilo de que um onboarding
realmente é feito — “criou o primeiro projeto”, “publicou algo”, “convidou
alguém pelo seu próprio fluxo”. Você manda o evento que já manda:
A garantia dele é mais fraca que a dos outros dois, e vale saber qual. O
analytics descarta eventos quando o buffer enche, sob carga; o nosso barramento
não. Depois que o evento é gravado, o resto do caminho é do barramento e a
garantia é a mesma de todo o resto — mas o começo desse caminho pode perder um, e
um evento perdido é um passo que fica cinza.
POST /v1/track
com a chave secreta do ambiente, e cada linha nomeia o contato a que
pertence:
POST /v1/events — a porta que
o track() do browser usa — não consegue nomear ninguém, e um evento sem
contato não satisfaz passo de ninguém: se o seu passo depende de um fato do
servidor, é esta a porta.
Para onde um passo leva
Um guia que nomeia o trabalho e esconde a porta faz o leitor navegar duas vezes. Então um passo carrega o próprio destino:action_url aceita um caminho do seu próprio app (/settings/domain) ou
uma URL http(s) absoluta — o mesmo par que uma
notificação e um banner aceitam. A forma
relativa a protocolo é recusada: //evil.example/x começa com barra, passa por
caminho em qualquer revisão, e é uma URL absoluta para o host de outra pessoa.
O widget desenha esse passo como um link de verdade, não como um botão que
navega — clique do meio, copiar endereço e abrir em outra aba são a razão de um
link ser um link. A linha inteira é o alvo e uma seta diz isso; uma URL absoluta
abre em nova aba, um caminho fica no app.
O action_label não é desenhado na linha. Uma palavra à direita de toda
linha clicável é uma coluna de verbos competindo com os títulos, e a seta já diz
que a linha abre. O que ele ainda faz é nomear a ação para um leitor de tela, e
declarar que um passo sem destino tem uma — aquela que o seu próprio código
trata.
Um passo concluído continua com a porta aberta. Feito não é sumido: quem
conectou o domínio semana passada ainda quer aquela tela.
A frase que não cabe na linha
Um passo é uma linha, e isso é o ponto — um guia que se lê de relance é um guia que as pessoas leem. Mas alguns passos precisam de uma segunda frase: por que este está travado, o que a coisa é de fato, o que ela comprou. Essa frase vai no hover, depois de um segundo:hint_pending é o caso comum e lê
certo: nada no hover depois de feito, que é o tanto honesto a dizer sobre uma coisa
que a pessoa já fez.
Uma checklist aceita um terceiro, o hint_locked, porque um título tem um estado
que uma linha não tem. O widget já desenha um cadeado e o nome do que o grupo espera,
e um nome é uma porta, não um motivo:
Um passo tem só dois, e a ausência é deliberada: um passo não tem estado travado.
O que parece passo travado em outros produtos é passo dentro de um grupo travado, e um
grupo travado é desenhado fechado — então um terceiro campo ali seria configuração que
nunca consegue aparecer. O caso mais próximo é um passo de entitlement de um recurso
que o time não tem, e esse é honestamente pendente: “disponível no Pro” é
hint_pending.
No máximo 200 caracteres. Mais que isso é um parágrafo no hover, que some no instante
em que a pessoa move o mouse para ler.
Ele fica verde enquanto a pessoa está olhando
Um passo espera um fato chegar no nosso barramento, o que leva alguns segundos depois dotrack(). O widget não faz polling — um poll correria com o dreno em
vez de ler um número vivo — então, sem ajuda, o check só apareceria no próximo
carregamento, muito depois do momento em que ele significava alguma coisa.
Por isso o track() também avisa localmente:
completed continua sendo a resposta
do servidor, então nenhuma lista parabeniza ninguém, se esconde, nem grava
conclusão em cima de um palpite.
Para o app que termina algo sem chamar track():
Quando a ação não é uma navegação
Metade do que um passo quer fazer no clique é abrir a sua própria modal, começar um tour, ou focar um campo na página em que a pessoa já está. Nada disso tem endereço, então não vive no banco — o seu código é que vive:true cancela a navegação. É isso
que permite um único handler interceptar os dois passos que o seu app conhece e
deixar o resto navegar.
Um passo cuja ação é só uma função tem action_label e nenhum action_url — ele
declara que existe uma porta e que a sua página é quem abre. Numa página que
nunca ligou o onStepAction, esse passo desenha um controle que não faz nada, e
é por isso que o painel avisa ao lado do campo.
Clicar nunca satisfaz um passo, em nenhuma das duas formas. Se o clique é o
que você está medindo, a composição é um passo
product_event cujo track()
está no mesmo handler:Várias listas, um guia
Cada checklist em que a pessoa está é desenhada como um grupo de um guia só: o primeiro não concluído e desbloqueado fica aberto, os outros dobrados. Um guia que abrisse tudo seria uma parede de vinte passos, e a pergunta de quem chega é “e agora”, não “o que existe”. Ordene comunlocks_after. Uma lista que espera outra é desenhada fechada, com um
cadeado e o nome do que ela espera — ela continua NO guia em vez de sumir, porque ver o
que vem a seguir é a diferença entre um guia e uma fila. Só no mesmo ambiente, e um
ciclo é 400: duas listas esperando uma pela outra é um par que nunca abriria.
Deixe a pessoa escolher com selectable. Uma lista selecionável fica fora do guia
até ela colocá-la lá, pelo modal “do que você precisa para começar?” atrás do
Editar do guia. O público continua decidindo o que é oferecido; isto entrega o
último passo a ela. O menu viaja junto com o guia numa requisição só, então o modal
abre com as escolhas dela já marcadas.
Salvar envia a resposta inteira, nunca um diff — senão “desmarquei isto” e “isto
foi lançado enquanto meu modal estava aberto” seriam a mesma requisição, e a segunda
desfaria o seu próprio lançamento sem avisar.
Nada disso mexe no que satisfaz um passo. Escolher decide quais listas estão no guia;
um passo continua sendo satisfeito por um fato que chega no bus ou por um direito do
plano, e nenhuma tela em lugar nenhum marca um.
Inline é o lugar de um dashboard, num espaço que você abriu para ele. As posições
flutuantes são para o produto que não tem esse espaço: um cartão no canto que a pessoa
olha entre uma tarefa e outra. Essas trazem um fechar, que o inline não precisa — um
painel sobre a página do qual não se consegue livrar é um painel que as pessoas
aprendem a contornar.
Fechar é por checklist, via POST /v1/contact/checklist/{id}/dismiss, que o
<OnboardingChecklist /> chama por você. Um ambiente tem várias listas ativas, então
uma marca na pessoa faria a que você lançar no mês que vem nascer escondida para todo
mundo que fechou a anterior — dispensar uma não diz nada sobre as outras, nem sobre as
que ainda não existem.
E é um adiamento, não uma resposta. O snooze_days da checklist decide quanto
tempo: sete dias por padrão, e depois o cartão volta. Quem aperta o X num guia de setup
quase nunca quer dizer “nunca mais me ajude” — quer dizer “agora não”, e gravar a
leitura mais forte possível do gesto mais fraco possível é como um guia some da conta
de alguém que nunca terminou de configurar nada.
Use snooze_days: 0 para a checklist que é mesmo uma oferta única; aí o fechar vale
para sempre. Entre os dois, períodos maiores são para a lista que a pessoa pode
legitimamente não querer por um tempo — quem nunca vai comprar o recurso de um passo de
entitlement veria o mesmo cartão voltar pelo resto da vida da conta, e uma cobrança com
agenda continua sendo uma cobrança.
Mudar o número nunca mexe num adiamento em curso. O fim é calculado quando a pessoa
fecha o cartão e fica gravado, então uma promessa feita a alguém não é encurtada por
uma edição que essa pessoa nunca viu.
A página dedicada de setup pede as fechadas. Fechar diz “no meu dashboard não”, e
quem navega até /getting-started acabou de dizer o contrário sobre aquela tela:
?include_dismissed=true, e cada checklist volta carregando dismissed,
para uma página que desenha a própria UI poder dizer isso. O botão de fechar sai
junto — um controle que esconde algo que volta no render seguinte é um controle que
não funciona.
Ele passa por cima da dispensa e de mais nada: uma checklist desligada continua
desligada, o público continua decidindo, e uma escolhível ainda precisa ter sido
escolhida. Essas são coisas sobre a checklist; fechar é uma preferência sobre uma tela.
Escolher a lista de novo no menu de objetivos traz ela de volta na hora, sobrando o
que sobrar do adiamento. Aquela tela é onde alguém diz “quero isto no meu guia”, então
uma dispensa que sobrevivesse a ela seria uma recusa que ninguém falou em voz alta.
Não é uma recusa e não mexe no progresso: fechar diz “aqui não”, e os passos continuam
sendo satisfeitos por fatos que chegam no bus. O painel lê dois números ao lado do funil
de conclusão — dismissals, quantas pessoas já fecharam, e hidden, para quantas está
escondida agora. A diferença entre os dois é o adiamento funcionando. Os dois iguais
é a cara de um cartão que as pessoas estão empurrando para longe.
Num app Next, importe do @userkit/nextjs — é o mesmo componente, e em
modo proxy a leitura é encaminhada pelos
handlers, com a sessão num cookie httpOnly da sua própria origem. Só a leitura: não há
metade de escrita em lugar nenhum desta superfície.
200 e uma lista vazia, não um 401. O widget roda
na sua landing page, e um erro de console é a forma errada de dizer “não há nada para
mostrar aqui”.
satisfied_at é sempre null num passo de entitlement: nada foi armazenado, então
não há momento a relatar. completed é derivado dos passos em vez de guardado ao
lado deles, o que é o que o mantém correto também numa lista que carrega um passo de
entitlement.
O id do cliente viaja com a leitura, porque é ele que decide os passos de
entitlement. Ele identifica e nunca autoriza — um time a que a sessão não pertence
não casa com vínculo nenhum.
Quem vê
segment_id mira a checklist numa audiência, ou null
para todo mundo. Ele decide quem vê a lista, não quem é registrado contra ela: o
progresso é escrito para quem produziu o fato, então alguém que entra no segmento
depois chega com os passos já cumpridos já marcados.
O funil
null, não um zero que ele não ganhou. Nada é
armazenado para esses passos, então não há contagem a dar — e 0 seria um número
sobre o qual alguém agiria. materialized diz qual dos dois você está olhando, para
que um gráfico desenhe uma lacuna em vez de uma barra.
completed é null pelo mesmo motivo sempre que a checklist carrega um passo de
entitlement: um número de “terminou a lista inteira” calculado sobre os passos que
ele enxerga seria um piso apresentado como total. Uma tranquilização é exatamente o
que faz alguém parar de conferir.
audience é quem está no segmento, ou todo contato identificado do ambiente quando a
checklist é para todo mundo.
O que ela publica
checklist.step_completed e checklist.completed são entregáveis como
webhooks — o segundo é o que vale assinar, porque “essa
pessoa terminou o onboarding” é o momento de que uma sequência de boas-vindas ou um
alerta de vendas pendura.
checklist.completed dispara no máximo uma vez por pessoa por checklist,
sustentado por uma linha marcadora escrita na mesma transação. O barramento é
at-least-once, então um consumidor que rodasse de novo sem ela anunciaria uma
formatura duas vezes.
Ele dispara apenas para uma checklist feita inteiramente de passos de evento. Um
passo de entitlement é decidido na leitura, contra um time, então o caminho que
anuncia a conclusão — que roda quando um fato chega, e conhece só a pessoa — não
consegue dizer se a lista terminou. Uma checklist da qual você quer pendurar uma
automação deve ser feita só de passos de evento.
Os payloads levam ids, chaves e títulos: o passo que alguém satisfez e a lista a que
ele pertence. Nunca a regra por trás — qual fato ou qual funcionalidade um passo
espera é configuração sua, e quem assina não precisa disso para agir sobre alguém ter
chegado lá. Editar ou excluir uma checklist não é entregue.
Permissões
Toda a superfície de staff éengagement:manage — owner e admin por padrão. Um nome
cobre checklists, changelog e pesquisas: é o mesmo ato, que é operar o que seus
usuários veem dentro do seu produto.