Docs
Customize the SDK
Everything you can change about how the SDKs look and behave, with a short snippet for each of the five platforms: Web, React Native, Flutter, Android (Compose) and iOS (SwiftUI). Pick a platform in any tab row and the whole page follows.
A "Looks like" line under each heading says what you will see, because this page has no screenshots. Anything that exists on one platform only is marked, and the table at the end sums it up.
On this page: Theme · Tray placement · Gift button in your message bar · Texts · Reduced motion · Low quality · Test mode · Get coins · Combo · Gift pictures · Effect tiers · Render slots · Battle bar · Wall, leaderboard, away · Inbox · More options · Custom elements · Platforms
Theme: one accent colour, light and dark
Every colour of the tray, the notice, the wall, the card and the leaderboard follows your brand colour, and the text on it stays readable.
Build a whole theme from one colour with themeFromAccent, or take a ready
preset (dark or light). Text pairs are kept at 4.5:1 contrast. To change a single
colour, override that token on top of a theme: the 17 tokens are
background, surface, border, text,
muted, accent, accentText, coin,
coinText, selected, danger,
noticeBackground, noticeText, comboText,
noticeBigBackground, noticeBigText and glow. Pass
the same theme to the tray and the effect layer (and to every other component).
Web
import { darkTheme, lightTheme, themeFromAccent } from "@giftingstack/sdk/web";
const theme = themeFromAccent("#ff4f9a", "light"); // or darkTheme, lightTheme
const effects = new GiftEffectLayer(screen, { theme });
const handle = createGiftTray({ container: screen, client, receiver, effects, theme });
handle.tray.update({ theme: { ...theme, coin: "#ffd27a" } }); // switch live, one token changed
effects.setOptions({ theme });
React Native
import { darkTheme, lightTheme, themeFromAccent } from "@giftingstack/sdk/react-native";
const theme = themeFromAccent("#ff4f9a", "light"); // or darkTheme, lightTheme
<GiftTray gifts={tray.gifts} balance={tray.balance} onSend={tray.send} theme={theme} />
<GiftEffectLayer ref={effects} theme={theme} />
// one token changed: theme={{ ...theme, coin: "#ffd27a" }}
Flutter
final theme = themeFromAccent('#ff4f9a', GiftThemeMode.light); // or darkTheme, lightTheme
GiftTray(gifts: tray.gifts, balance: tray.balance, onSend: tray.send, theme: theme)
GiftEffectLayer(controller: effects, theme: theme)
// one token changed:
final brand = theme.copyWith(coin: const Color(0xFFFFD27A));
Android (Compose)
val theme = GiftingTheme.fromAccent("#ff4f9a", dark = false) // or GiftingTheme.Dark, GiftingTheme.Light
GiftTray(gifts = state.gifts, balance = state.balance, onSend = { tray.send(it) }, theme = theme)
GiftEffectLayer(effects, theme = theme)
// one token changed:
val brand = theme.copy(coin = Color(0xFFFFD27A))
iOS (SwiftUI)
var theme = try GiftingTheme.fromAccent("#ff4f9a", mode: .light) // or GiftingTheme.dark, GiftingTheme.light
GiftTray(gifts: tray.gifts, balance: tray.balance, onSend: { tray.send($0) }, theme: theme)
GiftEffectLayer(controller: effects, theme: theme)
// one token changed:
theme.coin = Color(red: 1, green: 0.82, blue: 0.48)
On the web you can also set the CSS variables (--gs-accent,
--gs-coin, --gs-surface and so on) from your own style sheet
instead of passing a theme. A bad accent colour (not a hex value) throws.
Tray placement
By default the tray is a bottom sheet you can drag between three sizes; on a wide desktop page it can also be a popover under your gift button, or a panel inside your layout.
Every platform has the bottom sheet: peek (handle, balance and the Send
bar), default (two rows of gifts) and expanded (up to 90 % of
the screen). Choose where it opens, listen for changes and keep the effect notice above
it with the inset callback. sheet set to false gives the old fixed tray in
your layout.
Web
// Bottom sheet (the default below 600 px)
const handle = createGiftTray({
container: screen, client, receiver, effects,
initialSnap: "peek", // "peek" | "default" | "expanded"
snaps: { expanded: 0.6 }, // heights as fractions of the container
onInsetChange: (px) => effects.setOptions({ noticeBottom: px + 16 }),
scrim: true, // a dim behind the sheet, hidden while it is at peek
scrimTap: "dismiss", // a tap on the dim: "lower" (default) | "dismiss"
onDismiss: () => {}, // the tap asked to close (the tray is already closed)
});
giftButton.onclick = () => handle.toggle(); // open if closed, close if open
// Desktop: a popover under your gift button, or inline in a column
createGiftTray({ container: playerColumn, anchor: giftButton, client, receiver, effects });
createGiftTray({ container: column, client, receiver, effects, placement: "inline" });
// placement: "auto" (default) | "sheet" | "popover" | "inline"
Popover and inline are web only. A popover needs an
anchor and a container at least 600 px wide; Escape and a click outside
close it.
React Native
<GiftTray
tray={tray} // the holder from useGiftTray: its open state, toggle() and close() drive the sheet
gifts={tray.gifts} balance={tray.balance} onSend={tray.send}
initialSnap="peek" // "peek" | "default" | "expanded"
snaps={{ expanded: 0.6 }}
onSnapChange={(snap) => {}}
onInsetChange={setInset} // pass inset + 16 as noticeBottom of the effect layer
bottomInset={safeArea.bottom}
scrim // a dim behind the sheet, hidden while it is at peek
scrimTap="dismiss" // a tap on the dim: "lower" (default) | "dismiss"
onDismiss={() => {}} // the tap asked to close (tray.close() already ran)
// sheet={false}: the old fixed tray in the normal layout
/>
// Your Gift button: <Pressable onPress={tray.toggle} />
// tray = useGiftTray({ ..., open: false }) starts closed; also tray.open(), tray.close(), tray.isOpen
Flutter
GiftTray(
tray: tray, // the GiftTrayNotifier: its open state, toggle() and close() drive the sheet
gifts: tray.gifts, balance: tray.balance, onSend: tray.send,
initialSnap: SheetSnap.peek, // peek | standard | expanded ("standard" is the "default" snap)
snaps: const {SheetSnap.expanded: 0.6},
onSnapChange: (snap) {},
onInsetChange: (px) => setState(() => noticeBottom = px + 16),
bottomInset: MediaQuery.paddingOf(context).bottom,
scrim: true, // a dim behind the sheet, hidden while it is at peek
scrimTap: ScrimTap.dismiss, // a tap on the dim: ScrimTap.lower (default) | dismiss
onDismiss: () {}, // the tap asked to close (tray.close() already ran)
// sheet: false, the old fixed tray in the normal layout
)
// Your Gift button: onPressed: tray.toggle
// GiftTrayNotifier(open: false) starts closed; also tray.open(), tray.close(), tray.isOpen
Android (Compose)
GiftTray(
tray = tray, // the holder: gifts, balance, send and the open state (toggle(), close()) come from it
initialSnap = SheetSnap.Peek, // Peek | Default | Expanded
snaps = SheetSnapFractions(expanded = 0.6),
onSnapChange = { snap -> },
onInsetChange = { inset = it }, // GiftEffectLayer(noticeBottom = inset + 12.dp)
bottomInset = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding(),
scrim = true, // a dim behind the sheet, hidden while it is at peek
scrimTap = ScrimTap.Dismiss, // a tap on the dim: ScrimTap.Lower (default) | Dismiss
onDismiss = { }, // the tap asked to close (tray.close() already ran)
// sheet = false: the old fixed tray in the normal layout
)
// Your Gift button: onClick = { tray.toggle() }
// the holder starts closed with open = false; also tray.open(), tray.close(), tray.isOpen
iOS (SwiftUI)
GiftTray(
tray: tray, // the GiftTrayModel: gifts, balance, send and the open state (toggle(), close())
gridMaxHeight: 230, // used with sheet: false, the old fixed tray
sheet: true,
initialSnap: .peek, // .peek | .default | .expanded
snaps: nil, // SheetSnaps(...): heights as fractions
onSnapChange: { snap in },
onInsetChange: { noticeBottom = $0 + 16 },
scrim: true, // a dim behind the sheet, hidden while it is at peek
scrimTap: .dismiss, // a tap on the dim: .lower (default) | .dismiss
onDismiss: { } // the tap asked to close (tray.close() already ran)
)
// Your Gift button: Button("Gift") { tray.toggle() }
// the holder starts closed with open: false; also tray.open(), tray.close(), tray.isOpen
Popover and inline are not on React Native, Flutter, Android or iOS: they use the bottom sheet only (see the table on SDKs).
Dim behind the sheet and your Gift button
scrim (off by default) draws a dim behind the sheet over the whole screen.
It fades in over 200 ms (at once when the person turned animations off), goes away while
the sheet is at peek so the page or the stream stays in sight, and takes the taps above
the sheet, at peek too: a tap on the empty space beside a small tray closes it, and
the next tap reaches your page. (Leave scrim off if the page must stay
touchable beside a small tray.) scrimTap says what a tap on it does:
"lower" (default) lowers a tall sheet to peek and closes a peeking one,
"dismiss" closes the tray from every state; onDismiss tells
you. Your Gift button is one line,
tray.toggle(): it opens a closed tray and closes an open one
(open(), close() and isOpen are there too, and
open: false starts it closed). While it is closed the sheet and the dim are
invisible and take no touches, and the inset callback says 0. The web popover keeps its
own behaviour: Escape and a click outside close it.
Gift button in your message bar
A small gift button that sits in your own chat or comment bar, like an emoji button. Tap it: the keyboard goes down and the gift tray takes its place. Tap the text field: the tray goes away and the keyboard comes back.
GiftingStack has no chat of its own; it fits into yours. Put GiftButton
inside or next to your text field and give the tray that field. The SDK then does the
swap on every platform: the button closes the keyboard and opens the tray in the
keyboard's place (on a phone about as tall as the keyboard was, docked at the bottom,
with no dim, so the conversation stays in sight); a tap on the field closes the tray and
the keyboard comes back; the button again does the same. With a field the tray starts
closed. Everything else stays: collapseOnSelect, onSent,
combos, and onInsetChange, which you use to keep your message bar on top of
the tray as the keyboard would. On a wide web page the button opens the popover instead.
The gift line in your chat is yours to draw: onSent gives you each of your
own gifts, the room's events the others'.
// Web
const button = new GiftButton(messageBar, { theme }); // or <gift-button id="gift">
createGiftTray({ container: screen, client, receiver, effects,
anchor: button.element, input: messageField, collapseOnSelect: true,
onInsetChange: (px) => messageBar.style.setProperty("bottom", px + "px") });
// <gift-tray receiver="mia" anchor="#gift" input="#msg"></gift-tray>
// React Native
const field = useRef(null);
const tray = useGiftTray({ client, receiver, effects, input: field });
<TextInput ref={field} /> <GiftButton tray={tray} />
<GiftTray tray={tray} collapseOnSelect onInsetChange={setInset} />
// Flutter
final tray = GiftTrayNotifier(client: client, receiver: receiver, input: focusNode);
TextField(focusNode: focusNode), GiftButton(tray: tray), GiftTray(tray: tray)
// Android (Compose)
TextField(value, onValueChange, modifier = Modifier.giftTrayInput(holder))
GiftButton(tray = holder); GiftTray(tray = holder)
// iOS (SwiftUI)
@FocusState var typing: Bool
TextField("Message", text: $text).giftTrayInput(model, focused: $typing)
GiftButton(tray: model); GiftTray(tray: model)
GiftButton options: label (default "Send a gift", read by
screen readers), filled (off: the glyph in your accent colour, like an
emoji button; on: an accent circle), size (small 32,
medium 36 or large 44: three fixed sizes, never a free number),
icon (your own glyph instead of the gift, see below), disabled
and theme, the same tokens as the tray. In a battle one button is enough: the
tray's receivers option adds a "Send to" row with every side
(how). While its tray is open it shows the
selected colour and says "expanded". The tray learns the keyboard's height
the first time the keyboard shows (300 until then, and never more than 60 % of the
screen); pass your own snaps.default to choose the height yourself.
Your own icon
icon replaces the gift glyph; the circle, colours, states and tap target
stay. The icon is centred in a fixed box (22 of 36 of the circle) and fitted into it:
it is scaled down, never up, with its proportions kept, and anything outside the box is
cut off, so your artwork can never change the size of the button. You get the colour
the glyph would have (your accent colour, or the text-on-accent colour when
filled), so a one-colour icon follows your theme.
// Web: SVG markup (currentColor follows the glyph colour) or an element (an img is not tinted)
new GiftButton(messageBar, { icon: '<svg viewBox="0 0 24 24"><path d="..." fill="currentColor"/></svg>', size: "large" });
// React Native: <GiftButton tray={tray} icon={(color) => <MyIcon color={color} />} />
// Flutter: GiftButton(tray: tray, icon: (context, color) => Icon(Icons.star, color: color))
// Android: GiftButton(tray = holder, icon = { color -> Icon(MyIcon, null, tint = color) })
// iOS: GiftButton(tray: model, icon: { color in AnyView(Image("star").foregroundColor(color)) })
Texts and translations
Every word the SDK shows or speaks to a screen reader is yours: the tray title, the Send button, the card heading, the leaderboard tabs.
The tray takes its texts as plain options. The wall, the away card and the leaderboard
take a labels object with the keys they use (for example
awayTitle, dismiss, tryAgain,
pinned, loading, and per-period tabs and
empty texts). Unset ones stay English, and {name}-style places
are filled in for you. The battle bar has its own labels (see below).
Web
createGiftTray({
container: screen, client, receiver, effects,
title: "Hediye gönder", coinName: "jeton", sendLabel: "Gönder",
notEnoughLabel: "Yetersiz jeton", getCoinsLabel: "Jeton al", testModeLabel: "Test modu",
});
new GiftAwaySummary(screen, { client, effects, labels: { awayTitle: "Sen yokken", dismiss: "Kapat" } });
React Native
<GiftTray
gifts={tray.gifts} balance={tray.balance} onSend={tray.send}
title="Hediye gönder" coinName="jeton" sendLabel="Gönder"
notEnoughLabel="Yetersiz jeton" getCoinsLabel="Jeton al" testModeLabel="Test modu"
/>
<GiftAwaySummary client={client} effects={effects} labels={{ awayTitle: "Sen yokken", dismiss: "Kapat" }} />
Flutter
GiftTray(
gifts: tray.gifts, balance: tray.balance, onSend: tray.send,
title: 'Hediye gönder', coinName: 'jeton', sendLabel: 'Gönder',
notEnoughLabel: 'Yetersiz jeton', getCoinsLabel: 'Jeton al', testModeLabel: 'Test modu',
)
GiftAwaySummary(client: client, effects: effects,
labels: const GiftLabels(awayTitle: 'Sen yokken', dismiss: 'Kapat'))
Android (Compose)
GiftTray(
gifts = state.gifts, balance = state.balance, onSend = { tray.send(it) },
title = "Hediye gönder", coinName = "jeton", sendLabel = "Gönder",
notEnoughLabel = "Yetersiz jeton", getCoinsLabel = "Jeton al", testModeLabel = "Test modu",
)
GiftAwaySummary(client, effects = effects,
labels = GiftLabels(awayTitle = "Sen yokken", dismiss = "Kapat"))
iOS (SwiftUI)
GiftTray(
gifts: tray.gifts, balance: tray.balance, onSend: { tray.send($0) },
title: "Hediye gönder", coinName: "jeton", sendLabel: "Gönder",
notEnoughLabel: "Yetersiz jeton", getCoinsLabel: "Jeton al",
testModeLabel: "Test modu"
)
var labels = GiftLabels()
labels.awayTitle = "Sen yokken"
labels.dismiss = "Kapat"
@StateObject var away = GiftAwayModel(client: client, labels: labels)
Web custom elements take the tray texts as attributes (tray-title,
coin-name, send-label, not-enough-label,
get-coins-label) and labels as a property.
Reduced motion
With reduced motion on, gifts do not fly: only the small notice ("ayla sent Rose") slides in, the cards appear without sliding, and on web and Flutter the sheet snaps without animating.
By default every platform follows the system setting (web:
prefers-reduced-motion, live; Android: "Remove animations"; iOS: Reduce
Motion; Flutter: MediaQuery.disableAnimations). Pass
reducedMotion to force it on or off; every component takes the same option.
Web
new GiftEffectLayer(screen, { reducedMotion: true }); // true | false | leave out to follow the browser
React Native
<GiftEffectLayer ref={effects} reducedMotion />
Flutter
GiftEffectLayer(controller: effects, reducedMotion: true) // true | false | null (follows the system)
Android (Compose)
GiftEffectLayer(effects, reducedMotion = true) // true | false | null (follows the system)
iOS (SwiftUI)
GiftEffectLayer(controller: effects, reducedMotion: true) // true | false | nil (follows the system)
Low-quality mode for slow phones
Large gifts lose the coin rain and the light rays, and there are fewer sparks, so a slow phone keeps its frame rate; the gift picture and the notice are the same.
The app chooses: the SDK does not detect a slow device. Test on low-end phones and switch it on if needed.
Web
new GiftEffectLayer(screen, { quality: "low" });
React Native
<GiftEffectLayer ref={effects} quality="low" />
Flutter
GiftEffectLayer(controller: effects, quality: EffectQuality.low)
Android (Compose)
GiftEffectLayer(effects, quality = EffectQuality.Low)
iOS (SwiftUI)
GiftEffectLayer(controller: effects, quality: .low)
The away card replays gifts through your layer; give it the same quality so
its replay is just as light.
Test mode badge
A small "Test mode" pill next to the tray title, shown while the client token was made with a test key, so testers know no real balance moves.
Pass the state holder to the tray (tray={tray}, tray: tray,
tray = holder) and the badge shows by itself while
GET /v1/client/me answers livemode: false. testMode
is optional: unset follows the state, false forces the badge off,
true forces it on. The badge text is testModeLabel. On the
web, createGiftTray does this by itself.
Web
createGiftTray({ container: screen, client, receiver, effects, testModeLabel: "Test modu" });
// With the presentational class you pass it yourself:
tray.update({ testMode: true });
React Native
<GiftTray tray={tray} />
Flutter
GiftTray(tray: tray)
Android (Compose)
GiftTray(tray = holder)
iOS (SwiftUI)
GiftTray(tray: model)
The "Get coins" button
A "+ Coins" button sits by the balance, and when the balance is too low the Send button turns into "Get coins"; both call your function, which opens your own coin store.
GiftingStack sells no coins and takes no payment: your function opens your store, and
your server credits the coins afterwards. Without onTopUp neither button is
shown and a low balance shows "Not enough coins". The text is
getCoinsLabel.
Web
createGiftTray({ container: screen, client, receiver, effects, onTopUp: openCoinStore });
React Native
<GiftTray gifts={tray.gifts} balance={tray.balance} onSend={tray.send} onTopUp={openCoinStore} />
Flutter
GiftTray(gifts: tray.gifts, balance: tray.balance, onSend: tray.send, onTopUp: openCoinStore)
Android (Compose)
GiftTray(gifts = state.gifts, balance = state.balance, onSend = { tray.send(it) }, onTopUp = { openCoinStore() })
iOS (SwiftUI)
GiftTray(gifts: tray.gifts, balance: tray.balance, onSend: { tray.send($0) }, onTopUp: { openCoinStore() })
On the web element, add the top-up attribute and listen for the
gift-topup event.
Combo counter
Sending the same gift again and again turns the notice into a counter that pops: "ayla sent Rose" x2, x3, x4.
Notices stack like banners in a live stream: up to three lines at once, the newest at the bottom and older ones above it. Gifts from different people (or different gifts) each get their own line; a combo updates its own line in place. Every line leaves on its own after its time, and a fourth line pushes the oldest one out.
A combo is the same sender and the same gift again within a time window, 2,500 ms by
default. Every sender and gift pair counts on its own, so several people can run a combo
at the same time and each one keeps its own line. Change the window with comboWindowMs; it beats the number from
remote recipes. On Android the window belongs to the layer state, on iOS to the effect
controller.
Web
new GiftEffectLayer(screen, { comboWindowMs: 3000 });
React Native
<GiftEffectLayer ref={effects} comboWindowMs={3000} />
Flutter
GiftEffectLayer(controller: effects, comboWindowMs: 3000)
Android (Compose)
val effects = rememberGiftEffectLayerState(comboWindowMs = 3000)
iOS (SwiftUI)
@StateObject private var effects = GiftEffectController(comboWindowMs: 3000)
Your own gift pictures
Each gift shows its own picture in the tray, the lists and the notice, and a larger picture in the effect. Both come from the gift in your catalog, so one change in the dashboard updates every platform.
A gift has two picture fields: icon_url (the picture you see in the tray,
lists and notices) and animation_url (the large picture shown with the gift
effect). Both are optional. There are two ways to fill them:
-
Upload in the dashboard. Open Gifts, add or edit a gift and
press Upload image next to the picture field. We store the image and fill the
field with its address (on
uploads.giftingstack.com). The Uploaded images section on the same page lists what you uploaded, how much of your allowance is used, and lets you copy an address or delete an image. - Use your own https URL. Paste the address of an image on your own server or CDN. The field takes any https address; nothing is uploaded to us.
| Limit for uploads | Value |
|---|---|
| File types | PNG, JPEG and WebP |
| Size of one file | 2 MB |
| Size of one image | At most 2048 × 2048 pixels |
| Per project environment (test and live count separately) | 200 images and 100 MB |
| Who can upload and delete | The project owner. Team members see the list but cannot change it. |
Deleting an uploaded image does not change your gifts: a gift that uses it shows nothing
until you give it another picture, so check the gifts first. Uploaded images are public
to anyone who has the address, like any picture your app shows. You are responsible for
having the rights to what you upload (see the
Terms of Service). Uploading is a dashboard feature; through the
API you set icon_url and animation_url to a URL.
Effect tiers: which gifts get which effect
Cheap gifts float up with a small notice, mid-price gifts fly to the middle and burst into sparks, and the most expensive ones dim the screen with light rays, coin rain and a golden notice.
The effect depends on the gift's price in coins. By default under 100 is small, 100 to
9,999 is medium, 10,000 and up is large. Move the two limits with
thresholds on the effect layer (and on the away card, so its replay
matches). Large gifts play one at a time; small and medium ones overlap, with a cap.
Web
new GiftEffectLayer(screen, { thresholds: { mediumFrom: 50, largeFrom: 5000 } });
React Native
<GiftEffectLayer ref={effects} thresholds={{ mediumFrom: 50, largeFrom: 5000 }} />
Flutter
GiftEffectLayer(controller: effects, thresholds: const TierThresholds(mediumFrom: 50, largeFrom: 5000))
Android (Compose)
GiftEffectLayer(effects, thresholds = TierThresholds(mediumFrom = 50, largeFrom = 5000))
iOS (SwiftUI)
GiftEffectLayer(controller: effects, thresholds: TierThresholds(mediumFrom: 50, largeFrom: 5000))
In live rooms, minTier on the room ("medium" on web, an
EffectTier value on the others) keeps small gifts off a busy screen; see
Gift rooms.
Render slots: draw the inside yourself
The wall, the leaderboard and the away card keep their frame, loading, order, names, focus and accessibility; you draw what is inside each chip, each row or the card body.
Three hooks, the same names on all five platforms: renderChip (wall),
renderRow (leaderboard) and renderSummary (away card). Return
nothing (null, nil) and the default is drawn, so you can
change some items only. Details and the full pattern are on
Integration patterns.
Web
new GiftWall(slot, { client, user: "mateo", renderChip: ({ gift, count }) => `${gift.name} x${count}` });
new GiftLeaderboard(panel, { client, renderRow: ({ rank, name, scoreText }) => `#${rank} ${name} (${scoreText})` });
new GiftAwaySummary(screen, { client, effects, renderSummary: ({ headline }) => headline });
React Native
<GiftWall client={client} user="mateo" renderChip={({ gift, count }) => <Text>{gift.name} x{count}</Text>} />
<GiftLeaderboard client={client} renderRow={({ rank, name, scoreText }) => <Text>#{rank} {name} {scoreText}</Text>} />
<GiftAwaySummary client={client} effects={effects} renderSummary={({ headline, dismiss }) => <Pressable onPress={dismiss}><Text>{headline}</Text></Pressable>} />
Flutter
GiftWall(client: client, user: 'mateo', renderChip: (context, chip) => Text('${chip.gift.name} x${chip.countText}'))
GiftLeaderboard(client: client, renderRow: (context, row) => row.isMe ? null : MyRow(row.rank, row.name, row.scoreText))
GiftAwaySummary(client: client, renderSummary: (context, s) => MyCard(s.headline, onClose: s.dismiss))
Android (Compose)
GiftWall(client, user = "mateo", renderChip = { chip -> BasicText("${chip.gift.name} x${chip.countText}") })
GiftLeaderboard(client, renderRow = { row -> if (row.isMe) row.Default() else MyRow(row.rank, row.name, row.scoreText) })
GiftAwaySummary(client, renderSummary = { s -> MyCard(s.headline, onClose = s.dismiss) })
iOS (SwiftUI)
GiftWall(loader: walls, user: "mateo", renderChip: { chip in AnyView(Text("\(chip.gift.name) x\(chip.countText)")) })
GiftLeaderboard(model: board, renderRow: { row in row.isMe ? nil : AnyView(MyRow(row)) })
GiftAwaySummary(model: away, renderSummary: { s in AnyView(MyCard(s.headline, close: s.dismiss)) })
Web returns a DOM node or a string (a string is text, never HTML). A hook that throws
falls back to the default on the web and reports to onError. With your own
card body a tap does not dismiss it: call dismiss.
Battle bar: side colours and labels
A bar for a live battle round: the receivers with their pictures and scores, a countdown and a split bar whose boundary slides as gifts arrive. Two receivers are the classic versus; three to eight (a free-for-all where everybody races on their own) get one cell each in a row, the clock above and a bar of proportional coloured segments.
The bar draws what your server's round says and decides nothing itself; nothing is drawn
while the room has no round with two sides. The side colours default to the theme's
accent, its coin colour, then teal and pink, then the same four again: override them
with sideColors, a list with one colour per side (a shorter list keeps the
default for the sides it does not name; a pair works as before). Texts
(vs, the final clock, the winner line, the draw line, the spoken summary)
are labels. Name and picture of each receiver come from
resolveUsers (web) or users (the others); without them the bar
shows the ids. It follows the theme and reduced motion.
Web
import { GiftBattleBar, joinRoom } from "@giftingstack/sdk/web";
const room = joinRoom({ room: "live_42", getToken, client, effects });
new GiftBattleBar(stage, {
room, // or call bar.setView(view) yourself
resolveUsers: async (ids) => ({ ava: { name: "Ava", avatarUrl }, mateo: { name: "Mateo" } }),
sideColors: ["#e0457b", "#3a8dff"],
labels: { final: "Bitti", wins: "{name} kazandı" },
});
React Native
const room = useGiftRoom({ room: "live_42", getToken, client, effects });
const view = useBattleScore(room.controller);
<BattleBar
view={view}
users={{ ava: { name: "Ava" }, mateo: { name: "Mateo" } }}
sideColors={["#e0457b", "#3a8dff"]}
labels={{ final: "Bitti", wins: "{name} kazandı" }}
/>
Flutter
GiftBattleBar(
room: room, // or view: myBattleView
users: {'ava': BattleBarUser(name: 'Ava'), 'mateo': BattleBarUser(name: 'Mateo')},
sideColors: const [Color(0xFFE0457B), Color(0xFF3A8DFF)],
labels: const BattleBarLabels(finalText: 'Bitti', wins: '{name} kazandı'),
)
Android (Compose)
GiftBattleBar(
room, // or GiftBattleBar(room.battleView(), ...)
users = mapOf("ava" to BattleBarUser("Ava"), "mateo" to BattleBarUser("Mateo")),
sideColors = Color(0xFFE0457B) to Color(0xFF3A8DFF),
labels = BattleBarLabels(finalText = "Bitti", wins = "{name} kazandı"),
)
iOS (SwiftUI)
GiftBattleBar(
room: room, // a GiftRoomModel
users: ["ava": BattleBarUser(name: "Ava"), "mateo": BattleBarUser(name: "Mateo")],
sideColors: ["#E0457B", "#3A8DFF"],
labels: BattleBarLabels(finalText: "Bitti", wins: "{name} kazandı")
)
The web label for the final clock is final; on Flutter, Android and iOS it
is finalText. The web element is <gift-battle-bar> with
the attributes side-colors ("#e0457b,#3a8dff"),
tick-ms and reduced-motion. Rounds are started and ended by
your server; see Gift rooms.
In a battle the gift notice also says who got the gift ("sent Rose to Mia") and gets a
2 px border in that side's colour (the side colours above, for every side of the
round; the text keeps its colour). Pass the same sideColors to the
room as to the bar – joinRoom, useGiftRoom,
GiftRoom, rememberGiftRoom, GiftRoomModel or
<gift-room side-colors="#e0457b,#3a8dff"> – and give the room
the same resolveName that names the senders, so the receiver has a name.
Outside battles nothing changes. The user's own gift says who got it too when the tray
knows the room: pass the room's receiverFor to the tray as receiverOf
(web createGiftTray({ receiverOf: room.receiverFor }), React Native
useGiftTray({ receiverOf: room.controller.receiverFor }), Flutter
GiftTrayNotifier(receiverOf: room.receiverFor), Android
rememberGiftTrayHolder(receiverOf = room::receiverFor), iOS
GiftTrayModel(receiverOf: room.receiverFor)). If you play your own events, set
receiver: { name, side, color } on the event.
Gift wall, leaderboard and "while you were away"
The wall is a row of small gift chips with counts under a post or on a profile, the card says "While you were away: 37 gifts from 12 people" and replays the biggest ones, and the leaderboard ranks senders or receivers with tabs and your own row pinned.
The options you will reach for first are below; every option is listed in the SDK's README for your platform. Wall: how many chips, in which order and how big. Leaderboard: which board, which period tabs, a coins or gifts switch, how many rows, and which user to highlight. Card: when it counts as seen, how many gifts it replays and for how long.
Web
new GiftWall(slot, { client, user: "mert", reference: "post_42", max: 4, order: "catalog", size: "sm" });
new GiftLeaderboard(panel, {
client, board: "receivers", periods: ["week", "all"], metricToggle: true,
limit: 20, highlightUser: "mert", coinName: "jeton",
});
new GiftAwaySummary(screen, {
client, effects, ackOn: "finished", minGifts: 3, maxEffects: 3, budgetMs: 5000,
});
React Native
<GiftWall client={client} user="mert" reference="post_42" max={4} order="catalog" size="sm" />
<GiftLeaderboard
client={client} board="receivers" periods={["week", "all"]} metricToggle
limit={20} highlightUser="mert" coinName="jeton"
/>
<GiftAwaySummary client={client} effects={effects} ackOn="finished" minGifts={3} maxEffects={3} budgetMs={5000} />
Flutter
GiftWall(client: client, user: 'mert', reference: 'post_42', max: 4, order: WallOrder.catalog, size: GiftWallSize.sm) GiftLeaderboard( client: client, board: LeaderboardBoard.receivers, periods: const [LeaderboardPeriod.week, LeaderboardPeriod.all], metricToggle: true, limit: 20, highlightUser: 'mert', coinName: 'jeton', ) GiftAwaySummary(client: client, effects: effects, ackOn: AwayAckOn.finished, maxEffects: 3, budgetMs: 5000)
Android (Compose)
GiftWall(client, user = "mert", reference = "post_42", max = 4, order = WallOrder.Catalog, size = GiftWallSize.Sm)
GiftLeaderboard(
client, board = LeaderboardBoard.Receivers,
periods = listOf(LeaderboardPeriod.Week, LeaderboardPeriod.All),
metricToggle = true, limit = 20, highlightUser = "mert", coinName = "jeton",
)
GiftAwaySummary(client, effects = effects, ackOn = AwayAckOn.Finished, maxEffects = 3, budgetMs = 5000)
iOS (SwiftUI)
GiftWall(loader: walls, user: "mert", reference: "post_42", max: 4, order: .catalog, size: .sm) @StateObject var board = GiftLeaderboardModel(client: client, board: .receivers, periods: [.week, .all], limit: 20) GiftLeaderboard(model: board, metricToggle: true, highlightUser: "mert", coinName: "jeton") @StateObject var away = GiftAwayModel(client: client, ackOn: .finished, maxEffects: 3, budgetMs: 5000) GiftAwaySummary(model: away)
minGifts (no card for fewer gifts, but they are still acknowledged) exists
on web and React Native only. The three components share one request
when several are on screen, cache answers (wall 60 s, leaderboard tabs 30 s) and never
show an error the person cannot act on.
Inbox
A list of who sent the signed-in person which gift over the last 24 hours: one row per gift, newest first, with the sender's initial, "Ayla sent you Rose x3", the gift picture and how long ago on the right.
It loads its own first page, shows "Show more" while there are older gifts and says "No
gifts in the last 24 hours" when there are none. Pass resolveUsers to
show display names and avatars instead of ids, and pageSize (1 to 100,
default 20) for the rows per page. Gifts that were reversed are not listed. There is
no "read" state on the server: keep the time of the newest row you showed and call
inboxUnread(items, lastSeenMs) from onLoaded for a badge on
your Inbox tab. refresh() loads the list again, for example after an
incoming gift. Without a component, client.getInbox({ cursor, limit })
returns the same page.
Web
const inbox = new GiftInbox(panel, {
client, pageSize: 20, resolveUsers,
onLoaded: (items) => setBadge(inboxUnread(items, lastSeenMs)),
});
inbox.refresh();
React Native
<GiftInbox
ref={inboxRef} client={client} pageSize={20} resolveUsers={resolveUsers}
onLoaded={(items) => setBadge(inboxUnread(items, lastSeenMs))}
/>
Flutter
GiftInbox(client: client, theme: theme)
Android (Compose)
GiftInbox(client = client, theme = theme)
iOS (SwiftUI)
GiftInbox(model: GiftInboxModel(client: client))
More options
Small things you can set on the tray or the effect layer without a section of their own.
| Option | What it does | Where |
|---|---|---|
noticeBottom |
How high the "ayla sent Rose" notice sits above the bottom edge (default 150).
Set it to the sheet's inset plus 16, from onInsetChange, so it
stays above the tray.
|
All five |
columns, gridMaxHeight,
defaultSelectedId, disabled
|
Grid columns (4 by default), the height of the fixed tray (sheet
set to false), the gift selected first, and turning sending off while your own
request runs.
|
All five (web: columns and the others on the tray options;
attributes on <gift-tray>)
|
origin, pictureWaitMs |
Where large gifts start flying from, and how long an effect waits for its picture (300 ms). | All five (on the effect layer) |
pictureScale |
Draws the gift picture bigger in a wide layer such as a tablet. Automatic by default (1 on a phone, up to 2). |
Web (and picture-scale on the element), Flutter, Android and iOS;
not React Native
|
remoteRecipes, recipesVersion, recipesUrl
|
The effect numbers (durations, sparks, scale) are fetched from our recipes file
so we can tune them without an app update. remoteRecipes set to
false makes no request at all; a version pins a file; a URL self-hosts it. Your
own thresholds and comboWindowMs always win.
|
All five |
autoCheck, focusOnShow |
Whether the away card checks by itself at start and on return to the app, and whether it moves keyboard or screen reader focus to the card when it appears. | All five |
refreshIntervalMs |
Polling interval of the leaderboard (0, no polling, by default). | All five |
Custom elements Web only
The same components as plain HTML tags, so a page without a framework can use them by writing attributes.
import "@giftingstack/sdk/elements" registers the tags (or call
defineGiftElements()). The client is a property, never an
attribute; so are theme, labels, resolveUsers and
options (what attributes cannot say, such as snaps or a render
function). Events bubble.
<gift-effect-layer id="fx" quality="low" combo-window-ms="3000"></gift-effect-layer>
<gift-tray receiver="mert" reference="post-42" effects="#fx" top-up
tray-title="Hediye gönder" send-label="Gönder" placement="auto"></gift-tray>
<gift-battle-bar side-colors="#e0457b,#3a8dff"></gift-battle-bar>
<script type="module">
import "@giftingstack/sdk/elements";
const tray = document.querySelector("gift-tray");
tray.client = client;
tray.theme = themeFromAccent("#ff4f9a", "dark");
tray.addEventListener("gift-topup", openCoinStore);
</script>
| Tag | Attributes | Events |
|---|---|---|
<gift-effect-layer> |
quality, reduced-motion, picture-scale,
notice-bottom, combo-window-ms,
remote-recipes, recipes-version,
recipes-url
|
gift-effect-end |
<gift-tray> |
receiver, reference, effects,
play-on, tray-title, coin-name,
send-label, not-enough-label,
get-coins-label, columns,
grid-max-height, default-selected-id,
disabled, sheet="false", placement,
anchor, initial-snap, top-up
|
gift-sent, gift-error, gift-state,
gift-topup, gift-open-change,
gift-snap-change, gift-placement-change,
gift-inset-change
|
<gift-wall> |
user, reference, max, order,
size, refresh-key, pressable
|
gift-chip-press, gift-error |
<gift-away-summary> |
effects, auto-check, ack-on,
max-effects, budget-ms, quality,
reduced-motion, focus-on-show, min-gifts
|
gift-away-show, gift-away-done,
gift-error
|
<gift-leaderboard> |
board, periods ("day,week,all"),
initial-period, metric, metric-toggle,
limit, highlight-user,
refresh-interval-ms, coin-name, pressable
|
gift-user-press, gift-error |
<gift-battle-bar> |
side-colors, tick-ms, reduced-motion
|
|
<gift-room> |
room, effects, min-tier,
skip-own, max-replay-age-ms
|
gift-room-event, gift-room-viewers,
gift-room-gap, gift-room-state
|
React Native, Flutter, Android and iOS have no custom elements: they use the components and widgets shown above.
What is on which platform
Consistent with the table on SDKs. "yes" means the same name or a close one in every snippet above.
Web RN Flutter Android iOS
Theme (accent, presets, tokens) yes yes yes yes yes
Tray as bottom sheet yes yes yes yes yes
Tray as popover / inline yes - - - -
Texts and translations yes yes yes yes yes
Reduced motion yes yes yes yes yes
Low-quality mode yes yes yes yes yes
Test mode badge yes yes yes yes yes
"Get coins" button yes yes yes yes yes
Combo window yes yes yes yes yes
Effect tiers (thresholds) yes yes yes yes yes
Render slots yes yes yes yes yes
Battle bar sides and labels yes yes yes yes yes
Away card: minGifts yes yes - - -
Gift inbox yes yes yes yes yes
Custom elements (tags) yes - - - -
Setup, tokens and the first snippet per platform are there too; the three ways to put the components in your app are on Integration patterns.
Next: Integration patterns, SDKs and Gift rooms.