Callbacks

Most play components accept a callbacks prop — an object of optional functions the host application can register to observe what happens inside the component without owning the rendering. Use it for analytics, host-side state hydration, or kicking off follow-up flows when a user finishes / shares.

Callbacks are fire-and-forget observers — they never block rendering, never receive a return value, and never replace built-in behavior. To override UX (e.g. replace the share button), use the matching CTA prop (shareCTA, additionalCTA, etc.) instead.

Shape per component

Each component exposes a typed callbacks object. The shape is consistent — only the payload type differs based on the component's underlying SDK model.

// ClassicQuizPlay
interface ClassicQuizPlayCallbacks {
  onFinish?: (summary: ClassicQuizUserParticipationModel) => void;
  onShare?: (summary: ClassicQuizUserParticipationModel) => void;
}

// PollVote
interface PollVoteCallbacks {
  onFinish?: (vote: SinglePollVoteModel) => void;
}

// PersonalityQuizPlay
interface PersonalityQuizPlayCallbacks {
  onFinish?: (summary: PersonalityQuizParticipationModel) => void;
}

// MatchQuizPlay
interface MatchQuizPlayCallbacks {
  onFinish?: (prediction: PredictionResponseModel) => void;
}

// EventGamePlay
interface EventGamePlayCallbacks {
  onFinish?: (prediction: EventGamePredictionModel) => void;
}

// EitherOrPlay
interface EitherOrPlayCallbacks {
  onFinish?: (participation: EitherOrParticipationModel) => void;
}

onFinish

Fires when the user completes a participation in the current session — i.e. their submit / vote / prediction call has just succeeded.

The full SDK participation model (score, picks, points, prediction details, etc.) is passed to the callback so the host can react without re-fetching.

ComponentFires when
ClassicQuizPlayQuiz submitted and summary returned.
PollVoteVote submitted successfully.
PersonalityQuizPlayParticipation submitted and persona match returned.
MatchQuizPlayPrediction successfully submitted via sdk.matchQuiz.play.
EventGamePlayPrediction successfully submitted via sdk.eventGame.makePrediction / updatePrediction.
EitherOrPlayGame reaches the results state (lives exhausted or all pairs played).

onFinish does not fire when the component loads onto an already-finished state (e.g. revisiting a settled game, opening a quiz where max attempts are already reached). It fires only on a fresh participation event in the current session.

Example

import { ClassicQuizPlay } from "fansunited-frontend-components";

<ClassicQuizPlay
  {...otherProps}
  callbacks={{
    onFinish: (summary) => {
      analytics.track("quiz_completed", {
        quizId: summary.classicQuizId,
        correct: summary.correct,
        total: summary.classicQuizQuestions,
      });
    },
  }}
/>

onShare

ClassicQuizPlay only. Fires when the user clicks the default share button — i.e. when no shareCTA override is provided and the built-in navigator.share / Android bridge / clipboard fallback runs.

If you override the share button via shareCTA.onClick / shareCTA.url / shareCTA.component, your own handler is the source of truth — onShare does not fire, because you're already in control. See Share CTA for the override path.

<ClassicQuizPlay
  {...otherProps}
  callbacks={{
    onShare: (summary) => {
      analytics.track("quiz_shared", { quizId: summary.classicQuizId });
    },
  }}
/>

Payload models

Each callback receives the SDK participation model as its only argument. The shapes are defined in fansunited-sdk-esm and listed below for reference. Import the type when you need it:

import type {
  ClassicQuizUserParticipationModel,
  SinglePollVoteModel,
  PersonalityQuizParticipationModel,
  PredictionResponseModel,
  EventGamePredictionModel,
  EitherOrParticipationModel,
} from "fansunited-sdk-esm";

ClassicQuizUserParticipationModel

Passed to ClassicQuizPlay's onFinish and onShare.

FieldTypeDescription
classicQuizIdstringQuiz entity id.
classicQuizModelClassicQuizBasicModelSnapshot of the quiz at participation time (title, type, points config, branding, etc.).
correctnumberNumber of correct answers in this attempt.
classicQuizQuestionsnumberTotal number of questions in the quiz.
personalBestnumberThe user's best score across all their attempts.
pointsnumberPoints awarded for this attempt.
answersClassicQuizParticipationQuestionModel[]Per-question breakdown: { questionId, optionId, correct, correctOptionId }.
attemptsnumberHow many times the user has played this quiz.

SinglePollVoteModel

Passed to PollVote's onFinish. Extends PollVoteBasicModel.

