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;
}| Field | Description |
|---|---|
defaultLabel | Button label. Required unless component is provided. |
onClick | Click handler. Use for in-app modals or programmatic sign-in. |
url | Sign-in URL. Use for redirect-based sign-in flows. |
target | Anchor target. Defaults to _self. |
component | Custom React element to render in place of the default button. |
gate | Defers 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:
component— if provided, the custom React element is rendered.onClick— if provided (and nocomponent), a button is rendered that calls the handler.url— if provided (and nocomponent/onClick), a button is rendered that navigates.- 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
userIsLoggedInisfalseand the entity requires authentication, the sign-in screen replaces the main flow. - When
userIsLoggedInistrue, the sign-in CTA is never shown. - When the entity does not require authentication, the sign-in CTA is never shown — even if
userIsLoggedInisfalse.
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 },
};position | When the gate opens | How 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 numericafterStepfalls back to"before"and logs a warning.afterStepis clamped to at least1; at or past the last step it behaves as"after".- A
gateon a CTA with nocomponent,onClickorurlfalls 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. gateonly changes when an existing requirement is enforced. An entity whose
authRequirementis not"REGISTERED"is never gated, whatevergatesays.
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 setup | What happens | What you must do |
|---|---|---|
onClick opens an in-page modal | The 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/timedentity's clock does not run during the redirect. If yours are timed,
prefer anonClickmodal 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).
Updated 10 days ago
