Passer au contenu

Personnaliser le flux pour les Content Cards

Un flux de Content Cards correspond à la séquence de Content Cards dans vos applications mobiles ou Web. Cet article traite de la configuration du moment où le flux est actualisé, de l’ordre des cartes, de la gestion de plusieurs flux et des messages d’erreur « flux vide ». Pour obtenir la liste complète des types de cartes de contenu, consultez À propos des Content Cards.

À propos du cycle de vie de la session

Une session désigne la période pendant laquelle le SDK Braze suit l’activité des utilisateurs dans votre application après son lancement. Vous pouvez également forcer une nouvelle session en appelant la méthode changeUser().

Par défaut, une session commence lorsque vous appelez braze.openSession() pour la première fois. La session restera active pendant un maximum de 30 minutes d’inactivité (à moins que vous ne modifiiez le délai d’expiration par défaut de la session ou que l’utilisateur ne ferme l’application).

Par défaut, une session commence lorsque openSession() est appelé pour la première fois. Si votre application passe en arrière-plan puis revient au premier plan, le SDK vérifie si plus de 10 secondes se sont écoulées depuis le début de la session (à moins que vous ne modifiiez le délai d’expiration par défaut de la session). Dans ce cas, une nouvelle session débutera. Gardez à l’esprit que si l’utilisateur ferme votre application alors qu’elle est en arrière-plan, les données de session risquent de ne pas être envoyées à Braze tant qu’il n’aura pas rouvert l’application.

Appeler closeSession() ne mettra pas immédiatement fin à la session. La session prendra fin après 10 secondes si openSession() n’est pas rappelé par l’utilisateur démarrant une autre activité.

Par défaut, une session commence lorsque vous appelez Braze.init(configuration:). Cela se produit lorsque la notification UIApplicationWillEnterForegroundNotification est déclenchée, ce qui signifie que l’application est passée au premier plan.

Si votre application passe en arrière-plan, UIApplicationDidEnterBackgroundNotification est déclenché. L’application ne reste pas dans une session active lorsqu’elle est en arrière-plan. Lorsque votre application revient au premier plan, le SDK compare le temps écoulé depuis le début de la session au délai d’expiration de la session (à moins que vous ne modifiiez le délai d’expiration par défaut de la session). Si le temps écoulé depuis le début de la session dépasse le délai d’expiration, une nouvelle session commence.

Actualisation du flux

Actualisation automatique

Par défaut, le flux de Content Cards s’actualise automatiquement lorsque :

  • Une nouvelle session démarre.
  • Le flux de Content Cards par défaut est fermé puis rouvert plus de 60 secondes après la dernière actualisation.

Distribution en temps réel

Braze envoie également les mises à jour des Content Cards à l’appareil dès qu’elles se produisent, via une connexion active que le SDK maintient pendant la session. Les utilisateurs n’ont pas besoin de démarrer une nouvelle session ni d’attendre une actualisation pour voir le changement. La distribution en temps réel fonctionne aussi bien avec les flux de Content Cards par défaut qu’avec les flux personnalisés.

La distribution en temps réel couvre les mises à jour suivantes :

  • Un utilisateur devient éligible à une campagne de Content Cards au cours d’une session.
  • Un utilisateur atteint une étape Content Card dans un Canvas.
  • Une carte est retirée du flux d’un utilisateur.
  • Une carte est envoyée via l’API, par exemple avec l’endpoint /messages/send, /campaigns/trigger/send ou /canvas/trigger/send.

La distribution en temps réel nécessite les versions minimales suivantes du SDK :

Avec les versions antérieures du SDK, les cartes continuent d’arriver au démarrage de la session et lors de l’actualisation.

Actualisation manuelle

Pour actualiser manuellement le flux à un moment précis :

Demandez à tout moment une actualisation manuelle des Content Cards de Braze depuis le SDK Web en appelant requestContentCardsRefresh().

Vous pouvez également appeler getCachedContentCards pour obtenir toutes les cartes actuellement disponibles depuis la dernière actualisation des Content Cards.

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

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

Pour ouvrir les liens des Content Cards dans un nouvel onglet du navigateur plutôt que dans le même onglet, définissez openCardsInNewTab: true dans les options d’initialisation de votre SDK Web. Pour en savoir plus sur les options d’initialisation, consultez le guide du dépôt du SDK Web.

Demandez à tout moment une actualisation manuelle des Content Cards de Braze depuis le SDK Android en appelant requestContentCardsRefresh.

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