FieldTypeDescription
idstringVote id.
pollIdstringPoll entity id.
optionIdstringSingle-choice selection.
optionIdsstring[]All selected option ids (multi-choice polls).
userIdstringVoter's profile id.
createdAtstringISO timestamp of the vote.
previousVotesPreviousPollVoteModel[]The user's earlier votes on the same poll (one entry per attempt).
attemptsnumber | nullTotal number of votes the user has cast on this poll.

PersonalityQuizParticipationModel

Passed to PersonalityQuizPlay's onFinish.

FieldTypeDescription
personalityQuizIdstringQuiz entity id.
userIdstringPlayer's profile id.
questionsPersonalityQuizParticipationQuestionModel[]Per-question selections: { questionId, optionId }.
personasPersonalityQuizPersonaPercentsModel[]Match scores per persona: { personaId, percent }. The persona with the highest percent is the user's primary result.
attemptsnumberHow many times the user has played this quiz.

PredictionResponseModel

Passed to MatchQuizPlay's onFinish.

FieldTypeDescription
idstringPrediction id.
gameInstanceIdstringMatch quiz entity id.
gameTypestringGame type identifier.
userIdstringPlayer's profile id.
fixturesFixturesResponseModel[]Per-fixture prediction + result: { market, matchId, matchType, matchModel, prediction, result }.
totalFixturesnumberNumber of fixtures in the quiz.
settledFixturesnumberNumber of fixtures already settled (resolved by the backend).
statusstringPrediction status (active / settled / partial / cancelled, etc.).
wagernumberWager amount (if applicable).
tiebreakerTiebreakerModelTiebreaker selection.
pointsnumberTotal points earned.
templateIdstringTemplate id.
groupIdstringGroup id.
profileLastUpdatedAtstringISO timestamp.
createdAtstringISO timestamp the prediction was first submitted.
updatedAtstringISO timestamp of the latest edit.

At submit time fixtures[].result is empty — results are resolved server-side once the underlying matches finish.

EventGamePredictionModel

Passed to EventGamePlay's onFinish.

FieldTypeDescription
idstringPrediction id.
gameIdstringEvent game entity id.
profileIdstringPlayer's profile id.
profileModelProfileModel | nullSnapshot of the player's profile.
statusEventGamePredictionStatusEnumACTIVE | WON | LOST | PARTIALLY_WON | CANCELED.
fixturesEventGamePredictionFixturesModel[]Per-fixture prediction: { id, predictionType, prediction, correct, points }.
tiebreakerEventGameTiebreakerTiebreaker selection.
gamePointsnumber | nullPoints from fixture predictions.
bonusPointsnumber | nullBonus points.
totalPointsnumber | nullgamePoints + bonusPoints.
positionnumber | nullLeaderboard position (post-settlement).
createdAtstringISO timestamp the prediction was first submitted.
updatedAtstringISO timestamp of the latest edit.

At submit time status is ACTIVE and fixtures[].correct / points are unresolved. Settlement values appear after the underlying event is settled.

EitherOrParticipationModel

Passed to EitherOrPlay's onFinish.

FieldTypeDescription
instanceIdstringParticipation instance id.
eitherOrIdstringGame entity id.
gameStartedAtstringISO timestamp the participation started.
currentStepnumberIndex of the most recently played pair.
currentStreaknumberCurrent correct-answer streak.
remainingStepsnumberPairs left to play. At onFinish this is 0 (all pairs played) or the game ended early via lives exhaustion.
currentLivesnumberLives left. At onFinish this is 0 if the game ended via lives exhaustion.
currentPointsnumberPoints accumulated in this run.
personalBestnumberThe user's best score across all their participations.
participationCountnumberTotal number of times the user has started this game.
finishedParticipationsCountnumberOf those, how many were completed.
stepsEitherOrParticipationStepsModel[]Per-pair detail: { pairId, userSelection, correct, played, timestamp, expired, optionOne, optionTwo }.

Composition

callbacks is independent of every other prop. You can combine it with CTA overrides, lead capture, theming, etc. on the same component:

<ClassicQuizPlay
  {...otherProps}
  shareCTA={{ onClick: openCustomShareModal }}  // override UX
  callbacks={{
    onFinish: (summary) => analytics.track("quiz_completed", summary),
    // onShare omitted — shareCTA.onClick handles its own analytics
  }}
/>

Supported components

ClassicQuizPlay, PollVote, PersonalityQuizPlay, MatchQuizPlay, EventGamePlay, EitherOrPlay.