shad-auth-react
v0.4.0
Published
«ورود با شاد» برای برنامههای React — Login with Shad, React client
Readme
shad-auth-react
«ورود با شاد» for React apps — the client half of the reverse SSO flow, where a user starts on your app and signs in with their Shad account instead of being sent out from Shad.
Zero runtime dependencies. React is a peer dependency.
Install
npm install shad-auth-reactReact is never bundled and never resolved from inside this package — the host
app's React instance is the only one used. Hooks break the moment a second copy
exists, with Cannot read properties of null (reading 'useState').
A registry install cannot produce that copy: only dist/ is published. A local
npm link or file: install can, because the linked folder is a full checkout
and bundlers resolve react from the real path, not the symlink. Guard it:
- this package ships an
.npmrcwithlegacy-peer-deps=true, sonpm installhere does not pull a peer React intonode_modules; - in the host app, pin resolution to one copy:
// next.config.mjs
webpack: config => {
config.resolve.alias = {
...config.resolve.alias,
react: path.resolve(rootDir, 'node_modules/react'),
'react-dom': path.resolve(rootDir, 'node_modules/react-dom'),
};
return config;
},App Router
ShadLogin carries the "use client" directive, so it can be imported straight
into a server component. The imperative startShadLogin is browser-only — call
it from a client component or an event handler.
Use
import { ShadLogin } from "shad-auth-react";
<ShadLogin
clientId="shad-panel"
redirectUri="https://panel.example.ir/auth/callback"
onSuccess={(code) => api.post("/auth/shad", { code })}
onError={(err) => toast(err.code)}
/>That is the whole integration. The button opens the Shad login in a popup and hands you a single-use authorization code.
The code is not an identity token and cannot be read in the browser. Post it to your own backend, which exchanges it with Shad SSO for the token, and set your own session cookie from there.
id, err := shad.Exchange(ctx, code)
if err != nil { /* ... */ }
// From here on, role and access are your application's business.
session.Start(id.Subject)Styling the button
The built-in look follows Google's "Sign in with Google" button: its published theme colours, its heights, radius and hover and press tints, with the Shad mark in place of the G.
<ShadLogin {...props} theme="dark" size="medium" shape="pill" />| Prop | Values | Default |
|---|---|---|
| theme | light — white, grey outline · dark — near-black, for dark backgrounds · neutral — light grey, no outline | light |
| size | large 40px · medium 32px · small 20px | large |
| shape | rectangular · pill | rectangular |
Nothing follows the system colour scheme by itself — as with Google's button, the
theme is yours to match to the background. An app with its own dark mode passes
theme={isDark ? "dark" : "light"}.
style is merged over the built-in look, so small changes need nothing else:
<ShadLogin {...props} style={{ width: "100%", borderRadius: 8 }} />A className alone cannot restyle it: the defaults are inline, and an inline
style beats any class. Pass unstyled to drop them and let your own CSS decide
everything:
<ShadLogin {...props} unstyled className="btn btn-primary w-full" />The Shad mark keeps its own colours on every background, as Google's G does. While a
login is running the button is disabled and carries aria-busy="true" — target
that for a loading style, with [aria-busy="true"] in CSS or
aria-busy:opacity-60 in Tailwind.
Native button attributes pass straight through — id, aria-label,
data-testid, name and the rest:
<ShadLogin {...props} id="shad-login" data-testid="shad-login" />onClick and type belong to the component, and onError is the failed-login
callback, not the DOM event.
Imperative API
For a custom button, skip the component:
import { startShadLogin, ShadAuthError } from "shad-auth-react";
button.addEventListener("click", () => {
startShadLogin({ clientId, redirectUri })
.then(sendToBackend)
.catch((err: ShadAuthError) => console.warn(err.code));
});Call it directly inside the click handler. Browsers only allow window.open
while a user gesture is being handled, so opening after an await is treated as
unsolicited and blocked.
Options
| Option | Required | Meaning |
|---|---|---|
| clientId | yes | Issued when your app was registered with Shad SSO. |
| redirectUri | yes | Must match a registered URL exactly — the registry accepts no wildcards, so a value assembled at runtime from window.location is usually rejected. |
| stage | no | false (default) logs in against production, https://sso.noyanet.com. true uses staging, https://sso-stage.noyanet.com — set it in your non-production builds, since the package cannot tell them apart. |
| ssoOrigin | no | Any other SSO origin. Wins over stage when set. |
| acr | no | Minimum authentication level, e.g. "aal2" to force a second factor. Can raise the level above what the client is registered for, never lower it. |
| onPopupBlocked | no | "redirect" (default) or "error". |
Popup blocked
By default the whole page navigates to Shad instead of opening a window, which
is what strict popup blockers, Safari and in-app WebViews require. Your app does
nothing extra — the code comes back to your registered redirectUri as a query
parameter.
If that URL is a frontend route rather than a backend endpoint, read it with:
import { readRedirectResult } from "shad-auth-react";
const code = readRedirectResult(); // null when the URL carries no resultIt verifies the state, strips the parameters from the address bar, and throws a
ShadAuthError on mismatch.
Errors
onError always receives a ShadAuthError with a code:
| code | When |
|---|---|
| access_denied | The user cancelled or closed the login window. |
| popup_blocked | Only with onPopupBlocked: "error". |
| state_mismatch | The reply did not carry back the state we sent. Treat as hostile. |
| server_error | Anything Shad reported without a dedicated code. |
What this package will not do
- Store anything. No token, no code, no
localStorage. A token kept in the frontend turns any XSS anywhere in your app into standing access. - Tell you who the user is. Identity comes from your backend's exchange.
- Decide what the user may do. Shad SSO proves identity and nothing else; roles and permissions are entirely your application's business.
- Ship CSS. Styling is inline so nothing collides with, or gets purged by, your own setup. See Styling the button.
Build
npm install
npm run build # dist/esm + dist/cjs + types
npm run typecheck