@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 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
- `<AuthCallback>`. Sign-in defaults to a popup; pass `mode="redirect"` for a
49
- full-page flow.
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.