Sign In CTA

Components that support authentication-gated content accept userIsLoggedIn and signInCTA props. When the user is not logged in and the entity requires authentication (authRequirement: "REGISTERED" on the backend), the component renders a sign-in screen instead of the main flow.

You — the host application — own the actual sign-in logic. The component only renders the call-to-action.

SignInCTADetails

interface SignInCTADetails {
  defaultLabel?: string;
  onClick?: () => void;
  url?: string | null;
  target?: LinkTargetType; // "_blank" | "_self" | "_parent" | "_top"
  component?: React.ReactElement | null;
  gate?: SignInGateConfig;
}
FieldDescription
defaultLabelButton label. Required unless component is provided.
onClickClick handler. Use for in-app modals or programmatic sign-in.
urlSign-in URL. Use for redirect-based sign-in flows.
targetAnchor target. Defaults to _self.
componentCustom React element to render in place of the default button.
gateDefers the gate so the user plays first. Omit for the sign-in screen in front of the game.

Priority order

Only one rendering strategy is active at a time, picked in this order:

  1. component — if provided, the custom React element is rendered.
  2. onClick — if provided (and no component), a button is rendered that calls the handler.
  3. url — if provided (and no component/onClick), a button is rendered that navigates.
  4. None of the above — a disabled button is shown.

Example — onClick handler

import { SignInCTADetails } from "fansunited-frontend-core";

const signInCTA: SignInCTADetails = {
  defaultLabel: "Sign In",
  onClick: () => openSignInModal(),
};

<ClassicQuizPlay
  {...otherProps}
  userIsLoggedIn={isLoggedIn}
  signInCTA={signInCTA}
/>

Example — URL redirect

const signInCTA: SignInCTADetails = {
  defaultLabel: "Login",
  url: "https://your-auth.example.com/login",
  target: "_blank",
};

Example — fully custom component

const signInCTA: SignInCTADetails = {
  component: <MyBrandedSignInButton onSignIn={handleSignIn} />,
};

Behavior

  • When userIsLoggedIn is false and the entity requires authentication, the sign-in screen replaces the main flow.
  • When userIsLoggedIn is true, the sign-in CTA is never shown.
  • When the entity does not require authentication, the sign-in CTA is never shown — even if userIsLoggedIn is false.

Gate position

By default a game with authRequirement: "REGISTERED" shows the sign-in screen instead of the
game — the user never gets to a single step. gate moves that ask later, so they can start
playing and are asked once they have something to lose.

interface SignInGateConfig {
  position?: "before" | "step" | "after"; // defaults to "before"
  afterStep?: number;                     // required by "step", 1-based
  labels?: { title?: string; description?: string }; // "step" and "after"
}
const signInCTA: SignInCTADetails = {
  defaultLabel: "Sign in to continue",
  url: "https://your-auth.example.com/login",
  gate: { position: "step", afterStep: 3 },
};
positionWhen the gate opensHow it looks
"before" (default)In front of the game. The historical behaviour.The standalone sign-in screen, with its own copy.
"step"Once afterStep steps have been completed.In the content area, keeping the game's card, image and branding. Takes labels.
"after"Once every step is complete, before the participation is submitted and the result shown.Same content-area treatment as "step", so the copy can reassure the user their progress is saved. Takes labels.

Resolution rules — the safe default is always the current behaviour:

  • "step" without a usable numeric afterStep falls back to "before" and logs a warning.
  • afterStep is clamped to at least 1; at or past the last step it behaves as "after".
  • A gate on a CTA with no component, onClick or url falls back to "before" and logs a
    warning. Such a CTA renders as a disabled button, which is a dead end in front of the game but
    would strand a user who is already part-way through.
  • gate only changes when an existing requirement is enforced. An entity whose
    authRequirement is not "REGISTERED" is never gated, whatever gate says.

gate is read by ClassicQuizPlay, MatchQuizPlay and PersonalityQuizPlay. What a "step"
counts differs per component: questions for the two quizzes, markets for the match quiz game
(whose "after" gates the Play button on the summary step).

PollVote does not use gate at all. A poll is a single action, so instead of a screen it
always renders its options to everyone — locked, with the sign-in CTA in the vote button's
place. Signing in unlocks the options and restores the vote button. Nothing is selectable while
locked, so there is no draft to restore.

Telling the component the user signed in

Keep doing exactly what you do today: flip userIsLoggedIn to true. That is the primary
path. The component treats the false → true transition as the gate being satisfied, closes it, and
carries straight on — one click for the user, no extra confirmation step. The gate follows the
prop, so setting it back to false re-gates, and a sign-out mid-flow behaves the same as it does
on the up-front screen.

Your setupWhat happensWhat you must do
onClick opens an in-page modalThe component stays mounted with the input in memory.Flip userIsLoggedIn to true. The gate closes and play continues at the next step.
url redirect with target: "_self"The page unloads and the component remounts from scratch.Render with userIsLoggedIn: true on the way back. The input is restored automatically.
url / onClick with target: "_blank"The component stays mounted but no prop changes on its own.Flip userIsLoggedIn to true once the popup completes.

The component never probes your auth state — userIsLoggedIn is the only signal, exactly as with
the up-front screen. Until it flips, the gate stays up.

The SDK needs no re-instantiation in any case: it reads the token through your
authProvider.getIdToken() callback on each request.

Resuming after a redirect

A deferred gate keeps the unsubmitted answers in sessionStorage under
fu:draft:<component>:<entityId> (classic-quiz, match-quiz) for the duration of the sign-in hop. That is what makes
deferring safe, so it is not configurable — a client that wants no storage write leaves gate
unset.

  • It survives a same-tab redirect and is cleared when the participation is submitted, when the
    user plays again, or when the tab closes.
  • It is discarded if the entity's questions or markets changed while the user was away, or after 24 hours.
  • Closing the tab part-way therefore starts clean; the draft is not a long-term save.
  • A countdown/timed entity's clock does not run during the redirect. If yours are timed,
    prefer an onClick modal so the component is never unmounted.

Consents ticked before the gate are held back and recorded once the user has a profile —
posting them anonymously would silently fail.

Supported components

ClassicQuizPlay, PollVote, PersonalityQuizPlay, MatchQuizPlay, EventGamePlay, EitherOrPlay, Predictor. The Discussion component uses a simpler onSignInClick: () => void callback instead (see its page).


Did this page help you?