Demandez à tout moment une actualisation manuelle des Content Cards de Braze depuis le SDK Swift en appelant la méthode requestRefresh de la classe Braze.ContentCards :

En Swift, les Content Cards peuvent être actualisées soit avec un gestionnaire d’achèvement facultatif, soit avec un retour asynchrone grâce aux API de concurrence natives de Swift.

Gestionnaire d’achèvement

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
}];

Synchronisation complète ou partielle

Le SDK de Braze utilise deux types de synchronisation pour récupérer les Content Cards depuis le serveur :

  • Synchronisation complète : Récupère toutes les Content Cards auxquelles un utilisateur est éligible. Les synchronisations complètes ont lieu automatiquement tous les 7 jours ou à chaque appel de changeUser().
  • Synchronisation partielle : Récupère uniquement les nouvelles Content Cards depuis la dernière requête. Si l’utilisateur n’est éligible à aucune nouvelle carte, la réponse renvoie zéro carte. Les synchronisations partielles ont lieu à chaque appel de requestContentCardsRefresh() (sauf si 7 jours se sont écoulés depuis la dernière synchronisation complète, auquel cas une synchronisation complète est déclenchée à la place).

Les synchronisations partielles réduisent la charge du serveur et la consommation de batterie de l’appareil. Les Content Cards déjà reçues sont stockées localement dans le SDK : les utilisateurs continuent donc de voir leurs cartes disponibles même lorsqu’une synchronisation partielle ne renvoie aucune nouvelle carte.

Limite de débit

Braze utilise un algorithme de compartiment à jetons pour appliquer les limites de débit suivantes :

  • Jusqu’à 5 appels d’actualisation par appareil, partagés entre les utilisateurs et les appels à openSession()
  • Une fois la limite atteinte, un nouvel appel devient disponible toutes les 180 secondes (3 minutes)
  • Le système conserve jusqu’à cinq appels que vous pouvez utiliser à tout moment
  • subscribeToContentCards() renvoie toujours les cartes mises en cache, même en cas de limitation du débit

Personnaliser l’ordre d’affichage des cartes

Vous pouvez modifier l’ordre dans lequel vos Content Cards s’affichent. Vous pouvez ainsi affiner l’expérience utilisateur en donnant la priorité à certains types de contenu, comme les promotions à durée limitée.

Personnalisez l’ordre d’affichage des Content Cards dans votre flux à l’aide du paramètre filterFunction de showContentCards():. Par exemple :

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

Le ContentCardsFragment s’appuie sur un IContentCardsUpdateHandler pour gérer le tri ou les modifications des Content Cards avant leur affichage dans le flux. Vous pouvez définir un gestionnaire de mise à jour personnalisé via setContentCardUpdateHandler sur votre ContentCardsFragment.

Voici le IContentCardsUpdateHandler par défaut, que vous pouvez utiliser comme point de départ pour votre personnalisation :

Afficher l’exemple 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
  }
}
Afficher l’exemple 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)
      }
    }
  }
}

Pour filtrer et trier les Content Cards dans Jetpack Compose, définissez le paramètre cardUpdateHandler. Par exemple :

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
        }
    }
)

Personnalisez l’ordre du flux de cartes en modifiant directement la variable statique 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)

La personnalisation via BrazeContentCardUI.ViewController.Attributes n’est pas disponible en Objective-C.

Personnaliser le message « flux vide »

Lorsqu’un utilisateur n’est éligible à aucune Content Card, le SDK affiche un message d’erreur « flux vide » indiquant : « We have no updates. Please check again later. » Vous pouvez personnaliser ce message d’erreur « flux vide » comme dans l’exemple suivant :

Message d’erreur de flux vide indiquant « This is a custom empty state message. »

Le SDK Web ne permet pas de remplacer le texte « flux vide » par programmation. Vous pouvez choisir de le remplacer à chaque affichage du flux, mais cette approche est déconseillée : l’actualisation du flux peut prendre un certain temps et le texte de flux vide ne s’affichera pas immédiatement.

Si le ContentCardsFragment détermine que l’utilisateur n’est éligible à aucune Content Card, il affiche le message d’erreur de flux vide.

Un adaptateur spécial, l’EmptyContentCardsAdapter, remplace l’adaptateur standard ContentCardAdapter pour afficher ce message d’erreur. Pour définir le message personnalisé lui-même, remplacez la ressource de chaîne de caractères com_braze_feed_empty.

