Ir para o conteúdo

Personalize o feed para Content Cards

Um feed de cartão de conteúdo é a sequência de Content Cards nos seus aplicativos móveis ou da web. Este artigo aborda a configuração de quando o feed é atualizado, a ordem dos cartões, o gerenciamento de vários feeds e as mensagens de erro de “feed vazio”. Para a lista completa de tipos de cartões de conteúdo, consulte Sobre Content Cards.

Sobre o ciclo de vida da sessão

Uma sessão refere-se ao período de tempo em que o SDK da Braze rastreia a atividade do usuário em seu app após ser iniciado. Você também pode forçar uma nova sessão chamando o método changeUser().

Por padrão, uma sessão começa quando você chama braze.openSession() pela primeira vez. A sessão permanecerá ativa por até 30 minutos de inatividade (a menos que você altere o tempo limite padrão da sessão) ou o usuário feche o app.

Por padrão, uma sessão começa quando openSession() é chamado pela primeira vez. Se seu app for para o segundo plano e depois retornar ao primeiro plano, o SDK verificará se mais de 10 segundos se passaram desde que a sessão começou (a menos que você altere o tempo limite padrão da sessão). Se sim, uma nova sessão começará. Lembre-se de que, se o usuário fechar seu app enquanto ele estiver em segundo plano, os dados da sessão podem não ser enviados à Braze até que ele reabra o app.

Chamar closeSession() não encerrará imediatamente a sessão. Em vez disso, a sessão será encerrada após 10 segundos se openSession() não for chamado novamente pelo usuário ao iniciar outra atividade.

Por padrão, uma sessão começa quando você chama Braze.init(configuration:). Isso ocorre quando a notificação UIApplicationWillEnterForegroundNotification é acionada, indicando que o app entrou no primeiro plano.

Se seu app for para o segundo plano, UIApplicationDidEnterBackgroundNotification é acionado. O app não permanece em uma sessão ativa enquanto está em segundo plano. Quando seu app retorna ao primeiro plano, o SDK compara o tempo decorrido desde o início da sessão com o tempo limite da sessão (a menos que você altere o tempo limite padrão da sessão). Se o tempo desde o início da sessão exceder o período de tempo limite, uma nova sessão começa.

Atualização do feed

Atualização automática

Por padrão, o feed de Content Cards é atualizado automaticamente quando:

  • Uma nova sessão é iniciada
  • O feed padrão de Content Cards é fechado e reaberto depois de mais de 60 segundos desde a última atualização.

Entrega em tempo real

A Braze também envia atualizações de Content Cards para o dispositivo assim que elas acontecem. Isso ocorre por meio de uma conexão ativa que o SDK mantém durante a sessão. Os usuários não precisam iniciar uma nova sessão nem esperar uma atualização para ver a mudança. A entrega em tempo real funciona tanto para feeds de Content Cards padrão quanto para feeds personalizados.

A entrega em tempo real abrange as seguintes atualizações:

  • Um usuário se torna elegível para uma Campaign de Content Card durante uma sessão.
  • Um usuário avança para uma etapa de Content Card em um Canvas.
  • Um cartão é removido do feed de um usuário.
  • Um cartão é enviado pela API, como pelos endpoints /messages/send, /campaigns/trigger/send ou /canvas/trigger/send.

A entrega em tempo real exige as seguintes versões mínimas do SDK:

Em versões anteriores do SDK, os cartões continuam chegando no início da sessão e durante a atualização.

Atualização manual

Para atualizar o feed manualmente em um momento específico:

Solicite uma atualização manual dos Content Cards da Braze pelo Web SDK a qualquer momento chamando requestContentCardsRefresh().

Você também pode chamar getCachedContentCards para obter todos os cartões disponíveis no momento a partir da última atualização dos Content Cards.

import * as braze from "@braze/web-sdk";

function refresh() {
  braze.requestContentCardsRefresh();
}

Para abrir os links dos Content Cards em uma nova guia do navegador em vez da mesma guia, defina openCardsInNewTab: true nas opções de inicialização do Web SDK. Para saber mais sobre as opções de inicialização, consulte o guia do repositório do Web SDK.

Solicite uma atualização manual dos Content Cards da Braze pelo Android SDK a qualquer momento chamando requestContentCardsRefresh.

Braze.getInstance(context).requestContentCardsRefresh();
Braze.getInstance(context).requestContentCardsRefresh()

Solicite uma atualização manual dos Content Cards da Braze pelo Swift SDK a qualquer momento chamando o método requestRefresh na classe Braze.ContentCards:

No Swift, os Content Cards podem ser atualizados com um manipulador de conclusão opcional ou com um retorno assíncrono usando as APIs nativas de concorrência do Swift.

