Docs
Integration patterns
You decide how gifts show up in your app. The SDK gives you building blocks, not a flow. Pick one of three patterns per screen, and change your mind later without touching the server. The examples use "While you were away" (the gifts that arrived while the person was gone); the wall and the leaderboard work the same way. Every pattern has code for web, React Native, Flutter, Android (Compose) and iOS (SwiftUI). To change colours, texts or placement, see Customize the SDK.
1. SDK card
One line. We draw it and run it.
Fastest. Looks like GiftingStack, themed to your colors.
2. Badge and your own box
Our logic, your widgets.
A badge on your inbox and a list you style yourself. No popup.
3. Fully your own design
Our shell, your inside.
Keep the component, draw what is inside it with a render function.
Which one?
Start with 1, move down only when you must.
Each step gives you more control and costs you more code.
Setup once
Every snippet below uses these. Create the client once in your app. The names
screen, panel, badge, openInbox,
content and myBackend are placeholders for your own elements,
views and functions; the token endpoint is the one from the
Quickstart. Pick your platform in any of the tab
rows: the choice is remembered and switches every row on this page. Without JavaScript all
five are shown, one under the other.
Web
import { AwayController, createGiftingClient } from "@giftingstack/sdk";
import { GiftAwaySummary, GiftEffectLayer, GiftLeaderboard } from "@giftingstack/sdk/web";
const client = createGiftingClient({
getToken: async () => (await fetch("/api/gift-token")).text(),
});
const screen = document.querySelector("#screen"); // your container element
const panel = document.querySelector("#panel"); // another one, for the leaderboard
const badge = document.querySelector("#badge"); // your inbox badge
const effects = new GiftEffectLayer(screen); // plays the gift effects
React Native
import { useRef } from "react";
import { createGiftingClient } from "@giftingstack/sdk";
import {
GiftAwaySummary,
GiftEffectLayer,
GiftLeaderboard,
GiftWall,
useGiftAway,
type GiftEffectLayerHandle,
} from "@giftingstack/sdk/react-native";
const client = createGiftingClient({
getToken: async () => (await fetch("https://your.app/api/gift-token")).text(),
});
// In your screen component: one effect layer, rendered once over the screen.
const effects = useRef<GiftEffectLayerHandle>(null);
// ... <GiftEffectLayer ref={effects} />
Flutter
import 'package:giftingstack/giftingstack.dart';
final client = GiftingClient(
getToken: ({required bool forceRefresh}) =>
myBackend.giftingToken(forceRefresh: forceRefresh),
);
final effects = GiftEffectController();
// Once, last child of your Stack, so it draws over everything:
// Positioned.fill(child: GiftEffectLayer(controller: effects))
Android (Compose)
val client = GiftingClient(getToken = { forceRefresh -> myBackend.giftingToken(forceRefresh) })
@Composable
fun Screen() {
val effects = rememberGiftEffectLayerState()
Box(Modifier.fillMaxSize()) {
/* your screen, and the snippets below */
GiftEffectLayer(effects) // once, on top
}
}
iOS (SwiftUI)
import GiftingStack
import SwiftUI
let client = GiftingClient(getToken: { forceRefresh in
try await MyBackend.shared.giftingToken(forceRefresh: forceRefresh)
})
// In your view:
// @StateObject private var effects = GiftEffectController()
// and once, last in the ZStack:
// GiftEffectLayer(controller: effects).ignoresSafeArea()
When to choose which
-
SDK card: you want it working today, and a themed card at app open fits
your app. On web and React Native use
minGiftsto keep small summaries quiet. - Badge and your own box: a popup does not fit your app, the data belongs in your inbox, or your design system already has the list and badge components. You decide when the person has seen it.
-
Fully your own design: the card is right but its inside is not. The SDK
keeps loading, names, ordering, focus, accessibility, the effect replay and the
acknowledgement; you draw the body (
renderSummary), each wall chip (renderChip) or each leaderboard row (renderRow).
If none fits, use the headless controllers (AwayController,
LeaderboardController, GiftWallLoader) or the API directly. They
are the same logic the components run on, on all five platforms. The look of the ready
components (colours, texts, placement, motion) is covered in
Customize the SDK.
1. SDK card
Web
new GiftAwaySummary(screen, { client, effects, minGifts: 3 });
new GiftLeaderboard(panel, { client, board: "senders", limit: 10 });
React Native
<GiftAwaySummary client={client} effects={effects} minGifts={3} />
<GiftLeaderboard client={client} board="senders" limit={10} />
Flutter
Stack(children: [ YourScreen(), GiftAwaySummary(client: client, effects: effects), // fills the Stack; only the card takes touches Positioned.fill(child: GiftEffectLayer(controller: effects)), ]); // A screen of its own: GiftLeaderboard(client: client, board: LeaderboardBoard.senders, limit: 10)
There is no minGifts on Flutter: the card shows for any summary. Use
pattern 2 if you need a minimum.
Android (Compose)
Box(Modifier.fillMaxSize()) {
/* your screen */
GiftAwaySummary(client, effects = effects)
GiftEffectLayer(effects) // above the card, so the replay shows over it
}
// A screen of its own:
GiftLeaderboard(client, board = LeaderboardBoard.Senders, limit = 10)
There is no minGifts on Android: the card shows for any summary. Use
pattern 2 if you need a minimum.
iOS (SwiftUI)
@StateObject private var away = GiftAwayModel(client: client)
@StateObject private var board = GiftLeaderboardModel(client: client, board: .senders, limit: 10)
ZStack {
content
GiftAwaySummary(model: away)
GiftEffectLayer(controller: effects)
}
.onAppear { away.effects = effects }
// A screen of its own (wrap it in a ScrollView):
ScrollView { GiftLeaderboard(model: board) }
There is no minGifts on iOS: the card shows for any summary. Use pattern
2 if you need a minimum.
The component checks, shows, replays the gifts and acknowledges. On web and React Native a
summary below minGifts does not show, but it is still acknowledged, so it
does not come back. On Flutter, Android and iOS the card shows for any summary; if you
need a minimum, use pattern 2 and decide yourself.
2. Badge and your own box
Use the controller with ackOn set to manual and never start the card. It
stops at ready with the summary (total_gifts,
groups, top_senders; totalGifts and so on in Dart,
Kotlin and Swift). You draw the badge and the list, and acknowledge when the person opens
the inbox. On iOS the badge reads the model's state; on Android the controller needs a
coroutine scope (viewModelScope or a Compose scope).
Web
const away = new AwayController({ client, ackOn: "manual" });
away.subscribe(() => {
const { status, summary } = away.getState();
badge.hidden = status !== "ready";
badge.textContent = String(summary?.total_gifts ?? 0);
});
void away.check();
badge.onclick = () => {
openInbox();
void away.ack();
away.dismiss();
};
React Native
function InboxBadge() {
const { state, ack, dismiss } = useGiftAway({ client, ackOn: "manual" });
if (state.status !== "ready" || !state.summary) return null;
return (
<Pressable onPress={() => { openInbox(); void ack(); dismiss(); }}>
<Text>{state.summary.total_gifts}</Text>
</Pressable>
);
}
Flutter
final away = AwayController(
AwayControllerOptions(client: client, ackOn: AwayAckOn.manual),
);
away.subscribe(() {
final s = away.state; // s.status: idle, loading, empty, ready, ...
badge.value = s.status == AwayStatus.ready ? s.summary?.totalGifts ?? 0 : 0; // your ValueNotifier<int>
});
await away.check(); // at app open
// When the person opens your inbox:
openInbox();
await away.ack();
away.dismiss();
Android (Compose)
val away = AwayController(
AwayControllerOptions(client = client, ackOn = AwayAckOn.Manual),
scope = viewModelScope,
)
// away.state is a StateFlow<AwayState>: show the badge while status == AwayStatus.Ready.
val badgeCount = away.state.map { if (it.status == AwayStatus.Ready) it.summary?.totalGifts ?: 0L else 0L }
away.check() // at app open
// When the person opens your inbox:
openInbox()
away.ack()
away.dismiss()
iOS (SwiftUI)
@StateObject private var away = GiftAwayModel(client: client, autoCheck: false, ackOn: .manual)
// body: a badge of your own, no card is drawn
InboxBadge(count: away.state.status == .ready ? away.state.summary?.totalGifts ?? 0 : 0)
.onAppear { away.check() }
.onTapGesture {
openInbox()
away.controller.ack()
away.dismiss()
}
3. Fully your own design
Pass a render function. Return nothing and the default is drawn, so you can customise some
parts and leave the rest. On the web a function that throws falls back to the default and
reports to onError. The web hook returns a DOM node or a string (a string is
text, never HTML); React Native returns a node, Flutter a widget, Android a composable
lambda, and iOS an AnyView? (return nil to keep the default).
Web
new GiftAwaySummary(screen, {
client,
effects,
renderSummary: ({ headline, senders, dismiss }) => {
const box = document.createElement("div");
const p = document.createElement("p");
p.textContent = `${headline}. Top: ${senders[0]?.name ?? "-"}`;
const close = document.createElement("button");
close.textContent = "OK";
close.onclick = dismiss;
box.append(p, close);
return box;
},
});
new GiftLeaderboard(panel, {
client,
renderRow: ({ rank, name, scoreText }) => `#${rank} ${name} (${scoreText})`,
});
React Native
<GiftAwaySummary
client={client}
effects={effects}
renderSummary={({ headline, dismiss }) => (
<Pressable onPress={dismiss}><Text>{headline}</Text></Pressable>
)}
/>
<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>} />
Flutter
GiftAwaySummary(
client: client,
effects: effects,
// A tap on your own card does not dismiss it: call s.dismiss.
renderSummary: (context, s) => MyCard(s.headline, onClose: s.dismiss),
);
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), // null: the default row
);
Android (Compose)
GiftAwaySummary(client, effects = effects,
renderSummary = { s -> MyCard(s.headline, onClose = s.dismiss) }) // a tap does not dismiss; no close button is drawn
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) })
iOS (SwiftUI)
GiftAwaySummary(model: away,
renderSummary: { s in AnyView(MyCard(s.headline, close: s.dismiss)) })
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)) }) // nil: the default row
With your own body, a tap on the card does not dismiss it: put a button that calls
dismiss() in it (on Android and Flutter no close button is drawn either). The
three hooks exist on all five platforms with the same names: renderSummary,
renderRow and renderChip. The +37 overflow chip and
the pinned "You" row of the leaderboard stay the default.
See all three, live
The GiftingStack demo app has a hidden developer menu (long-press the logo for one second). Its "Example integration" row switches between Card, Badge + box and Custom, so you can see each pattern on the same data. Open the web version or the React Native version (on react-native-web). The demo uses a pretend client; nothing is sent to a server.
More: API or SDK? and Quickstart: send your first gift.