@providame/react
v0.0.15
Published
Providame React bindings — headless hooks and prebuilt auth components.
Readme
@providame/react
React bindings for Providame — headless hooks (build your own UI) and prebuilt components (drop-in), both over the same @providame/core engine.
Building a native or hybrid app (React Native or Expo, or any app without a browser cookie jar)? Use a native key (
nk_) instead of your publishable key — native mode comes from@providame/coreand is configured on the client. See the Native apps guide.
Install
npm install @providame/react react react-domUsage
import { ProvidameProvider, SignIn } from '@providame/react'
function App() {
return (
<ProvidameProvider publishableKey="pk_test_...">
<SignIn onComplete={(session) => console.log(session)} signUpUrl="/sign-up" />
</ProvidameProvider>
)
}Modal vs. page usage
These components serve two contexts, and the footer link behaves differently in each:
- Page — you render
<SignIn>at your own/sign-inroute. PasssignUpUrland the footer renders a normal link that navigates. - Modal —
<SignInButton mode="modal">handles this for you: the footer switches to sign-up in place, because navigating would tear the modal down.
Rendering these inside your own modal? Pass the switch callback instead of a URL, and hold the view state yourself:
{view === "sign-in" ? (
<SignIn onSwitchToSignUp={() => setView("sign-up")} />
) : (
<SignUp onSwitchToSignIn={() => setView("sign-in")} />
)}The callback takes precedence when both it and a URL are supplied.
SignInButtonno longer acceptssignUpUrl(norSignUpButtonsignInUrl). They had no effect inmode="redirect"and produced a modal-destroying navigation inmode="modal". Pass those URLs to<SignIn>/<SignUp>directly when rendering them as pages.
Headless
import { useSignIn } from '@providame/react'
function CustomSignIn() {
const signIn = useSignIn()
const submit = () => signIn.signInWithPassword('[email protected]', 'password')
return <button onClick={submit}>Sign in</button>
}Beyond create()/attemptFirstFactor()/signInWithPassword(), useSignIn() also
exposes one call per passwordless starter, plus the second-factor and forced-enrollment
steps a password sign-in can land on too:
await signIn.signInWithPasskey(identifier, window.location.hostname)
await signIn.signInWithOTPEmail(identifier)
await signIn.resendOTPEmail()
await signIn.attemptFirstFactor({ strategy: 'otp_email', code })
await signIn.signInWithOTPSMS(identifier) // identifier is still email; the code texts to the phone on file
await signIn.resendOTPSMS()
await signIn.attemptFirstFactor({ strategy: 'otp_sms', code })
await signIn.signInWithMagicLink(identifier, 'https://myapp.com/auth/callback')
// Second factor / forced enrollment
await signIn.attemptSecondFactor({ strategy: 'totp', code })
await signIn.enrollTotp() // status "needs_mfa_enrollment" -> signIn.enrollment.{ secret, otpauthUri }
await signIn.verifyTotpEnrollment(code)
signIn.firstFactors // FactorKind[] the backend will accept next
signIn.secondFactors // FactorKind[]
signIn.recoveryCodes // string[] | null, populated once right after a mid-flow TOTP enrollmentComponent styles are injected automatically at runtime — no separate CSS import is needed.
(@providame/vue is the exception: it ships a real stylesheet you must import.)
Social login
import { useSignInIdp } from '@providame/react'
function GoogleButton() {
const { signIn } = useSignInIdp()
return <button onClick={() => signIn('google')}>Sign in with Google</button>
}useSignInIdp() also exposes providers (this environment's enabled social-login
providers, fetched on mount) and error (populated by a failed start request, or
by a post-redirect social-login error).
Sign-up
useSignUp() is symmetric to useSignIn(), including the same mid-flow TOTP
enrollment step — <SignUp> is built entirely on top of it:
import { useSignUp } from '@providame/react'
function CustomSignUp() {
const signUp = useSignUp()
async function submit() {
await signUp.create({ email, password, firstName, lastName, inviteToken, turnstileToken })
// status is now "needs_verification"
}
async function verify(code: string) {
await signUp.attemptVerification({ code })
}
// Forced MFA enrollment, once status is "needs_mfa_enrollment"
async function enroll(code: string) {
await signUp.enrollTotp() // -> signUp.enrollment.{ secret, otpauthUri }
await signUp.verifyTotpEnrollment(code) // completes the flow
}
}inviteToken is required when the environment's sign-up policy is invite_only
(read it off your own invite-accept page's URL/props); every other mode ignores
it if set. A waitlist-mode environment completes verification but returns no
session — check status === "waitlisted" rather than isComplete.
needsVerification is a convenience alias for status === "needs_verification".
Password reset
<SignIn>'s own "Forgot password?" link already drives this flow in place.
Render the standalone <ResetPassword> component to give it its own page/route
instead:
import { ResetPassword } from '@providame/react'
<ResetPassword signInUrl="/sign-in" onComplete={(session) => navigate('/dashboard')} />Headless, useResetPassword() is symmetric to useSignIn()/useSignUp():
import { useResetPassword } from '@providame/react'
function CustomResetPassword() {
const reset = useResetPassword()
await reset.create({ email }) // status -> "needs_verification"
await reset.resend()
await reset.attempt({ code, password }) // sets the new password, signs the user in
// Second factor / forced enrollment — same shape as useSignIn/useSignUp
await reset.attemptSecondFactor({ strategy: 'totp', code })
await reset.enrollTotp()
await reset.verifyTotpEnrollment(code)
reset.status // FlowStatus
reset.session // Session | null, once complete
reset.recoveryCodes // string[] | null
reset.reset() // back to "idle"
}Customizing text and logo
<SignIn>, <SignUp>, and <ResetPassword> all accept title, subtitle, and
logo to override the heading, subheading, and logo image — each falls back to
a branding-derived value (or a plain default) when omitted. <SignUp>
additionally accepts collectName (default true, shows first/last name
fields) and inviteToken (see Sign-up above).
<SignIn title="Welcome back" subtitle="Sign in to continue" logo="/logo.svg" />What's in the box
Components — SignIn, SignUp, ResetPassword, UserButton, UserProfile,
SignInButton, SignUpButton, SignOutButton, SignedIn, SignedOut,
Protect, RedirectToSignIn, OrganizationSwitcher, OrganizationProfile,
CreateOrganization.
Hooks — useAuth, useUser, useSession, useSignIn, useSignUp,
useSignInIdp, useResetPassword, useOrganization, useOrganizationList,
useBranding, usePasskeyRegistrationLink, useProvidame.
Hook return values are plain snapshots of the render that produced them. Checking flow state synchronously right after an
awaitinside a handler reads the stale value — react to it in auseEffectinstead. (This is the one place the React and Vue bindings genuinely differ: Vue's refs are live.)
Buttons & account menu
<SignInButton mode="modal" onComplete={(session) => console.log(session)} />
<SignUpButton mode="modal" onComplete={(session) => console.log(session)} />
<UserButton onSignOut={() => navigate('/')} />onComplete fires once the flow inside the modal finishes; mode="redirect"
navigates to signInUrl/signUpUrl instead and ignores it. <UserButton>'s
onSignOut fires after a successful sign-out.
Organizations
import {
OrganizationSwitcher, OrganizationProfile, CreateOrganization,
useOrganization, useOrganizationList,
} from '@providame/react'
// Reads token claims — makes no request
const { organization, roles, permissions, has } = useOrganization()
// Fetches GET /me/organizations
const list = useOrganizationList()
list.isLoaded // false until the first load settles (successfully or not)
list.organizations
<OrganizationSwitcher hideCreate personalLabel="Just me" onChange={(orgId) => console.log(orgId)} />
<OrganizationProfile organizationId={someOtherOrgId} onClose={() => setOpen(false)} />
<CreateOrganization onCreated={(orgId) => list.setActive(orgId)} onCancel={() => setOpen(false)} />useOrganization() covers the active organization only and never fetches —
the token already carries org_id/org_slug/org_roles/org_permissions.
useOrganizationList() does fetch, since the full membership list isn't in a
token; it also exposes setActive(orgId).
setActive() is remembered across sign-ins, so you don't call it on every login: a returning user is put back in the organization they last used, a user who has never chosen starts in the one they joined first, and clearing it sticks too.
<OrganizationSwitcher>'s hideCreate drops the "Create organization" action;
personalLabel/hidePersonal customize or remove the no-organization option.
<OrganizationProfile> manages the active organization by default, or a
specific one via organizationId; onClose fires when its "Done" button is
clicked. It gates each action on the caller's own org:sys_* permissions
rather than a role name, so it works with roles you define yourself.
<CreateOrganization>'s onCreated/onCancel are the only way to react to a
successful create or cancel — there's no default navigation.
Gating UI
<Protect role="admin">...</Protect>
<Protect orgPermission="org:sys_memberships:manage">...</Protect>
<Protect role="admin" orgRole="org:admin">...</Protect> {/* both must pass */}role/permission gate on environment-level grants; orgRole/orgPermission
gate on the active organization. Omitted props are not constraints. This is UI
gating only — real enforcement is your backend verifying the token, via
@providame/backend.
Object-scoped access
<Protect> reads claims already in the token — synchronous, free, never fails.
<Can> asks a question about one specific resource — a network call, so it
needs a loading state and can fail:
<Can
permission="edit"
resource="document:42"
context={{ status: document.status }}
loading={<Spinner />}
fallback={<p>You can't edit this document.</p>}
>
<button>Edit</button>
</Can>Use <Protect role="admin"> for "is this user an admin"; use <Can permission="edit" resource="document:42">
for "can this user edit this document". <Can> fails closed — a network
error renders fallback, never children. The optional context prop passes
extra data an authorization condition can evaluate against — omit it when the
permission doesn't need one. useCan({ permission, resource, context }) is
the headless equivalent, returning { allowed, isLoading, error }.
Answers are cached per-question for the life of the session (cleared on
sign-in/out and organization switch) — mounting <Can> for the same resource
twice on one page costs one request, not two.
Theming
Every component accepts an appearance prop:
<SignIn appearance={{ variables: { colorPrimary: '#111827' } }} />Components also fetch your environment's configured branding (colors, logo, light and dark) on mount and apply it automatically.
Branding & the shared client
Every prebuilt component already calls useBranding() on mount — reach for it
directly only if you're building fully custom UI and want the same
colors/logo. The underlying fetch is memoized on the client, so mounting
several components on one page still costs a single request. useProvidame()
returns the shared client instance itself — the same one every hook above
uses internally — for anything not covered by a hook:
import { useBranding, useProvidame } from '@providame/react'
function CustomHeader() {
const branding = useBranding() // { logoUrl, primaryColor, appName, ... }
const providame = useProvidame() // the shared client every hook above uses internally
return <img src={branding.logoUrl} alt={branding.appName} />
}Verifying tokens & errors
verifyToken and ProvidameError are re-exported from @providame/core for
convenience — the full reference for server-side verification is
@providame/backend, which wraps the same function.
// On your own backend — never in the browser.
import { verifyToken } from '@providame/react'
const { userId, claims } = await verifyToken(accessToken, {
jwksUri: config.jwksUri,
issuer: config.issuer,
audience: config.projectId,
})Every hook's error field above is a ProvidameError: code is the backend's
error code, status the HTTP status, and fields a per-field validation map
when isValidationError is true. isInvalidCredentials is true for a
rejected credential/factor (a 401, or the invalid_credentials code):
if (signIn.error?.isInvalidCredentials) {
// a rejected credential/factor — a 401, or code "invalid_credentials"
} else if (signIn.error?.isValidationError) {
signIn.error.fields // per-field validation messages
}Docs
Full reference: /docs/sdk/react in your Providame dashboard.
Build
npm run build