Manipulador de conclusão

AppDelegate.braze?.contentCards.requestRefresh { result in
  // Implement completion handler
}

Async/Await

let contentCards = await AppDelegate.braze?.contentCards.requestRefresh()
[AppDelegate.braze.contentCards requestRefreshWithCompletion:^(NSArray<BRZContentCardRaw *> * contentCards, NSError * error) {
  // Implement completion handler
}];

Sincronização completa vs. sincronização parcial

O SDK da Braze usa dois tipos de sincronização ao buscar Content Cards no servidor:

  • Sincronização completa: Baixa todos os Content Cards para os quais o usuário é elegível. As sincronizações completas ocorrem automaticamente a cada 7 dias ou sempre que changeUser() é chamado.
  • Sincronização parcial: Baixa apenas os Content Cards novos desde a última solicitação. Se o usuário não for elegível para nenhum cartão novo, a resposta retorna zero cartões. As sincronizações parciais ocorrem sempre que requestContentCardsRefresh() é chamado (a menos que tenham se passado 7 dias desde a última sincronização completa; nesse caso, uma sincronização completa é disparada).

As sincronizações parciais reduzem a carga no servidor e o consumo de bateria do dispositivo. Os Content Cards já recebidos ficam armazenados localmente no SDK. Então, os usuários continuam vendo os cartões disponíveis mesmo quando uma sincronização parcial retorna zero cartões novos.

Limite de frequência

A Braze usa um algoritmo de token bucket para aplicar os seguintes limites de frequência:

  • Até 5 chamadas de atualização por dispositivo, compartilhadas entre usuários e chamadas para openSession()
  • Depois de atingir o limite, uma nova chamada fica disponível a cada 180 segundos (3 minutos)
  • O sistema mantém até cinco chamadas para você usar a qualquer momento
  • subscribeToContentCards() continua retornando os cartões em cache mesmo quando o limite de frequência é atingido

Personalização da ordem de exibição dos cartões

Você pode alterar a ordem em que seus Content Cards são exibidos. Assim, dá para ajustar a experiência do usuário priorizando certos tipos de conteúdo, como promoções com prazo limitado.

Personalize a ordem de exibição dos Content Cards no seu feed usando o parâmetro filterFunction de showContentCards():. Por exemplo:

braze.showContentCards(null, (cards) => {
  return sortBrazeCards(cards); // Where sortBrazeCards is your sorting function that returns the sorted card array
});

O ContentCardsFragment depende de um IContentCardsUpdateHandler para lidar com qualquer classificação ou modificação dos Content Cards antes que eles sejam exibidos no feed. Você pode definir um manipulador de atualização personalizado com setContentCardUpdateHandler no seu ContentCardsFragment.

Este é o IContentCardsUpdateHandler padrão, que pode servir como ponto de partida para a personalização:

Mostrar exemplo em Java
public class DefaultContentCardsUpdateHandler implements IContentCardsUpdateHandler {

  // Interface that must be implemented and provided as a public CREATOR
  // field that generates instances of your Parcelable class from a Parcel.
  public static final Parcelable.Creator<DefaultContentCardsUpdateHandler> CREATOR = new Parcelable.Creator<DefaultContentCardsUpdateHandler>() {
    public DefaultContentCardsUpdateHandler createFromParcel(Parcel in) {
      return new DefaultContentCardsUpdateHandler();
    }

    public DefaultContentCardsUpdateHandler[] newArray(int size) {
      return new DefaultContentCardsUpdateHandler[size];
    }
  };

  @Override
  public List<Card> handleCardUpdate(ContentCardsUpdatedEvent event) {
    List<Card> sortedCards = event.getAllCards();
    // Sort by pinned, then by the 'updated' timestamp descending
    // Pinned before non-pinned
    Collections.sort(sortedCards, new Comparator<Card>() {
      @Override
      public int compare(Card cardA, Card cardB) {
        // A displays above B
        if (cardA.getIsPinned() && !cardB.getIsPinned()) {
          return -1;
        }

        // B displays above A
        if (!cardA.getIsPinned() && cardB.getIsPinned()) {
          return 1;
        }

        // At this point, both A & B are pinned or both A & B are non-pinned
        // A displays above B since A is newer
        if (cardA.getUpdated() > cardB.getUpdated()) {
          return -1;
        }

        // B displays above A since A is newer
        if (cardA.getUpdated() < cardB.getUpdated()) {
          return 1;
        }

        // At this point, every sortable field matches so keep the natural ordering
        return 0;
      }
    });

    return sortedCards;
  }

