Headless SDK: challenges
Wire a multi-step challenge into your own UI in under 50 lines. The SDK handles state, polling, optimistic updates, and reward claim. You write the markup.
Key takeaways
Quick read- useChallenge returns the active challenge, objectiveProgress, progressPercentage, and refresh().
- Objective state polls every 30s (tunable). After emitting the qualifying event, call refresh() to update immediately.
- Completion is computed server-side: compare completedObjectives === totalObjectives, then hand off to your reward UI.
- Always render the full path from the start. Hidden objectives kill completion rate.
- Use useChallenge inside a single screen. For shared dashboards, use useChallenges (plural) with status + limit.
Quickstart
A working challenge in 40 lines
"use client";
import { useChallenge } from "@bricqs/sdk-react";
export function OnboardingChallenge() {
// engagementId inherited from <BricqsProvider>; resolves the active challenge.
const {
challenge,
objectiveProgress,
completedObjectives,
totalObjectives,
progressPercentage,
isLoading,
error,
} = useChallenge();
if (isLoading) return <div className="animate-pulse h-12" />;
if (error || !challenge) return <p>Could not load the challenge.</p>;
const isCompleted = totalObjectives > 0 && completedObjectives === totalObjectives;
return (
<section>
<header className="flex items-baseline justify-between mb-4">
<h2 className="font-bold">{challenge.name}</h2>
<span className="text-sm text-slate-500">{progressPercentage}% done</span>
</header>
<ol className="space-y-2">
{objectiveProgress.map((o) => (
<li
key={o.objectiveId}
className={`flex items-center gap-3 ${o.completed ? "opacity-60" : ""}`}
>
<span>{o.completed ? "✓" : "○"}</span>
<span>{o.objectiveName}</span>
</li>
))}
</ol>
{isCompleted && (
<p className="mt-4 text-green-700 font-semibold">Challenge complete</p>
)}
</section>
);
}Anatomy
What the hook returns
Three concepts cover almost every UI you will build.
API
GET /gamify/challenges/{id}/progress returns objectives, progress, and rank. The hook polls this every 30 seconds (tunable).
SDK
useChallenge() gives you the same shape plus loading/error state and a refresh(), inheriting engagementId from the provider.
User sees
A live progress bar, a checklist that advances as the user acts, and a completion moment that hands off to your reward UI.
Advancing objectives
Emit the event, then refresh
Challenge objectives advance from events. Emit the qualifying event (server-side is safest), then call refresh() so the UI updates without waiting for the next poll.
"use client";
import { useChallenge } from "@bricqs/sdk-react";
export function CompleteProfileStep() {
const { refresh } = useChallenge(); // engagementId from provider
async function onSave(profile: ProfileForm) {
await saveProfileToYourBackend(profile);
// Your backend emits the qualifying event with a deterministic idempotency
// key (POST /gamify/events). The activity_count objective advances server-side.
await fetch("/api/complete-profile-step", { method: "POST" });
// Pull the new state now instead of waiting for the 30s poll.
await refresh();
}
return <ProfileForm onSubmit={onSave} />;
}Emit from the server with an Idempotency-Key so double-clicks never double-credit; refresh() just re-reads the canonical state.
Multiple challenges
Listing active challenges
On dashboards, render every active challenge with one hook.
"use client";
import { useChallenges } from "@bricqs/sdk-react";
export function ChallengeDashboard() {
// engagementId inherited from <BricqsProvider>
const { challenges, isLoading } = useChallenges({
status: "active",
limit: 5,
});
if (isLoading) return <Skeleton count={3} />;
if (challenges.length === 0) return <Empty />;
return (
<ul className="grid gap-4">
{challenges.map((c) => (
<li key={c.id} className="rounded-xl border p-5">
<h3 className="font-bold mb-1">{c.name}</h3>
<p className="text-sm text-slate-500 mb-3">
{c.objectives.length} objectives · ends {c.endDate ?? "open"}
</p>
{/* Per-challenge progress: read it with useChallenge(c.id) in a child. */}
</li>
))}
</ul>
);
}Completion handoff
Handle the success moment cleanly
onCompleted(async (event) => {
// event = {
// challengeId,
// completedAt,
// reward: { id, type, value, claim_url? },
// facts: [...]
// }
// 1. Show your celebration UI
setShowConfetti(true);
// 2. Auto-claim the reward (or route to a claim screen)
if (event.reward.type === "coupon") {
const code = await claimReward(event.reward.id);
setCouponCode(code);
}
// 3. Fire analytics
analytics.track("challenge_completed", {
challenge_id: event.challengeId,
reward_id: event.reward.id,
});
});The completion event fires once per challenge per participant. The SDK guarantees this even on poll retries.
Common mistakes
What goes wrong
Emitting the qualifying event without an idempotency key. Double-clicks double-credit the objective.
Always pass Idempotency-Key on POST /gamify/events. Use a deterministic value (participant + action + period).
Hiding objectives that are not yet active. The user feels lost.
Render every objective from the start, marked as upcoming. Hidden steps tank completion rate.
Firing completion side-effects from a component that re-renders. They fire repeatedly.
Guard with a useEffect keyed on the completed flag (run-once), or subscribe to the live stream via useBricqsStream and react to the challenge.completed.v1 event exactly once per event id.
Using useChallenge for non-challenge state (points, tier).
Use the dedicated hooks: usePoints, useTier, useStreak. They are smaller, faster, and shaped for their purpose.
Polling unnecessarily on screens where the user is unlikely to act.
Pass pollInterval={0} or unmount the hook when off-screen. The default 15-second poll is fine for active surfaces only.
Developer FAQ
Common questions when integrating gamification with Bricqs.
Ready to ship?
Wire it up with the Bricqs SDK or API
Headless SDK for React UIs, REST API for any backend. Same engine behind both.