Le style utilisé pour afficher ce message est disponible dans Braze.ContentCardsDisplay.Empty et est reproduit dans l’extrait de code suivant :

<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>

Pour plus d’informations sur la personnalisation des éléments de style des Content Cards, consultez la section Personnaliser le style.

Pour personnaliser le message d’erreur « flux vide » avec Jetpack Compose, vous pouvez transmettre un emptyString à ContentCardsList. Vous pouvez également transmettre emptyTextStyle à ContentCardListStyling pour personnaliser davantage ce message.

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

Si vous préférez afficher un Composable à la place, vous pouvez transmettre emptyComposable à ContentCardsList. Si emptyComposable est spécifié, emptyString ne sera pas utilisé.

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

Personnalisez l’état vide du contrôleur de vue en définissant les Attributes correspondants.

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

Modifiez le texte qui s’affiche automatiquement dans les flux de Content Cards vides en redéfinissant les chaînes de caractères localisables des Content Cards dans le fichier ContentCardsLocalizable.strings de votre application.

Implémenter plusieurs flux

Vous pouvez filtrer les Content Cards dans votre application afin de n’afficher que certaines cartes, ce qui vous permet de disposer de plusieurs flux de Content Cards pour différents cas d’usage. Par exemple, vous pouvez gérer à la fois un flux transactionnel et un flux marketing. Pour cela, créez différentes catégories de Content Cards en définissant des paires clé-valeur dans le tableau de bord de Braze. Créez ensuite dans votre application ou votre site des flux qui traitent ces types de Content Cards différemment, en filtrant certains types et en affichant les autres.

Étape 1 : Définir des paires clé-valeur sur les cartes

Lorsque vous créez une campagne de cartes de contenu, définissez des données de paires clé-valeur sur chaque carte. Vous utiliserez cette paire clé-valeur pour classer les cartes par catégorie. Les paires clé-valeur sont stockées dans la propriété extras du modèle de données de la carte.

Dans cet exemple, nous allons définir une paire clé-valeur avec la clé feed_type, qui indiquera dans quel flux de Content Cards la carte doit s’afficher. La valeur correspondra à vos flux personnalisés, par exemple home_screen ou marketing.

Étape 2 : Filtrer les Content Cards

Une fois les paires clé-valeur attribuées, créez un flux doté d’une logique qui affiche les cartes souhaitées et filtre les cartes des autres types. Dans cet exemple, nous n’afficherons que les cartes dont la paire clé-valeur correspond à feed_type: "Transactional".

L’exemple suivant affiche le flux de Content Cards pour les cartes de type 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);
  });
}

Vous pouvez ensuite configurer un bouton pour basculer vers votre flux personnalisé :

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

Pour plus d’informations, consultez la documentation de la méthode du SDK.

Par défaut, le flux de Content Cards s’affiche dans un ContentCardsFragment, et IContentCardsUpdateHandler renvoie une liste de cartes à afficher après avoir reçu un ContentCardsUpdatedEvent du SDK de Braze. Toutefois, il se contente de trier les cartes et ne gère directement aucun filtrage.

Étape 2.1 : Créer un gestionnaire personnalisé

Vous pouvez filtrer les Content Cards en implémentant un IContentCardsUpdateHandler personnalisé à l’aide des paires clé-valeur définies dans le tableau de bord et récupérées par Card.getExtras(), puis en le modifiant pour supprimer de la liste toutes les cartes qui ne correspondent pas à la valeur de feed_type définie précédemment.

Afficher l’exemple Java
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;
    }
  };
}
Afficher l’exemple Kotlin
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
  }
}

Étape 2.2 : L’ajouter à un fragment

Après avoir créé un IContentCardsUpdateHandler, créez un ContentCardsFragment qui l’utilise. Ce flux personnalisé s’utilise comme n’importe quel autre ContentCardsFragment. Dans les différentes parties de votre application, affichez différents flux de Content Cards en fonction de la clé définie dans le tableau de bord. Chaque flux ContentCardsFragment affichera un ensemble unique de cartes grâce au IContentCardsUpdateHandler personnalisé de chaque fragment.

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

Pour filtrer les Content Cards affichées dans ce flux, utilisez cardUpdateHandler. Par exemple :

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" }

Pour aller plus loin, vous pouvez filtrer les cartes présentées dans le contrôleur de vue en définissant la propriété transform de votre structure Attributes, afin de n’afficher que les cartes correspondant à vos critères.

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!