  // Parcelable interface method
  @Override
  public int describeContents() {
    return 0;
  }

  // Parcelable interface method
  @Override
  public void writeToParcel(Parcel dest, int flags) {
    // No state is kept in this class so the parcel is left unmodified
  }
}
Mostrar exemplo em Kotlin
class DefaultContentCardsUpdateHandler : IContentCardsUpdateHandler {
  override fun handleCardUpdate(event: ContentCardsUpdatedEvent): List<Card> {
    val sortedCards = event.allCards
    // Sort by pinned, then by the 'updated' timestamp descending
    // Pinned before non-pinned
    sortedCards.sortWith(Comparator sort@{ cardA: Card, cardB: Card ->
      // A displays above B
      if (cardA.isPinned && !cardB.isPinned) {
        return@sort -1
      }

      // B displays above A
      if (!cardA.isPinned && cardB.isPinned) {
        return@sort 1
      }

      // At this point, both A & B are pinned or both A & B are non-pinned
      // A displays above B since A is newer
      if (cardA.updated > cardB.updated) {
        return@sort -1
      }

      // B displays above A since A is newer
      if (cardA.updated < cardB.updated) {
        return@sort 1
      }
      0
    })
    return sortedCards
  }

  // Parcelable interface method
  override fun describeContents(): Int {
    return 0
  }

  // Parcelable interface method
  override fun writeToParcel(dest: Parcel, flags: Int) {
    // No state is kept in this class so the parcel is left unmodified
  }

  companion object {
    // Interface that must be implemented and provided as a public CREATOR
    // field that generates instances of your Parcelable class from a Parcel.
    val CREATOR: Parcelable.Creator<DefaultContentCardsUpdateHandler?> = object : Parcelable.Creator<DefaultContentCardsUpdateHandler?> {
      override fun createFromParcel(`in`: Parcel): DefaultContentCardsUpdateHandler? {
        return DefaultContentCardsUpdateHandler()
      }

      override fun newArray(size: Int): Array<DefaultContentCardsUpdateHandler?> {
        return arrayOfNulls(size)
      }
    }
  }
}

Para filtrar e classificar Content Cards no Jetpack Compose, defina o parâmetro cardUpdateHandler. Por exemplo:

ContentCardsList(
    cardUpdateHandler = {
        it.sortedWith { cardA, cardB ->
            // A displays above B
            if (cardA.isPinned && !cardB.isPinned) {
                return@sortedWith -1
            }
            // B displays above A
            if (!cardA.isPinned && cardB.isPinned) {
                return@sortedWith 1
            }
            // At this point, both A & B are pinned or both A & B are non-pinned
            // A displays above B since A is newer
            if (cardA.updated > cardB.updated) {
                return@sortedWith -1
            }
            // B displays above A since A is newer
            if (cardA.updated < cardB.updated) {
                return@sortedWith 1
            }
            0
        }
    }
)

Personalize a ordem do feed de cartões modificando diretamente a variável estática Attributes.defaults.

var attributes = BrazeContentCardUI.ViewController.Attributes.defaults
attributes.transform = { cards in
    cards.sorted {
        if $0.pinned && !$1.pinned {
            return true
        } else if !$0.pinned && $1.pinned {
            return false
        } else {
            return $0.createdAt > $1.createdAt
        }
    }
}
let viewController = BrazeContentCardUI.ViewController(braze: AppDelegate.braze, attributes: attributes)

A personalização com BrazeContentCardUI.ViewController.Attributes não está disponível em Objective-C.

Personalizando a mensagem de “feed vazio”

Quando um usuário não se qualifica para nenhum Content Card, o SDK exibe uma mensagem de erro de “feed vazio” dizendo: “We have no updates. Please check again later.” Você pode personalizar essa mensagem de erro de “feed vazio” de forma semelhante à seguinte:

Uma mensagem de erro de feed vazio que diz "This is a custom empty state message."

O Web SDK não permite substituir o texto de “feed vazio” de forma programática. Você pode optar por substituí-lo sempre que o feed for exibido, mas isso não é recomendado. O feed pode levar algum tempo para atualizar, e o texto de feed vazio não aparecerá imediatamente.

Se o ContentCardsFragment determinar que o usuário não se qualifica para nenhum Content Card, ele exibirá a mensagem de erro de feed vazio.

Um adaptador especial, o EmptyContentCardsAdapter, substitui o ContentCardAdapter padrão para exibir essa mensagem de erro. Para definir a mensagem personalizada, substitua o recurso de string com_braze_feed_empty.

O estilo usado para exibir essa mensagem está disponível em Braze.ContentCardsDisplay.Empty e é reproduzido no snippet de código a seguir:

