@poly-x/react 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -2
- package/dist/index.cjs +893 -83
- package/dist/index.d.cts +228 -3
- package/dist/index.d.mts +228 -3
- package/dist/index.mjs +886 -85
- package/dist/styles.css +151 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -41,11 +41,138 @@ function Dashboard() {
|
|
|
41
41
|
Add a callback route (`/auth/callback`) that renders `<AuthCallback />` to
|
|
42
42
|
complete sign-in.
|
|
43
43
|
|
|
44
|
+
## Profile self-service
|
|
45
|
+
|
|
46
|
+
`<UserProfile>` gives a signed-in person a dialog to correct their own details —
|
|
47
|
+
first name, last name, phone number, national ID, gender and photo. You write no
|
|
48
|
+
form, no validation rules and no upload handling:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { UserProfile } from "@poly-x/react";
|
|
52
|
+
|
|
53
|
+
<UserProfile onSaved={() => router.refresh()} />;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Most apps want it on the account menu instead of as its own button:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<UserButton showUserProfile />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That turns the existing **Manage account** entry into one that opens the dialog.
|
|
63
|
+
It is opt-in, and `manageAccountUrl` always wins — so an app already linking that
|
|
64
|
+
entry to its own page keeps exactly what it has.
|
|
65
|
+
|
|
66
|
+
Requires `@poly-x/next`'s route handler to be mounted (the platform call is made
|
|
67
|
+
server-side; the browser never holds a credential). Validation mirrors the
|
|
68
|
+
platform's own rules, so a value the dialog accepts is one the platform accepts.
|
|
69
|
+
|
|
70
|
+
Editing anybody **else's** profile is not offered here — that is administration,
|
|
71
|
+
and it lives in the PolyX console with the permission model and audit trail.
|
|
72
|
+
Email, username and password are likewise excluded: each has its own verification
|
|
73
|
+
path that a profile form must not route around.
|
|
74
|
+
|
|
75
|
+
## Signing in silently, and switching workspace
|
|
76
|
+
|
|
77
|
+
Someone already signed into another application on the same domain should not be asked for a
|
|
78
|
+
password again. `useWarmHop()` tries that once when your sign-in screen mounts:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { useWarmHop, SignIn } from "@poly-x/react";
|
|
82
|
+
|
|
83
|
+
export default function SignInPage() {
|
|
84
|
+
const hop = useWarmHop();
|
|
85
|
+
if (hop.status !== "settled") return null; // never a half-drawn form
|
|
86
|
+
if (hop.result.status === "signed_in") router.replace("/");
|
|
87
|
+
return <SignIn />; // the ordinary path
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A failed attempt is invisible on purpose. Someone who is genuinely signed out pays the whole
|
|
92
|
+
cost of this and gains nothing from it, so there is a hard timeout, nothing in the flow
|
|
93
|
+
navigates, and every failure — offline, slow platform, unexpected response — ends at "show the
|
|
94
|
+
form" rather than at an exception or a spinner.
|
|
95
|
+
|
|
96
|
+
The attempt runs **once per tab**. The marker lives in `sessionStorage` rather than component
|
|
97
|
+
state because the fall-back renders the sign-in page, which is a route change in most apps — a
|
|
98
|
+
guard in state would reset at exactly the moment it needs to hold. Call `resetWarmHop()` after
|
|
99
|
+
signing out so the next sign-in may try again.
|
|
100
|
+
|
|
101
|
+
### Switching workspace
|
|
102
|
+
|
|
103
|
+
Someone with access to several workspaces can move between them without a password:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { switchWorkspace } from "@poly-x/react";
|
|
107
|
+
|
|
108
|
+
const result = await switchWorkspace(tenantId);
|
|
109
|
+
if (result.status === "signed_in") router.refresh(); // now scoped to that workspace
|
|
110
|
+
if (result.status === "no_access") showNoAccess();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
When someone has more than one workspace and none was named, the settled result is
|
|
114
|
+
`{ status: "select_workspace", workspaces }` — the SDK never picks for you, because putting
|
|
115
|
+
someone to work in the wrong tenant without their noticing is worse than asking:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
const hop = useWarmHop();
|
|
119
|
+
if (hop.status === "settled" && hop.result.status === "select_workspace") {
|
|
120
|
+
return <WorkspacePicker options={hop.result.workspaces} onPick={switchWorkspace} />;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Entitlement is decided **by the platform at the moment of the switch**, not from the list you are
|
|
125
|
+
holding, so a workspace revoked since you drew the picker never lets them in. What you get back
|
|
126
|
+
depends on what is left: still hold other workspaces and you get `select_workspace` listing them;
|
|
127
|
+
lost them all and you get `no_access`. Either way, not entry — so do not cache the decision in
|
|
128
|
+
front of it.
|
|
129
|
+
|
|
130
|
+
### When the workspace you asked for is quietly ignored
|
|
131
|
+
|
|
132
|
+
Worth knowing, because success is not always what it looks like. PolyX only honours a workspace
|
|
133
|
+
hint when the person has **more than one** workspace for that application. With exactly one, it
|
|
134
|
+
ignores the hint and seats them there anyway — answering an ordinary success. Nobody gains access
|
|
135
|
+
they are not entitled to, but they can end up **working in the wrong tenant without noticing**,
|
|
136
|
+
which is the failure the picker exists to prevent.
|
|
137
|
+
|
|
138
|
+
The SDK will not relay that as plain success. Pass a **tenant id** and land somewhere else, and you
|
|
139
|
+
get `workspace_mismatch` carrying both:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
const result = await switchWorkspace(tenantId);
|
|
143
|
+
if (result.status === "workspace_mismatch") {
|
|
144
|
+
// result.requested — what you asked for
|
|
145
|
+
// result.workspace — where they actually are now
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The session is left in place: it is valid, and you may be perfectly happy with it. What to do about
|
|
150
|
+
it is your call, not the SDK's.
|
|
151
|
+
|
|
152
|
+
An **organization code or slug** cannot be compared against what a session carries, so no mismatch
|
|
153
|
+
is claimed for those — guessing would invent failures. Instead **every** successful result names
|
|
154
|
+
the workspace actually entered, so you can check for yourself:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
if (result.status === "signed_in" && result.workspace !== expected) { /* … */ }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Switching does not navigate or reload — the SDK cannot know what is unsaved on your screen, so
|
|
161
|
+
what happens next is yours to decide.
|
|
162
|
+
|
|
163
|
+
### The boundary
|
|
164
|
+
|
|
165
|
+
Silent sign-on works between applications that **share the ecosystem cookie's domain**.
|
|
166
|
+
Browsers block third-party cookies by default, so an application on an unrelated domain falls
|
|
167
|
+
back to ordinary sign-in. That is browser policy, not something to configure around.
|
|
168
|
+
*Switching workspace is unaffected* — it happens inside one application.
|
|
169
|
+
|
|
44
170
|
## API
|
|
45
171
|
|
|
46
172
|
`PolyXProvider` · `useAuth` (`isLoaded`, `isSignedIn`, `signIn`, `signOut`,
|
|
47
173
|
`getToken`) · `useUser` · `useSession` · `<SignIn>` · `<Protected>` ·
|
|
48
|
-
`<
|
|
49
|
-
|
|
174
|
+
`<UserButton>` · `<UserProfile>` · `<AuthCallback>` · `useWarmHop` ·
|
|
175
|
+
`attemptWarmHop` · `switchWorkspace` · `resetWarmHop`. Sign-in defaults to a
|
|
176
|
+
popup; pass `mode="redirect"` for a full-page flow.
|
|
50
177
|
|
|
51
178
|
MIT licensed.
|