GiftingStack
Sign in Get started

Guide

How to add virtual gifts to a Firebase chat

Your app already chats through Firestore: a messages collection under each chat, read with a live listener. GiftingStack adds the gifts: coins, the gift tray, the effects and the ledger. GiftingStack carries no chat of its own, so every gift lands in your Firestore chat, next to the messages your users already read. This guide shows both ways to get it there, and where the gift button, the tray and the effects go on the screen.

Two ways a gift reaches the chat

Pick one per screen, so a gift never shows twice. Both start from the same setup: a project, a gift catalog and a client token (quickstart).

1. Send the gift with the chat as its reference

For the server path, the webhook has to know which chat the gift belongs to. Give the tray the chat's document id as reference (your own context id, up to 128 characters); it comes back in the gift.sent event.

const tray = createGiftTray({
  container: screen,
  client,
  receiver: otherMemberId,                 // who gets the gift (your user id)
  reference: `chat:${chatId}`,             // "chat:ava-mateo"
  effects,
  onTopUp: openCoinStore,
});

2. Server: write a gift message on gift.sent

Verify the signature on the raw body first, with the verify function from Webhooks. Then write with the Firebase Admin SDK (npm i firebase-admin). The document id is the event id and create refuses an existing document, so a repeated delivery never writes a second message.

import express from "express";
import { initializeApp } from "firebase-admin/app";
import { FieldValue, getFirestore } from "firebase-admin/firestore";

initializeApp();
const db = getFirestore();

app.post("/webhooks/giftingstack", express.text({ type: "application/json" }), async (req, res) => {
  if (!verify(req.body, req.get("GiftingStack-Signature") ?? "", process.env.GS_WEBHOOK_SECRET))
    return res.sendStatus(400);
  const event = JSON.parse(req.body);
  const gift = event.data;
  if (event.type !== "gift.sent" || !gift.reference?.startsWith("chat:")) return res.sendStatus(200);

  const chatId = gift.reference.slice("chat:".length);
  try {
    await db.collection("chats").doc(chatId).collection("messages").doc(event.id).create({
      type: "gift",
      senderId: gift.sender,
      giftName: gift.gift.name,
      giftIcon: gift.gift.icon_url,
      quantity: gift.quantity,
      createdAt: FieldValue.serverTimestamp(),
    });
  } catch (err) {
    if (err.code !== 6) return res.sendStatus(500);              // 6: already written; else retried later
  }
  res.sendStatus(200);
});
match /chats/{chatId}/messages/{messageId} {
  allow create: if isMember(chatId) && request.resource.data.type == "text";
}

When Firestore fails, the handler answers 500 and GiftingStack delivers the event again later. Answer within 10 seconds: when writing can take longer, queue it and answer first.

3. App: a live gift row from the gift room

In a live stream, every viewer joins the gift room. The room's onEvent hands over each gift sent into it by someone else; the tray's onSent hands over the viewer's own. Both carry the gift transaction id, so a gift replayed after a reconnect is not added again. The stream's chat messages from Firestore go into the same list.

import { GiftEffectLayer, createGiftTray, joinRoom } from "@giftingstack/sdk/web";
import { collection, onSnapshot, orderBy, query } from "firebase/firestore";

const shown = new Set();
function addGiftRow(tx, who, giftName, icon, quantity) {
  if (shown.has(tx)) return;
  shown.add(tx);
  chatList.append(renderGiftRow({ who, giftName, icon, quantity }));   // your row component
}

const effects = new GiftEffectLayer(videoBox);           // effects play over the video
const room = joinRoom({
  room: "live_123",
  getToken,
  client,
  effects,
  onEvent: (e) => addGiftRow(e.tx, e.senderName ?? "Someone", e.gift.name, e.gift.icon, e.quantity),
});
const tray = createGiftTray({
  container: screen,
  client,
  receiver: hostId,
  roomId: "live_123",
  effects,
  anchor: giftButton,
  collapseOnSelect: true,
  scrim: true,
  onTopUp: openCoinStore,
  onSent: (t) => addGiftRow(t.id, "You", t.gift.name, t.gift.icon_url, t.quantity),
});
giftButton.onclick = () => tray.toggle();

// the stream's chat, from Firestore, into the same list
const messages = query(collection(db, "chats", "live_123", "messages"), orderBy("createdAt"));
const stop = onSnapshot(messages, (snap) => {
  for (const change of snap.docChanges()) {
    if (change.type === "added") chatList.append(renderMessage(change.doc.data()));
  }
});
// leaving the stream: room.leave(); tray.destroy(); stop();

React Native: the same two callbacks on the hooks.

const room = useGiftRoom({
  room: "live_123", getToken, client, effects,
  onEvent: (e) => addGiftRow(e.tx, e.senderName ?? "Someone", e.gift.name, e.gift.icon, e.quantity),
});
const tray = useGiftTray({
  client, receiver: hostId, roomId: "live_123", effects,
  onSent: (t) => addGiftRow(t.id, "You", t.gift.name, t.gift.icon_url, t.quantity),
});

Flutter GiftRoom(onEvent: ...) with GiftTrayNotifier(onSent: ...), Android rememberGiftRoom(onEvent = ...) with rememberGiftTrayHolder(onSent = ...), iOS GiftRoomModel(..., onEvent: ...) with GiftTrayModel(..., onSent: ...). Every room event has tx, senderName, gift.name and quantity. The room carries gift events only: no message text, no chat.

4. The screen layout

Colours, texts and motion: Customize. Rooms, tokens and limits: Gift rooms.

Next steps