<style name="Braze.ContentCardsDisplay.Empty">
  <item name="android:lineSpacingExtra">1.5dp</item>
  <item name="android:text">@string/com_braze_feed_empty</item>
  <item name="android:textColor">@color/com_braze_content_card_empty_text_color</item>
  <item name="android:textSize">18.0sp</item>
  <item name="android:gravity">center</item>
  <item name="android:layout_height">match_parent</item>
  <item name="android:layout_width">match_parent</item>
</style>

Para saber mais sobre como personalizar os elementos de estilo dos Content Cards, consulte Personalizando o estilo.

Para personalizar a mensagem de erro de “feed vazio” com o Jetpack Compose, você pode passar uma emptyString para ContentCardsList. Você também pode passar emptyTextStyle para ContentCardListStyling para personalizar ainda mais essa mensagem.

ContentCardsList(
    emptyString = "No messages today",
    style = ContentCardListStyling(
        emptyTextStyle = TextStyle(...)
    )
)

Se preferir exibir um Composable, você pode passar emptyComposable para ContentCardsList. Se emptyComposable for especificado, a emptyString não será usada.

ContentCardsList(
    emptyComposable = {
        Image(
            painter = painterResource(id = R.drawable.noMessages),
            contentDescription = "No messages"
        )
    }
)

Personalize o estado vazio do view controller definindo os Attributes relacionados.

var attributes = BrazeContentCardUI.ViewController.Attributes.defaults
attributes.emptyStateMessage = "This is a custom empty state message"
attributes.emptyStateMessageFont = .preferredFont(forTextStyle: .title1)
attributes.emptyStateMessageColor = .secondaryLabel

Altere o texto que aparece automaticamente em feeds de Content Cards vazios redefinindo as strings localizáveis dos Content Cards no arquivo ContentCardsLocalizable.strings do seu app.

Implementação de vários feeds

Você pode filtrar os Content Cards no seu app para exibir apenas cartões específicos. Assim, é possível ter vários feeds de Content Cards para diferentes casos de uso. Por exemplo, você pode manter um feed transacional e um feed de marketing. Para isso, crie diferentes categorias de Content Cards definindo pares de valores-chave no dashboard da Braze. Em seguida, crie feeds no seu app ou site que tratem esses tipos de Content Cards de forma diferente, filtrando alguns tipos e exibindo outros.

Etapa 1: Definir pares de valores-chave nos cartões

Ao criar uma Campaign de Content Cards, defina dados de pares de valores-chave em cada cartão. Você usará esse par de valores-chave para categorizar os cartões. Os pares de valores-chave ficam armazenados na propriedade extras do modelo de dados do cartão.

Neste exemplo, vamos definir um par de valores-chave com a chave feed_type, que indica em qual feed de Content Cards o cartão deve ser exibido. O valor corresponde aos seus feeds personalizados, como home_screen ou marketing.

Etapa 2: Filtrar Content Cards

Depois de atribuir os pares de valores-chave, crie um feed com uma lógica que exiba os cartões desejados e filtre os cartões de outros tipos. Neste exemplo, vamos exibir apenas os cartões com o par de valores-chave correspondente feed_type: "Transactional".

O exemplo a seguir mostra o feed de Content Cards para cartões do tipo Transactional:

/**
 * @param {String} feed_type - value of the "feed_type" KVP to filter
 */
function showCardsByFeedType(feed_type) {
  braze.showContentCards(null, function(cards) {
    return cards.filter((card) => card.extras["feed_type"] === feed_type);
  });
}

Em seguida, você pode configurar um botão de alternância para o seu feed personalizado:

// show the "Transactional" feed when this button is clicked
document.getElementById("show-transactional-feed").onclick = function() {
  showCardsByFeedType("Transactional");
};

Para saber mais, consulte a documentação do método do SDK.

Por padrão, o feed de Content Cards é exibido em um ContentCardsFragment, e o IContentCardsUpdateHandler retorna uma lista de cartões a serem exibidos após receber um ContentCardsUpdatedEvent do SDK da Braze. No entanto, ele apenas classifica os cartões e não faz nenhuma filtragem diretamente.

Etapa 2.1: Criar um handler personalizado

Você pode filtrar Content Cards implementando um IContentCardsUpdateHandler personalizado com os pares de valores-chave definidos no dashboard e obtidos por Card.getExtras(). Depois, modifique-o para remover da lista todos os cartões que não correspondam ao valor de feed_type definido anteriormente.

Show Java example
private IContentCardsUpdateHandler getUpdateHandlerForFeedType(final String desiredFeedType) {
  return new IContentCardsUpdateHandler() {
    @Override
    public List<Card> handleCardUpdate(ContentCardsUpdatedEvent event) {
      // Use the default card update handler for a first
      // pass at sorting the cards. This is not required
      // but is done for convenience.
      final List<Card> cards = new DefaultContentCardsUpdateHandler().handleCardUpdate(event);

      final Iterator<Card> cardIterator = cards.iterator();
      while (cardIterator.hasNext()) {
        final Card card = cardIterator.next();

        // Make sure the card has our custom KVP
        // from the dashboard with the key "feed_type"
        if (card.getExtras().containsKey("feed_type")) {
          final String feedType = card.getExtras().get("feed_type");
          if (!desiredFeedType.equals(feedType)) {
            // The card has a feed type, but it doesn't match
            // our desired feed type, remove it.
            cardIterator.remove();
          }
        } else {
          // The card doesn't have a feed
          // type at all, remove it
          cardIterator.remove();
        }
      }

      // At this point, all of the cards in this list have
      // a feed type that explicitly matches the value we put
      // in the dashboard.
      return cards;
    }
  };
}
Show Kotlin example
private fun getUpdateHandlerForFeedType(desiredFeedType: String): IContentCardsUpdateHandler {
  return IContentCardsUpdateHandler { event ->
    // Use the default card update handler for a first
    // pass at sorting the cards. This is not required
    // but is done for convenience.
    val cards = DefaultContentCardsUpdateHandler().handleCardUpdate(event)

    val cardIterator = cards.iterator()
    while (cardIterator.hasNext()) {
      val card = cardIterator.next()

      // Make sure the card has our custom KVP
      // from the dashboard with the key "feed_type"
      if (card.extras.containsKey("feed_type")) {
        val feedType = card.extras["feed_type"]
        if (desiredFeedType != feedType) {
          // The card has a feed type, but it doesn't match
          // our desired feed type, remove it.
          cardIterator.remove()
        }
      } else {
        // The card doesn't have a feed
        // type at all, remove it
        cardIterator.remove()
      }
    }

    // At this point, all of the cards in this list have
    // a feed type that explicitly matches the value we put
    // in the dashboard.
    cards
  }
}

Etapa 2.2: Adicionar a um fragmento

Depois de criar um IContentCardsUpdateHandler, crie um ContentCardsFragment que o utilize. Esse feed personalizado pode ser usado como qualquer outro ContentCardsFragment. Nas diferentes partes do seu app, exiba feeds de Content Cards diferentes com base na chave fornecida no dashboard. Cada feed ContentCardsFragment exibirá um conjunto exclusivo de cartões graças ao IContentCardsUpdateHandler personalizado em cada fragmento.

Show Java example
// We want a Content Cards feed that only shows "Transactional" cards.
ContentCardsFragment customContentCardsFragment = new ContentCardsFragment();
customContentCardsFragment.setContentCardUpdateHandler(getUpdateHandlerForFeedType("Transactional"));
Show Kotlin example
// We want a Content Cards feed that only shows "Transactional" cards.
val customContentCardsFragment = ContentCardsFragment()
customContentCardsFragment.contentCardUpdateHandler = getUpdateHandlerForFeedType("Transactional")

Para filtrar quais Content Cards são exibidos neste feed, use cardUpdateHandler. Por exemplo:

ContentCardsList(
     cardUpdateHandler = {
         it.filter { card ->
             card.extras["feed_type"] == "Transactional"
         }
     }
 )

The following example will show the Content Cards feed for Transactional type cards:

// Filter cards by the `Transactional` feed type based on your key-value pair.
let transactionalCards = cards.filter { $0.extras["feed_type"] as? String == "Transactional" }

Para ir além, você pode filtrar os cartões apresentados no view controller definindo a propriedade transform na sua struct Attributes, para exibir apenas os cartões que atendem aos seus critérios.

var attributes = BrazeContentCardUI.ViewController.Attributes.defaults
attributes.transform = { cards in
  cards.filter { $0.extras["feed_type"] as? String == "Transactional" }
}

// Pass your attributes containing the transformed cards to the Content Card UI.
let viewController = BrazeContentCardUI.ViewController(braze: AppDelegate.braze, attributes: attributes)
// Filter cards by the `Transactional` feed type based on your key-value pair.
NSMutableArray<BRZContentCardRaw *> *transactionalCards = [[NSMutableArray alloc] init];
for (BRZContentCardRaw *card in AppDelegate.braze.contentCards.cards) {
  if ([card.extras[@"feed_type"] isEqualToString:@"Transactional"]) {
    [transactionalCards addObject:card];
  }
}
New Stuff!