@strivacity/sdk-react 3.0.3 → 4.0.0-beta.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +1562 -406
  3. package/dist/assets/rolldown-runtime.cjs +1 -0
  4. package/dist/assets/rolldown-runtime.mjs +1 -0
  5. package/dist/components.cjs +2 -0
  6. package/dist/components.cjs.map +1 -0
  7. package/dist/components.d.ts +11 -0
  8. package/dist/components.mjs +2 -0
  9. package/dist/components.mjs.map +1 -0
  10. package/dist/errors.cjs +2 -0
  11. package/dist/errors.cjs.map +1 -0
  12. package/dist/errors.d.ts +1 -0
  13. package/dist/errors.mjs +2 -0
  14. package/dist/errors.mjs.map +1 -0
  15. package/dist/hooks.cjs +2 -0
  16. package/dist/hooks.cjs.map +1 -0
  17. package/dist/hooks.d.ts +33 -0
  18. package/dist/hooks.mjs +2 -0
  19. package/dist/hooks.mjs.map +1 -0
  20. package/dist/index.cjs +2 -2
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.ts +9 -13
  23. package/dist/index.mjs +2 -2
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/storages.cjs +1 -0
  26. package/dist/storages.d.ts +1 -0
  27. package/dist/storages.mjs +1 -0
  28. package/dist/types.cjs +1 -2
  29. package/dist/types.d.ts +147 -110
  30. package/dist/types.mjs +1 -2
  31. package/dist/utils.cjs +1 -0
  32. package/dist/utils.d.ts +1 -0
  33. package/dist/utils.mjs +1 -0
  34. package/package.json +51 -7
  35. package/testing/tests/components.spec.tsx +146 -0
  36. package/testing/tests/errors.spec.ts +10 -0
  37. package/testing/tests/guard.spec.tsx +119 -0
  38. package/testing/tests/hooks.spec.tsx +418 -0
  39. package/testing/tests/index.spec.ts +105 -0
  40. package/testing/tests/storages.spec.ts +10 -0
  41. package/testing/tests/utils.spec.ts +10 -0
  42. package/testing/utils/common.tsx +63 -0
  43. package/dist/AuthProvider.cjs +0 -2
  44. package/dist/AuthProvider.cjs.map +0 -1
  45. package/dist/AuthProvider.d.ts +0 -7
  46. package/dist/AuthProvider.mjs +0 -2
  47. package/dist/AuthProvider.mjs.map +0 -1
  48. package/dist/LoginRenderer.cjs +0 -2
  49. package/dist/LoginRenderer.cjs.map +0 -1
  50. package/dist/LoginRenderer.d.ts +0 -20
  51. package/dist/LoginRenderer.mjs +0 -2
  52. package/dist/LoginRenderer.mjs.map +0 -1
  53. package/dist/composables.cjs +0 -2
  54. package/dist/composables.cjs.map +0 -1
  55. package/dist/composables.d.ts +0 -12
  56. package/dist/composables.mjs +0 -2
  57. package/dist/composables.mjs.map +0 -1
  58. package/dist/types.cjs.map +0 -1
  59. package/dist/types.mjs.map +0 -1
package/README.md CHANGED
@@ -1,116 +1,222 @@
1
1
  # @strivacity/sdk-react
2
2
 
3
- A React library that integrates Strivacity's policy-driven authentication journeys into your application using the OAuth 2.0 PKCE flow. Supports `redirect`, `popup`, `native`, and `embedded` modes.
3
+ React SDK for [Strivacity](https://www.strivacity.com) - adds PKCE-protected OIDC authentication to your React application with hooks.
4
+
5
+ Built on top of [@strivacity/sdk-core](../sdk-core) - see the [core SDK documentation](../sdk-core/README.md) for detailed information about authentication flows, configuration options, and advanced features.
6
+
7
+ **See also:**
8
+ - [Full Documentation](https://docs.strivacity.com/reference/overview) - Complete guide for all authentication modes
9
+ - [Example App](../../apps/react) - Working React example covering all four login modes
10
+ - [Core SDK](../sdk-core/README.md) - Framework-agnostic SDK documentation
11
+
12
+ ## Table of contents
13
+
14
+ - [Prerequisites](#prerequisites)
15
+ - [Installation](#installation)
16
+ - [Choosing a mode](#choosing-a-mode)
17
+ - [Quick start](#quick-start)
18
+ - [Authentication modes](#authentication-modes)
19
+ - [redirect mode](#redirect-mode)
20
+ - [popup mode](#popup-mode)
21
+ - [embedded mode](#embedded-mode)
22
+ - [native mode](#native-mode)
23
+ - [Hooks API](#hooks-api)
24
+ - [useStrivacity](#usestrivacity)
25
+ - [useNativeLogin](#usenativelogin)
26
+ - [withAuthGuard](#withauthguard)
27
+ - [Route guards](#route-guards)
28
+ - [Token management](#token-management)
29
+ - [Shared features](#shared-features)
30
+ - [Configuration reference](#configuration-reference)
31
+ - [Migration guide](#migration-guide)
32
+ - [Vulnerability Reporting](#vulnerability-reporting)
33
+ - [License](#license)
34
+ - [Contributing](#contributing)
4
35
 
5
- See our [Developer Portal](https://www.strivacity.com/learn-support/developer-hub) to get started with developing with the Strivacity product.
6
-
7
- ## Overview
8
-
9
- This SDK allows you to integrate Strivacity's policy-driven journeys into your React application. It wraps the `@strivacity/sdk-core` library as a React context provider and exposes a `useStrivacity` hook that provides authentication state and methods throughout your component tree. The SDK uses the OAuth 2.0 PKCE flow to authenticate with Strivacity. For detailed configuration options, available modes, and advanced usage refer to the [`@strivacity/sdk-core` documentation](https://github.com/Strivacity/sdk-js/blob/main/packages/sdk-core/README.md).
10
-
11
- ## Demo Application
36
+ ---
12
37
 
13
- - [Example app](https://github.com/Strivacity/sdk-js/tree/main/apps/react)
14
- - [Ionic Example app](https://github.com/Strivacity/sdk-js/tree/main/apps/ionic-react)
38
+ ## Prerequisites
15
39
 
16
- ## Requirements
40
+ - React 18+
41
+ - A Strivacity tenant with an application configured (issuer URL, client ID, redirect URI)
17
42
 
18
- - React: 18+
43
+ ---
19
44
 
20
- ## Install
45
+ ## Installation
21
46
 
22
47
  ```bash
23
48
  npm install @strivacity/sdk-react
24
49
  ```
25
50
 
26
- ## Usage
51
+ ---
52
+
53
+ ## Choosing a mode
54
+
55
+ The SDK supports **four authentication modes**:
27
56
 
28
- ### Initialization
57
+ | Mode | Login UI | Best for |
58
+ | ---------- | ---------------------------------------- | -------------------------------------------- |
59
+ | `redirect` | Strivacity hosted page | Standard web apps |
60
+ | `popup` | Strivacity hosted page in a popup | SPAs that must stay on the current page |
61
+ | `embedded` | Strivacity web components in your page | Branded login inside your own layout |
62
+ | `native` | Your own components driven by flow state | Full UI control, step-by-step form rendering |
29
63
 
30
- Wrap your application with `StyAuthProvider` in your entry point:
64
+ > All modes use the same PKCE-protected OIDC flow under the hood. The `mode` option only controls where the login UI lives and how the flow state is consumed.
65
+
66
+ ---
67
+
68
+ ## Quick start
69
+
70
+ ### 1. Wrap your app with the provider
71
+
72
+ `StyAuthProvider` is a React context provider - wrap it around your root component once. It initializes the SDK and provides the auth context to every component in the tree via React context.
31
73
 
32
74
  ```tsx
75
+ // main.tsx
33
76
  import { createRoot } from 'react-dom/client';
34
- import { BrowserRouter, Route, Routes } from 'react-router';
35
- import { StyAuthProvider, type SDKOptions } from '@strivacity/sdk-react';
36
-
37
- const options: SDKOptions = {
38
- mode: 'redirect', // or 'popup', 'native', 'embedded'
39
- issuer: 'https://<YOUR_DOMAIN>',
40
- scopes: ['openid', 'profile'],
41
- clientId: '<YOUR_CLIENT_ID>',
42
- redirectUri: '<YOUR_REDIRECT_URI>',
43
- };
77
+ import { StyAuthProvider } from '@strivacity/sdk-react';
78
+ import { App } from './App';
44
79
 
45
80
  createRoot(document.getElementById('app')!).render(
46
- <BrowserRouter>
47
- <StyAuthProvider options={options}>
48
- <Routes>
49
- <Route path="/" element={<App />} />
50
- </Routes>
51
- </StyAuthProvider>
52
- </BrowserRouter>,
81
+ <StyAuthProvider
82
+ options={{
83
+ mode: 'redirect', // authentication mode
84
+ issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
85
+ clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
86
+ redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
87
+ scopes: ['openid', 'profile', 'email'], // requested user permissions/data
88
+ }}
89
+ >
90
+ <App />
91
+ </StyAuthProvider>,
53
92
  );
54
93
  ```
55
94
 
56
- Use the `useStrivacity` hook in any component to access authentication state:
95
+ ### 2. Use the auth state
96
+
97
+ Call `useStrivacity()` in any component to read the current authentication state and trigger login or logout.
57
98
 
58
99
  ```tsx
59
100
  import { useStrivacity } from '@strivacity/sdk-react';
60
101
 
61
- export default function MyComponent() {
62
- const { loading, isAuthenticated, idTokenClaims } = useStrivacity();
102
+ export function Header() {
103
+ const { loading, isAuthenticated, idTokenClaims, login, logout } = useStrivacity();
104
+
105
+ if (loading) {
106
+ return null;
107
+ }
108
+
109
+ return (
110
+ <div>
111
+ {isAuthenticated ? (
112
+ <>
113
+ <span>Hello, {idTokenClaims?.given_name}</span>
114
+ <button onClick={() => logout()}>Log out</button>
115
+ </>
116
+ ) : (
117
+ <button onClick={() => login()}>Log in</button>
118
+ )}
119
+ </div>
120
+ );
63
121
  }
64
122
  ```
65
123
 
66
- ### Redirect / Popup mode
124
+ ---
125
+
126
+ ## Authentication modes
67
127
 
68
- In `redirect` mode the user is taken to the identity provider in the same window; in `popup` mode authentication happens in a popup. Both are initiated the same way from code.
128
+ ### redirect mode
69
129
 
70
- #### Login page example
130
+ > For details on how this mode works, see the [hosted journey documentation](https://docs.strivacity.com/reference/hosted-journey).
131
+
132
+ The current browser tab navigates to the Strivacity-hosted login page and back to the configured `redirectUri` after authentication.
133
+
134
+ ```tsx
135
+ // main.tsx
136
+ import { createRoot } from 'react-dom/client';
137
+ import { StyAuthProvider } from '@strivacity/sdk-react';
138
+ import { App } from './App';
139
+
140
+ createRoot(document.getElementById('app')!).render(
141
+ <StyAuthProvider
142
+ options={{
143
+ mode: 'redirect', // authentication mode
144
+ issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
145
+ clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
146
+ redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
147
+ scopes: ['openid', 'profile', 'email'], // requested user permissions/data
148
+ }}
149
+ >
150
+ <App />
151
+ </StyAuthProvider>,
152
+ );
153
+ ```
154
+
155
+ #### Login
156
+
157
+ Call this to start the login flow. It redirects the user to the Strivacity login page in current browser tab, where they authenticate.
71
158
 
72
159
  ```tsx
160
+ // pages/Login.tsx
73
161
  import { useEffect } from 'react';
74
162
  import { useStrivacity } from '@strivacity/sdk-react';
163
+ import type { RedirectFlow } from '@strivacity/sdk-react';
75
164
 
76
165
  export default function Login() {
77
- const { login } = useStrivacity();
166
+ const { loading, login } = useStrivacity<RedirectFlow>();
78
167
 
79
168
  useEffect(() => {
80
- login();
81
- }, []);
169
+ if (loading) {
170
+ return;
171
+ }
172
+
173
+ void login({
174
+ // Optional parameters
175
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
176
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
177
+ audiences: ['https://api.example.com'], // target resources for the access token
178
+ locationMethod: 'assign', // window.location method: 'assign' adds to browser history, 'replace' doesn't (default: 'assign')
179
+ targetWindow: 'self', // 'self' redirects current window, 'top' redirects top-level window (default: 'self')
180
+ });
181
+ }, [loading, login]);
82
182
 
83
183
  return (
84
184
  <section>
85
- <h1>Redirecting...</h1>
185
+ <h1>Redirecting to login...</h1>
86
186
  </section>
87
187
  );
88
188
  }
89
189
  ```
90
190
 
91
- #### Callback page example
191
+ #### Handle the callback
92
192
 
93
- The callback page handles the response from the identity provider. It calls `handleCallback()` and redirects to `/profile` on success:
193
+ Call this on your redirect URI page after the IDP sends the user back. It parses the query parameters from the callback URL, verifies the state matches what was stored during login (CSRF protection), exchanges the authorization code for tokens using PKCE, validates the ID token, and stores the session in the [configured storage](#storages). After that you can redirect to a protected page or render your app.
94
194
 
95
195
  ```tsx
196
+ // pages/Callback.tsx
197
+ import { useStrivacity } from '@strivacity/sdk-react';
96
198
  import { useEffect } from 'react';
97
199
  import { useNavigate } from 'react-router';
98
- import { useStrivacity } from '@strivacity/sdk-react';
99
200
 
100
201
  export default function Callback() {
202
+ const { loading, handleCallback } = useStrivacity();
101
203
  const navigate = useNavigate();
102
- const { handleCallback } = useStrivacity();
204
+ const searchParams = new URLSearchParams(window.location.search);
103
205
 
104
206
  useEffect(() => {
105
- (async () => {
106
- try {
107
- await handleCallback();
108
- await navigate('/profile');
109
- } catch (error) {
110
- console.error('Error during callback handling:', error);
111
- }
112
- })();
113
- }, []);
207
+ if (loading) {
208
+ return;
209
+ }
210
+
211
+ if (searchParams.get('error')) {
212
+ void navigate(`/error?${searchParams.toString()}`, { replace: true });
213
+ return;
214
+ }
215
+
216
+ handleCallback()
217
+ .then(() => navigate('/profile'))
218
+ .catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
219
+ }, [loading]);
114
220
 
115
221
  return (
116
222
  <section>
@@ -120,325 +226,329 @@ export default function Callback() {
120
226
  }
121
227
  ```
122
228
 
123
- #### Profile page example
229
+ > The callback URL is automatically read from `window.location.href` if not provided. You can pass a custom URL as the first parameter: `await sdk.handleCallback(customUrl)`.
230
+
231
+ #### Registration
232
+
233
+ Call this to start the registration flow. It works the same way as `login()` but opens the registration form instead.
124
234
 
125
235
  ```tsx
236
+ // pages/Register.tsx
237
+ import { useEffect } from 'react';
126
238
  import { useStrivacity } from '@strivacity/sdk-react';
239
+ import type { RedirectFlow } from '@strivacity/sdk-react';
127
240
 
128
- export default function Profile() {
129
- const { loading, isAuthenticated, accessToken, accessTokenExpired, accessTokenExpirationDate, idTokenClaims, refreshToken } = useStrivacity();
241
+ export default function Register() {
242
+ const { loading, register } = useStrivacity<RedirectFlow>();
130
243
 
131
- if (loading) {
132
- return <h1>Loading...</h1>;
133
- }
244
+ useEffect(() => {
245
+ if (loading) {
246
+ return;
247
+ }
248
+
249
+ void register({
250
+ loginHint: 'user@example.com',
251
+ });
252
+ }, [loading, register]);
134
253
 
135
254
  return (
136
255
  <section>
137
- <dl>
138
- <dt>
139
- <strong>accessToken</strong>
140
- </dt>
141
- <dd>
142
- <pre>{JSON.stringify(accessToken)}</pre>
143
- </dd>
144
- <dt>
145
- <strong>refreshToken</strong>
146
- </dt>
147
- <dd>
148
- <pre>{JSON.stringify(refreshToken)}</pre>
149
- </dd>
150
- <dt>
151
- <strong>accessTokenExpired</strong>
152
- </dt>
153
- <dd>
154
- <pre>{JSON.stringify(accessTokenExpired)}</pre>
155
- </dd>
156
- <dt>
157
- <strong>accessTokenExpirationDate</strong>
158
- </dt>
159
- <dd>
160
- <pre>{accessTokenExpirationDate ? new Date(accessTokenExpirationDate * 1000).toLocaleString() : JSON.stringify(null)}</pre>
161
- </dd>
162
- <dt>
163
- <strong>claims</strong>
164
- </dt>
165
- <dd>
166
- <pre>{JSON.stringify(idTokenClaims, null, 2)}</pre>
167
- </dd>
168
- </dl>
256
+ <h1>Redirecting to registration...</h1>
169
257
  </section>
170
258
  );
171
259
  }
172
260
  ```
173
261
 
174
- #### Logout page example
262
+ #### Logout
175
263
 
176
- The `postLogoutRedirectUri` parameter is optional and specifies where users are redirected after logout. This URI must be configured in the Admin Console as an allowed post-logout redirect URI.
264
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
177
265
 
178
266
  ```tsx
179
- import { useEffect } from 'react';
180
- import { useNavigate } from 'react-router';
267
+ // pages/Logout.tsx
181
268
  import { useStrivacity } from '@strivacity/sdk-react';
269
+ import { useEffect } from 'react';
182
270
 
183
271
  export default function Logout() {
184
- const navigate = useNavigate();
185
- const { isAuthenticated, logout } = useStrivacity();
272
+ const { loading, logout } = useStrivacity();
186
273
 
187
274
  useEffect(() => {
188
- (async () => {
189
- if (isAuthenticated) {
190
- await logout({ postLogoutRedirectUri: location.origin });
191
- } else {
192
- await navigate('/');
193
- }
194
- })();
195
- }, []);
275
+ if (loading) {
276
+ return;
277
+ }
196
278
 
197
- return (
198
- <section>
199
- <h1>Logging out...</h1>
200
- </section>
201
- );
279
+ void logout();
280
+ }, [loading, logout]);
281
+
282
+ return null;
202
283
  }
203
284
  ```
204
285
 
205
- #### Component example
286
+ > The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
287
+
288
+ #### Token management
289
+
290
+ Call these methods to manage the session and access token.
206
291
 
207
292
  ```tsx
208
293
  import { useStrivacity } from '@strivacity/sdk-react';
209
294
 
210
- export default function Nav() {
211
- const { isAuthenticated, idTokenClaims, login, logout } = useStrivacity();
212
- const name = `${idTokenClaims?.given_name} ${idTokenClaims?.family_name}`;
295
+ export function TokenPanel() {
296
+ const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
213
297
 
214
- return isAuthenticated ? (
215
- <div>
216
- <div>Welcome, {name}!</div>
217
- <button onClick={() => logout()}>Logout</button>
218
- </div>
219
- ) : (
298
+ async function onRefresh() {
299
+ // Refresh the access token using the refresh token
300
+ await refresh();
301
+ }
302
+
303
+ async function onRevoke() {
304
+ // Revoke all tokens at the authorization server and clear the local session
305
+ await revoke();
306
+ }
307
+
308
+ if (loading) {
309
+ return null;
310
+ }
311
+
312
+ // idTokenClaims, accessToken, and refreshToken are plain values from context -
313
+ // they update automatically whenever the SDK's session changes
314
+ return (
220
315
  <div>
221
- <div>Not logged in</div>
222
- <button onClick={() => login()}>Log in</button>
316
+ <button onClick={onRefresh}>Refresh</button>
317
+ <button onClick={onRevoke}>Revoke</button>
318
+ <pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
223
319
  </div>
224
320
  );
225
321
  }
226
322
  ```
227
323
 
228
- ### Native mode
324
+ ---
229
325
 
230
- In `native` mode the `StyLoginRenderer` component renders the authentication UI inline using your custom widget components. You can define custom components for each input type; see [Example widgets](https://github.com/Strivacity/sdk-js/tree/main/apps/react/src/components/widgets).
326
+ ### popup mode
231
327
 
232
- The example widgets use SCSS for styling and Luxon for date handling:
328
+ > For details on how this mode works, see the [hosted journey documentation](https://docs.strivacity.com/reference/hosted-journey).
233
329
 
234
- ```bash
235
- npm install sass luxon
236
- npm install --save-dev @types/luxon
237
- ```
330
+ The Strivacity login page opens in a separate window or tab. After authentication the opened window or tab closes itself and the parent page receives the session - no full-page navigation required.
238
331
 
239
332
  ```tsx
240
- import CheckboxWidget from './checkbox.widget';
241
- import DateWidget from './date.widget';
242
- import InputWidget from './input.widget';
243
- import LayoutWidget from './layout.widget';
244
- import MultiSelectWidget from './multiselect.widget';
245
- import PasscodeWidget from './passcode.widget';
246
- import LoadingWidget from './loading.widget';
247
- import PasswordWidget from './password.widget';
248
- import PhoneWidget from './phone.widget';
249
- import SelectWidget from './select.widget';
250
- import StaticWidget from './static.widget';
251
- import SubmitWidget from './submit.widget';
333
+ // main.tsx
334
+ import { createRoot } from 'react-dom/client';
335
+ import { StyAuthProvider } from '@strivacity/sdk-react';
336
+ import { App } from './App';
252
337
 
253
- export const widgets = {
254
- checkbox: CheckboxWidget,
255
- date: DateWidget,
256
- input: InputWidget,
257
- layout: LayoutWidget,
258
- loading: LoadingWidget,
259
- passcode: PasscodeWidget,
260
- password: PasswordWidget,
261
- phone: PhoneWidget,
262
- select: SelectWidget,
263
- multiSelect: MultiSelectWidget,
264
- static: StaticWidget,
265
- submit: SubmitWidget,
266
- };
338
+ createRoot(document.getElementById('app')!).render(
339
+ <StyAuthProvider
340
+ options={{
341
+ mode: 'popup', // authentication mode
342
+ issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
343
+ clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
344
+ redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
345
+ scopes: ['openid', 'profile', 'email'], // requested user permissions/data
346
+ }}
347
+ >
348
+ <App />
349
+ </StyAuthProvider>,
350
+ );
267
351
  ```
268
352
 
269
- #### Login page example
353
+ #### Login
354
+
355
+ Call this to start the login flow. It opens a popup window by default with the Strivacity login page, where the user authenticates. After that the popup closes itself and the session is stored in the [configured storage](#storages).
270
356
 
271
- The login page extracts `session_id` and optionally `language` from the URL on load, cleans up the URL, and passes them to the renderer. When a `session_id` is present the renderer calls `startSession(sessionId)` to resume the existing flow instead of starting a new one. When a `language` parameter is present it is passed to the renderer which uses it for the authentication UI and calls `onLanguageChange` with the resolved language.
357
+ By default a centered popup window opens. Pass `popupWindowTarget` to change where the window opens, and `popupWindowFeatures` to control its size and position:
272
358
 
273
359
  ```tsx
274
- import { useEffect, useState } from 'react';
360
+ // pages/Login.tsx
361
+ import { useEffect, useRef } from 'react';
362
+ import { useStrivacity } from '@strivacity/sdk-react';
275
363
  import { useNavigate } from 'react-router';
276
- import { StyLoginRenderer, FallbackError, type LoginFlowState } from '@strivacity/sdk-react';
277
- import { widgets } from '@/components/widgets';
364
+ import type { PopupFlow } from '@strivacity/sdk-react';
278
365
 
279
366
  export default function Login() {
367
+ const { loading, login } = useStrivacity<PopupFlow>();
280
368
  const navigate = useNavigate();
281
- const [sessionId, setSessionId] = useState<string | null>(null);
282
- const [language, setLanguage] = useState<string | null>(null);
369
+ const startedRef = useRef(false);
283
370
 
284
371
  useEffect(() => {
285
- if (window.location.search !== '') {
286
- const url = new URL(window.location.href);
287
- setSessionId(url.searchParams.get('session_id'));
288
-
289
- if (url.searchParams.has('language')) {
290
- setLanguage(url.searchParams.get('language'));
291
- }
292
-
293
- url.search = '';
294
- window.history.replaceState({}, '', url.toString());
295
- }
296
- }, []);
297
-
298
- const onLogin = async () => {
299
- await navigate('/profile');
300
- };
301
-
302
- const onFallback = (error: FallbackError) => {
303
- if (error.url) {
304
- window.location.href = error.url.toString();
305
- } else {
306
- alert(error);
372
+ if (loading || startedRef.current) {
373
+ return;
307
374
  }
308
- };
309
-
310
- const onError = (error: string) => {
311
- alert(error);
312
- };
313
-
314
- const onGlobalMessage = (message: string) => {
315
- alert(message);
316
- };
317
375
 
318
- const onBlockReady = ({ previousState, state }: { previousState: LoginFlowState; state: LoginFlowState }) => {
319
- console.log('previousState', previousState);
320
- console.log('state', state);
321
- };
376
+ // Prevent multiple calls (React StrictMode)
377
+ startedRef.current = true;
378
+
379
+ login({
380
+ // Optional parameters
381
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
382
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
383
+ audiences: ['https://api.example.com'], // target resources for the access token
384
+ popupWindowTarget: '_blank', // any valid browsing context name
385
+ popupWindowFeatures: {
386
+ width: 500,
387
+ height: 700,
388
+ left: 100,
389
+ top: 100,
390
+ toolbar: false,
391
+ location: false,
392
+ resizable: true,
393
+ scrollbars: true,
394
+ },
395
+ })
396
+ .then(() => navigate('/profile'))
397
+ .catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
398
+ }, [loading, login, navigate]);
322
399
 
323
400
  return (
324
- <StyLoginRenderer
325
- widgets={widgets}
326
- sessionId={sessionId}
327
- language={language}
328
- onLanguageChange={(lang) => setLanguage(lang)}
329
- onFallback={onFallback}
330
- onLogin={() => void onLogin()}
331
- onError={onError}
332
- onGlobalMessage={onGlobalMessage}
333
- onBlockReady={onBlockReady}
334
- />
401
+ <section>
402
+ <h1>Opening login popup...</h1>
403
+ </section>
335
404
  );
336
405
  }
337
406
  ```
338
407
 
339
- #### Callback page example
408
+ #### Handle the callback
409
+
410
+ The popup resolves automatically - no callback page is needed. Token exchange happens inside the popup and the result is posted back to the opener window.
340
411
 
341
- When a `session_id` is present in the URL the native flow is resumed by forwarding it to the login page. Otherwise the standard `handleCallback()` path is used:
412
+ #### Registration
413
+
414
+ Call this to start the registration flow. It works the same way as `login()` but opens the registration form instead.
342
415
 
343
416
  ```tsx
344
- import { useEffect } from 'react';
345
- import { useNavigate } from 'react-router';
417
+ // pages/Register.tsx
418
+ import { useEffect, useRef } from 'react';
346
419
  import { useStrivacity } from '@strivacity/sdk-react';
420
+ import { useNavigate } from 'react-router';
421
+ import type { PopupFlow } from '@strivacity/sdk-react';
347
422
 
348
- export default function Callback() {
423
+ export default function Register() {
424
+ const { loading, register } = useStrivacity<PopupFlow>();
349
425
  const navigate = useNavigate();
350
- const { handleCallback } = useStrivacity();
426
+ const startedRef = useRef(false);
351
427
 
352
428
  useEffect(() => {
353
- (async () => {
354
- const url = new URL(location.href);
355
- const sessionId = url.searchParams.get('session_id');
356
-
357
- if (sessionId) {
358
- await navigate(`/login?session_id=${sessionId}`);
359
- } else {
360
- try {
361
- await handleCallback();
362
- await navigate('/profile');
363
- } catch (error) {
364
- console.error('Error during callback handling:', error);
365
- }
366
- }
367
- })();
368
- }, []);
429
+ if (loading || startedRef.current) {
430
+ return;
431
+ }
432
+
433
+ // Prevent multiple calls (React StrictMode)
434
+ startedRef.current = true;
435
+
436
+ register({
437
+ // Optional parameters
438
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
439
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
440
+ audiences: ['https://api.example.com'], // target resources for the access token
441
+ popupWindowTarget: '_blank', // any valid browsing context name
442
+ popupWindowFeatures: {
443
+ width: 500,
444
+ height: 700,
445
+ left: 100,
446
+ top: 100,
447
+ toolbar: false,
448
+ location: false,
449
+ resizable: true,
450
+ scrollbars: true,
451
+ },
452
+ })
453
+ .then(() => navigate('/profile'))
454
+ .catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
455
+ }, [loading, register, navigate]);
369
456
 
370
457
  return (
371
458
  <section>
372
- <h1>Logging in...</h1>
459
+ <h1>Opening registration popup...</h1>
373
460
  </section>
374
461
  );
375
462
  }
376
463
  ```
377
464
 
378
- #### Entry page example
465
+ #### Logout
379
466
 
380
- The entry page processes flows started by an external process (e.g. password reset) by calling `entry()` to extract the necessary parameters to resume the flow and forwarding them to the callback page:
467
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
381
468
 
382
469
  ```tsx
383
- import { useEffect } from 'react';
384
- import { useNavigate } from 'react-router';
470
+ // pages/Logout.tsx
385
471
  import { useStrivacity } from '@strivacity/sdk-react';
472
+ import { useEffect } from 'react';
386
473
 
387
- export default function Entry() {
388
- const navigate = useNavigate();
389
- const { entry } = useStrivacity();
474
+ export default function Logout() {
475
+ const { loading, logout } = useStrivacity();
390
476
 
391
477
  useEffect(() => {
392
- (async () => {
393
- try {
394
- const data = await entry();
478
+ if (loading) {
479
+ return;
480
+ }
395
481
 
396
- if (data && Object.keys(data).length > 0) {
397
- await navigate(`/callback?${new URLSearchParams(data).toString()}`);
398
- } else {
399
- await navigate('/');
400
- }
401
- } catch (error) {
402
- console.error('Entry failed:', error);
403
- await navigate('/');
404
- }
405
- })();
406
- }, []);
482
+ void logout();
483
+ }, [loading, logout]);
407
484
 
408
- return (
409
- <section>
410
- <h1>Loading...</h1>
411
- </section>
412
- );
485
+ return null;
413
486
  }
414
487
  ```
415
488
 
416
- #### Profile page example
489
+ > The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
490
+
491
+ #### Token management
492
+
493
+ Call these methods to manage the session and access token.
494
+
495
+ ```tsx
496
+ import { useStrivacity } from '@strivacity/sdk-react';
497
+
498
+ export function TokenPanel() {
499
+ const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
500
+
501
+ async function onRefresh() {
502
+ // Refresh the access token using the refresh token
503
+ await refresh();
504
+ }
505
+
506
+ async function onRevoke() {
507
+ // Revoke all tokens at the authorization server and clear the local session
508
+ await revoke();
509
+ }
510
+
511
+ if (loading) {
512
+ return null;
513
+ }
417
514
 
418
- Same as the profile page example in redirect/popup mode.
515
+ // idTokenClaims, accessToken, and refreshToken are plain values from context -
516
+ // they update automatically whenever the SDK's session changes
517
+ return (
518
+ <div>
519
+ <button onClick={onRefresh}>Refresh</button>
520
+ <button onClick={onRevoke}>Revoke</button>
521
+ <pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
522
+ </div>
523
+ );
524
+ }
525
+ ```
419
526
 
420
- #### Logout page example
527
+ ---
421
528
 
422
- Same as the logout page example in redirect/popup mode.
529
+ ### embedded mode
423
530
 
424
- ### Embedded mode
531
+ > For details on how this mode works, see the [embedded journey documentation](https://docs.strivacity.com/reference/embedded-journey).
425
532
 
426
- In `embedded` mode the `<sty-login>` web component (loaded via `bundle.js` from the cluster) handles rendering. Import the bundle at application startup to register the Strivacity web components:
533
+ The login UI renders inside your own page using Strivacity web components (`<sty-login>`, `<sty-notifications>`, `<sty-language-selector>`). The component bundle isn't an npm package - load it dynamically from your Strivacity tenant cluster once during application bootstrap, alongside SDK initialization:
427
534
 
428
535
  ```tsx
536
+ // main.tsx
429
537
  import { createRoot } from 'react-dom/client';
430
- import { StyAuthProvider } from '@strivacity/sdk-react';
538
+ import { StyAuthProvider, injectScript } from '@strivacity/sdk-react';
539
+ import { App } from './App';
431
540
 
432
- void import(`${import.meta.env.VITE_ISSUER}/assets/components/bundle.js`);
541
+ // Load web components bundle
542
+ injectScript('sty-components', 'https://<YOUR_TENANT_DOMAIN>/assets/components/bundle.js');
433
543
 
434
544
  createRoot(document.getElementById('app')!).render(
435
545
  <StyAuthProvider
436
546
  options={{
437
- mode: 'embedded',
438
- issuer: 'https://<YOUR_DOMAIN>',
439
- scopes: ['openid', 'profile'],
440
- clientId: '<YOUR_CLIENT_ID>',
441
- redirectUri: '<YOUR_REDIRECT_URI>',
547
+ mode: 'embedded', // authentication mode
548
+ issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
549
+ clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
550
+ redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
551
+ scopes: ['openid', 'profile', 'email'], // requested user permissions/data
442
552
  }}
443
553
  >
444
554
  <App />
@@ -446,200 +556,1246 @@ createRoot(document.getElementById('app')!).render(
446
556
  );
447
557
  ```
448
558
 
449
- ## Logging
559
+ #### Login / Register
450
560
 
451
- The SDK supports optional logging to help you debug authentication flows and monitor SDK behavior. You can enable the built-in console logger or provide your own custom logger implementation.
561
+ In embedded mode `<sty-login>` web component does not take `issuer`, `clientId`, or `redirectUri` as attributes - those come from the SDK configuration above. Place it on your login page together with `sty-notifications` (toast-style system notifications) and `sty-language-selector` (a language switcher for the login flow). Since `<sty-login>` is a plain custom element, its properties (`params`, `sessionId`, `shortAppId`, `lang`) and DOM events are wired up through a ref instead of JSX props:
452
562
 
453
- ### Using the Default Logger
563
+ ```tsx
564
+ // pages/Login.tsx
565
+ import { useEffect, useRef } from 'react';
566
+ import { useNavigate } from 'react-router';
567
+ import type { LoginComponent } from '@strivacity/sdk-react/types';
454
568
 
455
- Enable the default console logger by adding the `logging` option:
569
+ export default function Login() {
570
+ const navigate = useNavigate();
571
+ const searchParams = new URLSearchParams(window.location.search);
456
572
 
457
- ```tsx
458
- import { StyAuthProvider, DefaultLogging, type SDKOptions } from '@strivacity/sdk-react';
573
+ // Optional: Resume a session started from an entry URL (e.g., password reset)
574
+ const sessionId = searchParams.get('session_id');
575
+ const shortAppId = searchParams.get('short_app_id');
576
+ const lang = searchParams.get('language') ?? navigator.language;
577
+ const loginRef = useRef<LoginComponent>(null);
459
578
 
460
- const options: SDKOptions = {
461
- mode: 'redirect',
462
- issuer: 'https://<YOUR_DOMAIN>',
463
- scopes: ['openid', 'profile'],
464
- clientId: '<YOUR_CLIENT_ID>',
465
- redirectUri: '<YOUR_REDIRECT_URI>',
466
- logging: DefaultLogging,
467
- };
468
- ```
579
+ useEffect(() => {
580
+ const element = loginRef.current;
469
581
 
470
- ### Creating a Custom Logger
582
+ if (!element) {
583
+ return;
584
+ }
471
585
 
472
- Implement the `SDKLogging` interface and pass your class to the `logging` option:
586
+ element.params = {
587
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
588
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
589
+ audiences: ['https://api.example.com'], // target resources for the access token
590
+ language: 'en-US', // set the UI language (BCP 47 language tag)
591
+ prompt: 'login', // use 'create' to open the registration flow instead
592
+ };
593
+ element.sessionId = sessionId;
594
+ element.shortAppId = shortAppId;
595
+ element.lang = lang;
596
+
597
+ const onLogin = () => navigate('/profile');
598
+ const onClose = () => window.location.reload();
599
+ const onError = (event: Event) => navigate(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
600
+
601
+ element.addEventListener('login', onLogin);
602
+ element.addEventListener('close', onClose);
603
+ element.addEventListener('error', onError);
604
+
605
+ return () => {
606
+ element.removeEventListener('login', onLogin);
607
+ element.removeEventListener('close', onClose);
608
+ element.removeEventListener('error', onError);
609
+ };
610
+ }, [navigate, sessionId, shortAppId, lang]);
473
611
 
474
- ```typescript
475
- import type { SDKLogging } from '@strivacity/sdk-react';
612
+ return (
613
+ <section>
614
+ <sty-notifications></sty-notifications>
615
+ <sty-login ref={loginRef}></sty-login>
616
+ <sty-language-selector></sty-language-selector>
617
+ </section>
618
+ );
619
+ }
620
+ ```
476
621
 
477
- export class MyLogger implements SDKLogging {
478
- xEventId?: string;
622
+ ##### Controlling when the flow starts
479
623
 
480
- debug(message: string): void {
481
- console.debug(this.xEventId ? `[${this.xEventId}] ${message}` : message);
482
- }
624
+ By default the login flow starts automatically as soon as `<sty-login>` connects to the DOM. Add the `lazy` attribute to take manual control, then call `start()` when ready. `start()` accepts an optional params object forwarded to the authorization request, or you can set params via the `params` property before calling it:
483
625
 
484
- info(message: string): void {
485
- console.info(this.xEventId ? `[${this.xEventId}] ${message}` : message);
486
- }
626
+ ```tsx
627
+ // pages/Login.tsx
628
+ import { useRef } from 'react';
629
+ import { useNavigate } from 'react-router';
630
+ import type { LoginComponent } from '@strivacity/sdk-react/types';
487
631
 
488
- warn(message: string): void {
489
- console.warn(this.xEventId ? `[${this.xEventId}] ${message}` : message);
632
+ export default function Login() {
633
+ const navigate = useNavigate();
634
+ const loginRef = useRef<LoginComponent>(null);
635
+
636
+ async function onStartClick() {
637
+ await loginRef.current?.start({
638
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
639
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
640
+ audiences: ['https://api.example.com'], // target resources for the access token
641
+ language: 'en-US', // set the UI language (BCP 47 language tag)
642
+ prompt: 'login', // use 'create' to open the registration flow instead
643
+ });
490
644
  }
491
645
 
492
- error(message: string, error: Error): void {
493
- console.error(this.xEventId ? `[${this.xEventId}] ${message}` : message, error);
494
- }
646
+ return (
647
+ <section>
648
+ <sty-notifications></sty-notifications>
649
+ <button onClick={onStartClick}>Continue to login</button>
650
+ <sty-login ref={loginRef} lazy onLogin={() => navigate('/profile')}></sty-login>
651
+ <sty-language-selector></sty-language-selector>
652
+ </section>
653
+ );
495
654
  }
496
655
  ```
497
656
 
498
- The `SDKLogging` interface requires `debug`, `info`, `warn`, and `error` methods. The optional `xEventId` property, when set by the SDK, provides a correlation ID to trace related log messages across the authentication flow.
499
-
500
- ## HTTP Client
501
-
502
- The SDK uses a built-in `fetch`-based HTTP client for all requests. You can replace it with your own implementation by extending `SDKHttpClient` and passing your class via the `httpClient` option. This is useful when you need to attach custom headers (e.g. `x-sty-app-id`) to every outgoing request or route traffic through a proxy.
503
-
504
- ### Adding custom headers to every request
657
+ You can also set params via the `params` property before calling `start()`:
505
658
 
506
659
  ```tsx
507
- import { StyAuthProvider, SDKHttpClient, type HttpClientResponse, type SDKOptions } from '@strivacity/sdk-react';
660
+ import { useRef } from 'react';
661
+ import type { LoginComponent } from '@strivacity/sdk-react/types';
508
662
 
509
- class CustomHttpClient extends SDKHttpClient {
510
- async request<T>(url: string, options?: RequestInit): Promise<HttpClientResponse<T>> {
511
- const mergedOptions: RequestInit = {
512
- ...options,
513
- headers: {
514
- 'x-sty-app-id': 'my-app',
515
- ...(options?.headers as Record<string, string>),
516
- },
517
- };
663
+ export function LazyLogin() {
664
+ const loginRef = useRef<LoginComponent>(null);
518
665
 
519
- const response = await fetch(url, mergedOptions);
666
+ async function onStartClick() {
667
+ if (!loginRef.current) {
668
+ return;
669
+ }
520
670
 
521
- return {
522
- headers: response.headers,
523
- ok: response.ok,
524
- status: response.status,
525
- statusText: response.statusText,
526
- url: response.url,
527
- json: async () => (await response.json()) as T,
528
- text: async () => await response.text(),
671
+ loginRef.current.params = {
672
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
673
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
674
+ audiences: ['https://api.example.com'], // target resources for the access token
675
+ language: 'en-US', // set the UI language (BCP 47 language tag)
676
+ prompt: 'login', // use 'create' to open the registration flow instead
529
677
  };
678
+ await loginRef.current.start();
530
679
  }
531
- }
532
680
 
533
- const options: SDKOptions = {
534
- // ...other options
535
- httpClient: CustomHttpClient,
536
- };
681
+ return (
682
+ <>
683
+ <sty-login ref={loginRef} lazy></sty-login>
684
+ <button onClick={onStartClick}>Start Login</button>
685
+ </>
686
+ );
687
+ }
537
688
  ```
538
689
 
539
- Any header you add inside `request()` is automatically included in every SDK request
690
+ ##### Login events
540
691
 
541
- ### CORS configuration
692
+ The `<sty-login>` element dispatches `login`, `close`, and `error` custom events. Attach listeners on the element ref inside `useEffect`:
542
693
 
543
- For custom request headers to reach the Strivacity cluster, the cluster must be configured to explicitly allow them. Add the header name(s) to the **Access-Control-Allow-Headers** list in the cluster settings. Without this, browsers will block the preflight `OPTIONS` request and the SDK call will fail with a CORS error.
694
+ ```tsx
695
+ // pages/Login.tsx
696
+ import { useEffect, useRef } from 'react';
697
+ import { useNavigate } from 'react-router';
698
+ import type { LoginComponent } from '@strivacity/sdk-react/types';
544
699
 
545
- ```
546
- Access-Control-Allow-Headers: x-sty-app-id, <any other custom headers>
547
- ```
700
+ export default function Login() {
701
+ const navigate = useNavigate();
702
+ const loginRef = useRef<LoginComponent>(null);
548
703
 
549
- ## API Documentation
704
+ useEffect(() => {
705
+ const element = loginRef.current;
550
706
 
551
- ### `useStrivacity` hook
707
+ if (!element) {
708
+ return;
709
+ }
552
710
 
553
- ```typescript
554
- useStrivacity<T extends PopupContext | RedirectContext | NativeContext>(): T;
555
- ```
711
+ const onLogin = () => {
712
+ // User authenticated - navigate to a protected page
713
+ navigate('/profile');
714
+ };
715
+ const onClose = () => {
716
+ // User cancelled or closed the login flow
717
+ window.location.reload();
718
+ };
719
+ const onError = (event: Event) => {
720
+ // A fatal error occurred - the message is available in event.detail
721
+ navigate(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
722
+ };
556
723
 
557
- The hook returns a different context type depending on the `mode` configured in `StyAuthProvider`.
724
+ element.addEventListener('login', onLogin);
725
+ element.addEventListener('close', onClose);
726
+ element.addEventListener('error', onError);
558
727
 
559
- **Shared properties (all modes)**
728
+ return () => {
729
+ element.removeEventListener('login', onLogin);
730
+ element.removeEventListener('close', onClose);
731
+ element.removeEventListener('error', onError);
732
+ };
733
+ }, [navigate]);
560
734
 
561
- - **`sdk: RedirectFlow | PopupFlow | NativeFlow`**: The underlying SDK flow instance.
562
- - **`loading: boolean`**: `true` while the session is being initialized.
563
- - **`options: SDKOptions`**: The configured SDK options.
564
- - **`isAuthenticated: boolean`**: `true` when the user has a valid session.
565
- - **`idTokenClaims: IdTokenClaims | null`**: Claims from the ID token, or `null` if not authenticated.
566
- - **`accessToken: string | null`**: The current access token.
567
- - **`refreshToken: string | null`**: The current refresh token.
568
- - **`accessTokenExpired: boolean`**: `true` when the access token has expired.
569
- - **`accessTokenExpirationDate: number | null`**: Expiration timestamp (Unix seconds) of the access token.
735
+ return (
736
+ <section>
737
+ <sty-notifications></sty-notifications>
738
+ <sty-login ref={loginRef}></sty-login>
739
+ <sty-language-selector></sty-language-selector>
740
+ </section>
741
+ );
742
+ }
743
+ ```
570
744
 
571
- ---
745
+ ##### Notification events
572
746
 
573
- **Type: `RedirectContext`**
747
+ The components dispatch `notification` events on the `document` that the `<sty-notifications>` component automatically displays. If you don't want to use `<sty-notifications>`, you can listen to these events and handle them yourself:
574
748
 
575
- - **`login(options?: LoginOptions): Promise<void>`**: Initiates login by redirecting to the identity provider.
576
- - **`register(options?: RegisterOptions): Promise<void>`**: Initiates registration using a redirect flow.
577
- - **`refresh(): Promise<void>`**: Refreshes the user's session.
578
- - **`revoke(): Promise<void>`**: Revokes the current session tokens.
579
- - **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out via redirect.
580
- - **`handleCallback(url?: string): Promise<void>`**: Processes the authorization callback after redirect.
581
- - **`entry(): Promise<Record<string, string>>`**: Processes an externally-initiated flow URL and returns the parameters needed to resume the flow.
749
+ ```tsx
750
+ import { useEffect } from 'react';
582
751
 
583
- ---
752
+ export function CustomNotifications() {
753
+ useEffect(() => {
754
+ function onNotification(event: Event) {
755
+ const customEvent = event as CustomEvent;
756
+
757
+ if (customEvent.detail.action === 'show') {
758
+ // Add new notification to your custom notification system
759
+ const notification = customEvent.detail.notification;
760
+ console.log('New notification:', notification);
761
+ // Handle the notification display in your own UI
762
+ } else if (customEvent.detail.action === 'clear') {
763
+ // Clear all notifications
764
+ console.log('Clear all notifications');
765
+ }
766
+ }
584
767
 
585
- **Type: `PopupContext`**
768
+ document.addEventListener('notification', onNotification);
586
769
 
587
- - **`login(options?: LoginOptions): Promise<void>`**: Initiates login using a popup window.
588
- - **`register(options?: RegisterOptions): Promise<void>`**: Initiates registration using a popup.
589
- - **`refresh(): Promise<void>`**: Refreshes the user's session.
590
- - **`revoke(): Promise<void>`**: Revokes the current session tokens.
591
- - **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out via popup.
592
- - **`handleCallback(url?: string): Promise<void>`**: Processes the authorization callback.
593
- - **`entry(): Promise<Record<string, string>>`**: Processes an externally-initiated flow URL.
770
+ return () => document.removeEventListener('notification', onNotification);
771
+ }, []);
594
772
 
595
- ---
773
+ return null;
774
+ }
775
+ ```
596
776
 
597
- **Type: `NativeContext`**
777
+ ##### Dynamic language switching
598
778
 
599
- - **`login(options?: LoginOptions): Promise<NativeFlowHandler>`**: Initiates login using the native flow.
600
- - **`register(options?: RegisterOptions): Promise<NativeFlowHandler>`**: Initiates registration using the native flow.
601
- - **`refresh(): Promise<void>`**: Refreshes the user's session.
602
- - **`revoke(): Promise<void>`**: Revokes the current session tokens.
603
- - **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out via redirect.
604
- - **`handleCallback(url?: string): Promise<void>`**: Processes the authorization callback.
605
- - **`entry(): Promise<Record<string, string>>`**: Processes an externally-initiated flow URL.
779
+ The `<sty-language-selector>` component provides a built-in UI for language switching. If you don't want to use it, you can change the UI language dynamically by updating the `lang` property on the `<sty-login>` component:
606
780
 
607
- ---
781
+ ```tsx
782
+ import { useRef, useState } from 'react';
608
783
 
609
- ### `StyLoginRenderer` component
784
+ export function LanguageSwitcher() {
785
+ const loginRef = useRef<HTMLElement & { lang: string }>(null);
786
+ const [currentLang, setCurrentLang] = useState('en-US');
610
787
 
611
- Used in `native` mode to render the authentication UI with your own widget components.
788
+ function changeLanguage(lang: string) {
789
+ setCurrentLang(lang);
612
790
 
613
- **Props**
791
+ if (loginRef.current) {
792
+ loginRef.current.lang = lang;
793
+ }
794
+ }
614
795
 
615
- - **`params?: NativeParams`**: Additional parameters for the native login flow.
616
- - **`widgets?: PartialRecord<WidgetType, React.ComponentType>`**: Custom React components for each widget type used in the flow.
617
- - **`sessionId?: string | null`**: Session ID for resuming an existing authentication session.
618
- - **`language?: string | null`**: Language tag (e.g. `"en-US"`) for the authentication UI. Defaults to `navigator.language`. After the session starts the component calls `onLanguageChange` with the resolved language. See the [Translations](https://docs.strivacity.com/docs/translations) page to learn about language precedence implemented by the product.
796
+ return (
797
+ <section>
798
+ <div>
799
+ <button onClick={() => changeLanguage('en-US')}>English</button>
800
+ <button onClick={() => changeLanguage('fr-FR')}>Français</button>
801
+ <button onClick={() => changeLanguage('de-DE')}>Deutsch</button>
802
+ </div>
803
+ <sty-login ref={loginRef} lang={currentLang}></sty-login>
804
+ </section>
805
+ );
806
+ }
807
+ ```
619
808
 
620
- **Event callbacks**
809
+ #### Handle the callback
621
810
 
622
- - **`onLogin`**: Called on successful authentication. Receives `IdTokenClaims | null`.
623
- - **`onFallback`**: Called when the native flow needs to fall back to redirect. Receives `FallbackError` with a fallback URL.
624
- - **`onError`**: Called when an error occurs during authentication.
625
- - **`onGlobalMessage`**: Called when the flow wants to display a global message (e.g. account lockout warning).
626
- - **`onBlockReady`**: Called on flow state transitions. Receives `{ previousState: LoginFlowState; state: LoginFlowState }`. Useful for analytics and custom logging.
627
- - **`onLanguageChange`**: Called after the session starts with the resolved language string.
811
+ No separate callback page is needed. The `<sty-login>` component handles the entire authentication flow automatically, including token exchange, and dispatches a `login` event when authentication completes successfully.
628
812
 
629
- ## Vulnerability Reporting
813
+ #### Externally-initiated flows (entry)
630
814
 
631
- The [Guidelines for responsible disclosure](https://www.strivacity.com/report-a-security-issue) details the procedure for disclosing security issues. Please do not report security vulnerabilities on the public issue tracker.
815
+ For flows started externally (e.g. a password reset email link), the user lands on the entry URL you configured in your Strivacity application native client settings. Call `entry()` on that landing page to resolve the flow parameters from the IDP (`session_id`, `short_app_id`, `language`).
632
816
 
633
- ## License
817
+ You have two options:
634
818
 
635
- @strivacity/sdk-react is available under the MIT License. See the [LICENSE](https://github.com/Strivacity/sdk-js/blob/main/LICENSE) file for more info.
819
+ **Option 1: Redirect to a separate login page**
636
820
 
637
- ## Contributing
821
+ Forward the parameters as query params to your login page:
638
822
 
639
- Please see our [contributing guide](https://github.com/Strivacity/sdk-js/blob/main/CONTRIBUTING.md).
823
+ ```tsx
824
+ // pages/Entry.tsx
825
+ import { useStrivacity } from '@strivacity/sdk-react';
826
+ import { useEffect } from 'react';
827
+ import { useNavigate } from 'react-router';
828
+ import type { EmbeddedFlow } from '@strivacity/sdk-react';
829
+
830
+ export default function Entry() {
831
+ const { loading, entry } = useStrivacity<EmbeddedFlow>();
832
+ const navigate = useNavigate();
833
+
834
+ useEffect(() => {
835
+ if (loading) {
836
+ return;
837
+ }
838
+
839
+ entry()
840
+ .then((data) => {
841
+ // Redirect to login page with flow parameters
842
+ const params = new URLSearchParams({
843
+ session_id: data.session_id,
844
+ short_app_id: data.short_app_id,
845
+ language: data.language,
846
+ });
847
+ window.location.href = `/login?${params}`;
848
+ })
849
+ .catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
850
+ }, [loading, entry, navigate]);
851
+
852
+ return (
853
+ <section>
854
+ <h1>Loading...</h1>
855
+ </section>
856
+ );
857
+ }
858
+ ```
859
+
860
+ Then on your login page, read the parameters and pass them to `<sty-login>`:
861
+
862
+ ```tsx
863
+ // pages/Login.tsx
864
+ import { useEffect, useRef } from 'react';
865
+ import { useNavigate } from 'react-router';
866
+ import type { LoginComponent } from '@strivacity/sdk-react/types';
867
+
868
+ export default function Login() {
869
+ const navigate = useNavigate();
870
+ const searchParams = new URLSearchParams(window.location.search);
871
+
872
+ // Read parameters from URL
873
+ const sessionId = searchParams.get('session_id');
874
+ const shortAppId = searchParams.get('short_app_id');
875
+ const language = searchParams.get('language');
876
+ const loginRef = useRef<LoginComponent>(null);
877
+
878
+ useEffect(() => {
879
+ const element = loginRef.current;
880
+
881
+ if (!element) {
882
+ return;
883
+ }
884
+
885
+ element.sessionId = sessionId;
886
+ element.shortAppId = shortAppId;
887
+ element.lang = language;
888
+
889
+ const onLogin = () => navigate('/profile');
890
+ const onError = (event: Event) => navigate(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
891
+
892
+ element.addEventListener('login', onLogin);
893
+ element.addEventListener('error', onError);
894
+
895
+ return () => {
896
+ element.removeEventListener('login', onLogin);
897
+ element.removeEventListener('error', onError);
898
+ };
899
+ }, [navigate, sessionId, shortAppId, language]);
900
+
901
+ return (
902
+ <section>
903
+ <sty-notifications></sty-notifications>
904
+ <sty-login ref={loginRef}></sty-login>
905
+ <sty-language-selector></sty-language-selector>
906
+ </section>
907
+ );
908
+ }
909
+ ```
910
+
911
+ **Option 2: Render login on the entry page**
912
+
913
+ Pass the parameters directly to `<sty-login>` on the same page:
914
+
915
+ ```tsx
916
+ // pages/Entry.tsx
917
+ import { useStrivacity } from '@strivacity/sdk-react';
918
+ import { useEffect, useRef, useState } from 'react';
919
+ import { useNavigate } from 'react-router';
920
+ import type { LoginComponent } from '@strivacity/sdk-react/types';
921
+ import type { EmbeddedFlow } from '@strivacity/sdk-react';
922
+
923
+ export default function Entry() {
924
+ const { loading, entry } = useStrivacity<EmbeddedFlow>();
925
+ const navigate = useNavigate();
926
+ const loginRef = useRef<LoginComponent>(null);
927
+
928
+ const [sessionId, setSessionId] = useState<string | null>(null);
929
+ const [shortAppId, setShortAppId] = useState<string | null>(null);
930
+ const [language, setLanguage] = useState<string | null>(null);
931
+
932
+ useEffect(() => {
933
+ if (loading) {
934
+ return;
935
+ }
936
+
937
+ entry()
938
+ .then((data) => {
939
+ // Set state for sty-login component
940
+ setSessionId(data.session_id);
941
+ setShortAppId(data.short_app_id);
942
+ setLanguage(data.language);
943
+ })
944
+ .catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
945
+ }, [loading, entry, navigate]);
946
+
947
+ useEffect(() => {
948
+ const element = loginRef.current;
949
+
950
+ if (!element || !sessionId) {
951
+ return;
952
+ }
953
+
954
+ element.sessionId = sessionId;
955
+ element.shortAppId = shortAppId;
956
+ element.lang = language;
957
+
958
+ const onLogin = () => navigate('/profile');
959
+ const onError = (event: Event) => navigate(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
960
+
961
+ element.addEventListener('login', onLogin);
962
+ element.addEventListener('error', onError);
963
+
964
+ return () => {
965
+ element.removeEventListener('login', onLogin);
966
+ element.removeEventListener('error', onError);
967
+ };
968
+ }, [navigate, sessionId, shortAppId, language]);
969
+
970
+ if (!sessionId) {
971
+ return (
972
+ <section>
973
+ <h1>Loading...</h1>
974
+ </section>
975
+ );
976
+ }
977
+
978
+ return (
979
+ <section>
980
+ <sty-notifications></sty-notifications>
981
+ <sty-login ref={loginRef}></sty-login>
982
+ <sty-language-selector></sty-language-selector>
983
+ </section>
984
+ );
985
+ }
986
+ ```
987
+
988
+ #### Logout
989
+
990
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
991
+
992
+ ```tsx
993
+ // pages/Logout.tsx
994
+ import { useStrivacity } from '@strivacity/sdk-react';
995
+ import { useEffect } from 'react';
996
+
997
+ export default function Logout() {
998
+ const { loading, logout } = useStrivacity();
999
+
1000
+ useEffect(() => {
1001
+ if (loading) {
1002
+ return;
1003
+ }
1004
+
1005
+ void logout();
1006
+ }, [loading, logout]);
1007
+
1008
+ return null;
1009
+ }
1010
+ ```
1011
+
1012
+ > The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
1013
+
1014
+ #### Token management
1015
+
1016
+ Call these methods to manage the session and access token.
1017
+
1018
+ ```tsx
1019
+ import { useStrivacity } from '@strivacity/sdk-react';
1020
+
1021
+ export function TokenPanel() {
1022
+ const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
1023
+
1024
+ async function onRefresh() {
1025
+ // Refresh the access token using the refresh token
1026
+ await refresh();
1027
+ }
1028
+
1029
+ async function onRevoke() {
1030
+ // Revoke all tokens at the authorization server and clear the local session
1031
+ await revoke();
1032
+ }
1033
+
1034
+ if (loading) {
1035
+ return null;
1036
+ }
1037
+
1038
+ // idTokenClaims, accessToken, and refreshToken are plain values from context -
1039
+ // they update automatically whenever the SDK's session changes
1040
+ return (
1041
+ <div>
1042
+ <button onClick={onRefresh}>Refresh</button>
1043
+ <button onClick={onRevoke}>Revoke</button>
1044
+ <pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
1045
+ </div>
1046
+ );
1047
+ }
1048
+ ```
1049
+
1050
+ ---
1051
+
1052
+ ### native mode
1053
+
1054
+ > For details on how this mode works, see the [native journey documentation](https://docs.strivacity.com/reference/native-journey).
1055
+
1056
+ You build the entire login UI with your own components. `useNativeLogin()` drives a "headless" auth flow: instead of redirecting to a hosted page, the SDK returns a JSON description of the current screen that you render yourself, submit each form step with `submitForm()`, and repeat until the flow finalizes automatically.
1057
+
1058
+ > The example below shows a simplified custom implementation. For a complete native renderer with all widget types, see the [example app](../../apps/react/src/components/auth/native/NativeLogin.tsx).
1059
+
1060
+ #### Login / Register
1061
+
1062
+ Create a login page using `useNativeLogin`:
1063
+
1064
+ ```tsx
1065
+ // pages/Login.tsx
1066
+ import { useNativeLogin } from '@strivacity/sdk-react';
1067
+
1068
+ export default function Login() {
1069
+ const searchParams = new URLSearchParams(window.location.search);
1070
+
1071
+ const { state, forms, messages, loading, submitForm, setFormValue } = useNativeLogin({
1072
+ params: {
1073
+ prompt: 'login', // use 'create' to open the registration flow instead
1074
+ language: 'en-US', // set the UI language (BCP 47 language tag)
1075
+ sdk: 'web-minimal', // rendering mode: 'web-minimal' for simplified rendering (see below), 'web' (default) for full rendering hints and branding
1076
+ sessionId: searchParams.get('session_id'), // pass a session ID to resume an existing flow
1077
+ },
1078
+ onLogin: async () => {
1079
+ window.location.href = '/profile';
1080
+ },
1081
+ onClose: () => {
1082
+ window.location.reload();
1083
+ },
1084
+ onError: async (error) => {
1085
+ window.location.href = `/error?message=${encodeURIComponent(error.message)}`;
1086
+ },
1087
+ onFallback: (error) => {
1088
+ // Fallback to hosted journey if native widget not supported
1089
+ window.location.href = error.url.toString();
1090
+ },
1091
+ onGlobalMessage: (message) => {
1092
+ alert(message.text);
1093
+ },
1094
+ });
1095
+
1096
+ if (loading || !state.screen) {
1097
+ return (
1098
+ <section>
1099
+ <h1>Loading...</h1>
1100
+ </section>
1101
+ );
1102
+ }
1103
+
1104
+ if (state.screen === 'identifier') {
1105
+ return (
1106
+ <section>
1107
+ <h2>Sign In</h2>
1108
+ <form
1109
+ onSubmit={async (event) => {
1110
+ event.preventDefault();
1111
+ await submitForm('identifier');
1112
+ }}
1113
+ >
1114
+ <input
1115
+ type="text"
1116
+ placeholder="Email"
1117
+ value={(forms['identifier']?.identifier as string) ?? ''}
1118
+ onChange={(event) => setFormValue('identifier', 'identifier', event.target.value)}
1119
+ />
1120
+ {messages['identifier']?.identifier && <div className="error">{messages['identifier'].identifier.text}</div>}
1121
+ <button type="submit">Continue</button>
1122
+ </form>
1123
+ </section>
1124
+ );
1125
+ }
1126
+
1127
+ if (state.screen === 'password') {
1128
+ return (
1129
+ <section>
1130
+ <h2>Enter Password</h2>
1131
+ <form
1132
+ onSubmit={async (event) => {
1133
+ event.preventDefault();
1134
+ await submitForm('password');
1135
+ }}
1136
+ >
1137
+ <input
1138
+ type="password"
1139
+ placeholder="Password"
1140
+ value={(forms['password']?.password as string) ?? ''}
1141
+ onChange={(event) => setFormValue('password', 'password', event.target.value)}
1142
+ />
1143
+ {messages['password']?.password && <div className="error">{messages['password'].password.text}</div>}
1144
+ <button type="submit">Sign In</button>
1145
+ </form>
1146
+ </section>
1147
+ );
1148
+ }
1149
+
1150
+ return null;
1151
+ }
1152
+ ```
1153
+
1154
+ #### Handle the callback
1155
+
1156
+ No separate callback page is needed. Once `state.finalizeUrl` is set, `submitForm()` automatically finalizes the session internally to exchange the authorization code for tokens and store it.
1157
+
1158
+ #### Externally-initiated flows (entry)
1159
+
1160
+ For flows started externally (e.g. a password reset email link), the user lands on the entry URL you configured in your Strivacity application native client settings. Call `entry()` on that landing page to resolve the flow parameters from the IDP (`session_id`, `short_app_id`, `language`).
1161
+
1162
+ You have two options:
1163
+
1164
+ **Option 1: Redirect to a separate login page**
1165
+
1166
+ ```tsx
1167
+ // pages/Entry.tsx
1168
+ import { useStrivacity } from '@strivacity/sdk-react';
1169
+ import { useEffect } from 'react';
1170
+ import { useNavigate } from 'react-router';
1171
+ import type { NativeFlow } from '@strivacity/sdk-react';
1172
+
1173
+ export default function Entry() {
1174
+ const { loading, entry } = useStrivacity<NativeFlow>();
1175
+ const navigate = useNavigate();
1176
+
1177
+ useEffect(() => {
1178
+ if (loading) {
1179
+ return;
1180
+ }
1181
+
1182
+ entry()
1183
+ .then((data) => {
1184
+ const params = new URLSearchParams({
1185
+ session_id: data.session_id,
1186
+ short_app_id: data.short_app_id,
1187
+ language: data.language,
1188
+ });
1189
+ window.location.href = `/login?${params}`;
1190
+ })
1191
+ .catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
1192
+ }, [loading, entry, navigate]);
1193
+
1194
+ return (
1195
+ <section>
1196
+ <h1>Loading...</h1>
1197
+ </section>
1198
+ );
1199
+ }
1200
+ ```
1201
+
1202
+ Then on your login page, read query parameters from the URL and pass it to `useNativeLogin` to resume the flow, exactly as shown in the Login / Register example above:
1203
+
1204
+ ```tsx
1205
+ // pages/Login.tsx
1206
+ import { useNativeLogin } from '@strivacity/sdk-react';
1207
+
1208
+ export default function Login() {
1209
+ const searchParams = new URLSearchParams(window.location.search);
1210
+
1211
+ const { state, loading, forms, messages, submitForm, setFormValue } = useNativeLogin({
1212
+ params: {
1213
+ sessionId: searchParams.get('session_id'),
1214
+ language: searchParams.get('language'),
1215
+ },
1216
+ onLogin: async () => {
1217
+ window.location.href = '/profile';
1218
+ },
1219
+ });
1220
+
1221
+ // ...render based on `state.screen` as shown in the Login / Register example above
1222
+ }
1223
+ ```
1224
+
1225
+ **Option 2: Render login on the entry page**
1226
+
1227
+ Call `useNativeLogin` directly inside `Entry.tsx`, feeding it the `session_id` resolved from `entry()` - no redirect needed:
1228
+
1229
+ ```tsx
1230
+ // pages/Entry.tsx
1231
+ import { useStrivacity, useNativeLogin } from '@strivacity/sdk-react';
1232
+ import { useEffect, useState } from 'react';
1233
+ import { useNavigate } from 'react-router';
1234
+ import type { NativeFlow } from '@strivacity/sdk-react';
1235
+
1236
+ export default function Entry() {
1237
+ const { loading: sdkLoading, entry } = useStrivacity<NativeFlow>();
1238
+ const navigate = useNavigate();
1239
+ const [sessionId, setSessionId] = useState<string | null>(null);
1240
+ const [language, setLanguage] = useState<string | null>(null);
1241
+ const [ready, setReady] = useState(false);
1242
+
1243
+ useEffect(() => {
1244
+ if (sdkLoading) {
1245
+ return;
1246
+ }
1247
+
1248
+ entry()
1249
+ .then((data) => {
1250
+ setSessionId(data.session_id);
1251
+ setLanguage(data.language);
1252
+ setReady(true);
1253
+ })
1254
+ .catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
1255
+ }, [sdkLoading, entry, navigate]);
1256
+
1257
+ const { state, loading, forms, messages, submitForm, setFormValue } = useNativeLogin({
1258
+ params: { sessionId, language },
1259
+ onLogin: async () => {
1260
+ await navigate('/profile');
1261
+ },
1262
+ onError: async (error) => {
1263
+ await navigate(`/error?message=${encodeURIComponent(error.message)}`);
1264
+ },
1265
+ });
1266
+
1267
+ if (!ready || loading || !state.screen) {
1268
+ return (
1269
+ <section>
1270
+ <h1>Loading...</h1>
1271
+ </section>
1272
+ );
1273
+ }
1274
+
1275
+ // ...render based on `state.screen` as shown in the Login / Register example above
1276
+ return null;
1277
+ }
1278
+ ```
1279
+
1280
+ #### Logout
1281
+
1282
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
1283
+
1284
+ ```tsx
1285
+ // pages/Logout.tsx
1286
+ import { useStrivacity } from '@strivacity/sdk-react';
1287
+ import { useEffect } from 'react';
1288
+
1289
+ export default function Logout() {
1290
+ const { loading, logout } = useStrivacity();
1291
+
1292
+ useEffect(() => {
1293
+ if (loading) {
1294
+ return;
1295
+ }
1296
+
1297
+ void logout();
1298
+ }, [loading, logout]);
1299
+
1300
+ return null;
1301
+ }
1302
+ ```
1303
+
1304
+ > The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
1305
+
1306
+ #### Token management
1307
+
1308
+ Call these methods to manage the session and access token.
1309
+
1310
+ ```tsx
1311
+ import { useStrivacity } from '@strivacity/sdk-react';
1312
+
1313
+ export function TokenPanel() {
1314
+ const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
1315
+
1316
+ async function onRefresh() {
1317
+ // Refresh the access token using the refresh token
1318
+ await refresh();
1319
+ }
1320
+
1321
+ async function onRevoke() {
1322
+ // Revoke all tokens at the authorization server and clear the local session
1323
+ await revoke();
1324
+ }
1325
+
1326
+ if (loading) {
1327
+ return null;
1328
+ }
1329
+
1330
+ // idTokenClaims, accessToken, and refreshToken are plain values from context -
1331
+ // they update automatically whenever the SDK's session changes
1332
+ return (
1333
+ <div>
1334
+ <button onClick={onRefresh}>Refresh</button>
1335
+ <button onClick={onRevoke}>Revoke</button>
1336
+ <pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
1337
+ </div>
1338
+ );
1339
+ }
1340
+ ```
1341
+
1342
+ ---
1343
+
1344
+ ## Hooks API
1345
+
1346
+ ### useStrivacity
1347
+
1348
+ The main hook for accessing the SDK instance and authentication state.
1349
+
1350
+ ```ts
1351
+ import { useStrivacity } from '@strivacity/sdk-react';
1352
+ import type { RedirectFlow } from '@strivacity/sdk-react';
1353
+
1354
+ const ctx = useStrivacity<RedirectFlow>();
1355
+ ```
1356
+
1357
+ #### Returns
1358
+
1359
+ ```ts
1360
+ {
1361
+ // SDK instance (access any SDK method)
1362
+ sdk: RedirectFlow | PopupFlow | EmbeddedFlow | NativeFlow;
1363
+
1364
+ // Reactive state (plain values - the component re-renders when they change)
1365
+ loading: boolean; // True during initialization
1366
+ isAuthenticated: boolean; // True if user has valid session
1367
+ idTokenClaims: IdTokenClaims | null; // Decoded ID token claims
1368
+ accessToken: string | null; // Current access token
1369
+ refreshToken: string | null; // Current refresh token
1370
+
1371
+ // Methods (all are async)
1372
+ login(params?: LoginParams): Promise<void>; // Start login flow
1373
+ register(params?: LoginParams): Promise<void>; // Start registration flow
1374
+ handleCallback(url?: string): Promise<void>; // Handle OAuth callback
1375
+ logout(params?: LogoutParams): Promise<void>; // End session
1376
+ refresh(): Promise<void>; // Refresh access token
1377
+ revoke(): Promise<void>; // Revoke tokens
1378
+ entry(): Promise<EntryData>; // Handle external entry (embedded/native only)
1379
+ }
1380
+ ```
1381
+
1382
+ ### useNativeLogin
1383
+
1384
+ Hook for managing native login flow state. Only available in `native` mode.
1385
+
1386
+ ```ts
1387
+ import { useNativeLogin } from '@strivacity/sdk-react';
1388
+ import type { NativeParams } from '@strivacity/sdk-react';
1389
+
1390
+ const ctx = useNativeLogin({
1391
+ params: { /* login params */ },
1392
+ onLogin: (session) => { /* handle login */ },
1393
+ onError: (error) => { /* handle error */ },
1394
+ // ... other callbacks
1395
+ });
1396
+ ```
1397
+
1398
+ #### Options
1399
+
1400
+ ```ts
1401
+ {
1402
+ params?: NativeParams; // Initial flow parameters
1403
+ onLogin?: (session: SessionData) => void | Promise<void>; // Called on successful login
1404
+ onClose?: () => void; // Called when user closes the flow
1405
+ onError?: (error: unknown) => void; // Called on error
1406
+ onFallback?: (error: FallbackError) => void; // Called when fallback needed
1407
+ onGlobalMessage?: (message: NativeFlowMessage) => void; // Called for global messages
1408
+ }
1409
+ ```
1410
+
1411
+ #### Returns
1412
+
1413
+ ```ts
1414
+ {
1415
+ // Reactive state (plain values - the component re-renders when they change)
1416
+ loading: boolean; // True while fetching next screen
1417
+ state: Partial<NativeFlowState>; // Current flow state (screen, forms, layout, etc.)
1418
+ forms: Record<string, Record<string, unknown>>; // Form data by form ID
1419
+ messages: Record<string, Record<string, NativeFlowMessage>>; // Validation messages
1420
+
1421
+ // Methods
1422
+ submitForm(formId: string, customBody?: Record<string, unknown>): Promise<void>; // Submit a form and advance to next screen
1423
+ setFormValue(formId: string, widgetId: string, value: unknown): void; // Update a single field value before submission
1424
+ setMessage(formId: string, widgetId: string, value: NativeFlowMessage): void; // Set a validation/info message on a widget
1425
+ triggerFallback(message?: string): void; // Manually trigger fallback to hosted journey
1426
+ triggerClose(): void; // Signal that the login flow was closed by the user
1427
+ }
1428
+ ```
1429
+
1430
+ ### withAuthGuard
1431
+
1432
+ Higher-order component that guards a component with authentication. Waits for the SDK to finish loading, checks the session, and redirects to the login page if the user is not authenticated.
1433
+
1434
+ ```tsx
1435
+ import { withAuthGuard } from '@strivacity/sdk-react';
1436
+
1437
+ export default withAuthGuard(
1438
+ function Profile() {
1439
+ return <section>Protected content</section>;
1440
+ },
1441
+ { loginUri: '/login' },
1442
+ );
1443
+ ```
1444
+
1445
+ #### Options
1446
+
1447
+ ```ts
1448
+ {
1449
+ loginUri?: string; // URI to redirect to if the user is not authenticated (default: '/login')
1450
+ onLoading?: () => ReactNode; // Optional render function shown while loading/checking authentication
1451
+ }
1452
+ ```
1453
+
1454
+ ---
1455
+
1456
+ ## Route guards
1457
+
1458
+ Protect routes that require authentication with the `withAuthGuard` higher-order component. It waits for the SDK to finish loading, verifies the session, and redirects to the login page if the user is not authenticated:
1459
+
1460
+ ```tsx
1461
+ // pages/Profile.tsx
1462
+ import { withAuthGuard, useStrivacity } from '@strivacity/sdk-react';
1463
+
1464
+ export default withAuthGuard(
1465
+ function Profile() {
1466
+ const { idTokenClaims } = useStrivacity();
1467
+
1468
+ return <section>Hello, {idTokenClaims?.given_name}</section>;
1469
+ },
1470
+ { loginUri: '/login' },
1471
+ );
1472
+ ```
1473
+
1474
+ Then register the guarded component as a regular route in your router setup:
1475
+
1476
+ ```tsx
1477
+ // main.tsx
1478
+ import { Routes, Route } from 'react-router';
1479
+ import Profile from './pages/Profile';
1480
+
1481
+ <Routes>
1482
+ <Route path="/profile" element={<Profile />} />
1483
+ </Routes>;
1484
+ ```
1485
+
1486
+ ---
1487
+
1488
+ ## Shared features
1489
+
1490
+ The React SDK is built on top of the core SDK and supports all its features:
1491
+
1492
+ - **[Storages](../sdk-core/README.md#storages)** - localStorage, sessionStorage, IndexedDB, Cache API, Memory, Worker
1493
+ - **[SDK events](../sdk-core/README.md#sdk-events)** - Subscribe to authentication lifecycle events
1494
+ - **[Logging](../sdk-core/README.md#logging)** - Built-in and custom logger support
1495
+ - **[HTTP client](../sdk-core/README.md#http-client)** - Custom HTTP client integration
1496
+ - **[Error handling](../sdk-core/README.md#error-handling)** - Typed error classes for different failure scenarios
1497
+ - **[Utility functions](../sdk-core/README.md#utility-functions)** - Base64URL, JWT decoding, encryption, etc.
1498
+ - **[Caching](../sdk-core/README.md#caching)** - OIDC metadata and JWKS caching
1499
+
1500
+ ---
1501
+
1502
+ ## Configuration reference
1503
+
1504
+ The React SDK accepts the same configuration as the core SDK. For detailed information about each option, see the [core SDK configuration reference](../sdk-core/README.md#configuration-reference).
1505
+
1506
+ ---
1507
+
1508
+ ## Advanced
1509
+
1510
+ ### Ionic / Capacitor support
1511
+
1512
+ The SDK works with Ionic and Capacitor applications. For native mobile platforms (iOS/Android), you need to override the default `urlHandler`, `callbackHandler`, and `storage` implementations to integrate with Capacitor plugins.
1513
+
1514
+ ```tsx
1515
+ // main.tsx
1516
+ import { createRoot } from 'react-dom/client';
1517
+ import { StyAuthProvider, SDKStorage, SDKHttpClient, LocalStorage } from '@strivacity/sdk-react';
1518
+ import { Capacitor, CapacitorHttp } from '@capacitor/core';
1519
+ import { Preferences } from '@capacitor/preferences';
1520
+ import { InAppBrowser } from '@capacitor/inappbrowser';
1521
+ import { redirectUrlHandler, redirectCallbackHandler } from '@strivacity/sdk-core/utils/handlers';
1522
+ import type { PluginListenerHandle } from '@capacitor/core';
1523
+ import type { HttpClientResponse } from '@strivacity/sdk-react';
1524
+ import { App } from './App';
1525
+
1526
+ // Custom HTTP client using Capacitor's HTTP plugin
1527
+ class CapacitorHttpClient extends SDKHttpClient {
1528
+ async request<T>(url: string, options?: RequestInit): Promise<HttpClientResponse<T>> {
1529
+ const response = await CapacitorHttp.request({
1530
+ url,
1531
+ method: options?.method || 'GET',
1532
+ headers: (options?.headers as Record<string, string>) || {},
1533
+ data: options?.body,
1534
+ webFetchExtra: options,
1535
+ });
1536
+
1537
+ return {
1538
+ headers: new Headers(response.headers),
1539
+ ok: response.status >= 200 && response.status < 300,
1540
+ status: response.status,
1541
+ statusText: '',
1542
+ url: response.url,
1543
+ json: () => Promise.resolve(response.data),
1544
+ text: () => Promise.resolve(response.data),
1545
+ };
1546
+ }
1547
+ }
1548
+
1549
+ // Custom storage using Capacitor Preferences
1550
+ class CapacitorStorage extends SDKStorage {
1551
+ async get(key: string): Promise<string | null> {
1552
+ const { value } = await Preferences.get({ key });
1553
+ return value;
1554
+ }
1555
+
1556
+ async set(key: string, value: string): Promise<void> {
1557
+ await Preferences.set({ key, value });
1558
+ }
1559
+
1560
+ async delete(key: string): Promise<void> {
1561
+ await Preferences.remove({ key });
1562
+ }
1563
+ }
1564
+
1565
+ createRoot(document.getElementById('app')!).render(
1566
+ <StyAuthProvider
1567
+ options={{
1568
+ mode: 'redirect',
1569
+ issuer: 'https://<YOUR_TENANT_DOMAIN>',
1570
+ clientId: 'YOUR_CLIENT_ID',
1571
+ redirectUri: 'https://your-app.example.com/callback',
1572
+ scopes: ['openid', 'profile', 'email'],
1573
+
1574
+ // Use Capacitor HTTP on native platforms
1575
+ httpClient: CapacitorHttpClient,
1576
+
1577
+ // Use Capacitor Preferences on native platforms, localStorage on web
1578
+ storage: Capacitor.getPlatform() === 'web' ? LocalStorage : CapacitorStorage,
1579
+
1580
+ // Handle URL redirects with InAppBrowser on native platforms
1581
+ async urlHandler(url, responseMode) {
1582
+ if (Capacitor.getPlatform() === 'web') {
1583
+ return redirectUrlHandler(url, responseMode);
1584
+ } else {
1585
+ await InAppBrowser.openInWebView({
1586
+ url,
1587
+ options: { /* Configure InAppBrowser options */ },
1588
+ });
1589
+ }
1590
+ },
1591
+
1592
+ // Handle OAuth callback with InAppBrowser listeners on native platforms
1593
+ async callbackHandler(url, responseMode) {
1594
+ if (Capacitor.getPlatform() === 'web') {
1595
+ return redirectCallbackHandler(url, responseMode);
1596
+ }
1597
+
1598
+ return new Promise(async (resolve, reject) => {
1599
+ let navigationListener: PluginListenerHandle | null = null;
1600
+ let finishListener: PluginListenerHandle | null = null;
1601
+ let userCancelled = true;
1602
+
1603
+ const cleanupListeners = async () => {
1604
+ if (navigationListener) {
1605
+ await navigationListener.remove();
1606
+ navigationListener = null;
1607
+ }
1608
+ if (finishListener) {
1609
+ await finishListener.remove();
1610
+ finishListener = null;
1611
+ }
1612
+ };
1613
+
1614
+ try {
1615
+ // Listen for page navigation in InAppBrowser
1616
+ navigationListener = await InAppBrowser.addListener(
1617
+ 'browserPageNavigationCompleted',
1618
+ async (event) => {
1619
+ const navigatedUrl = event.url;
1620
+
1621
+ // Check if the navigated URL matches our callback URL
1622
+ if (navigatedUrl && navigatedUrl.startsWith(url)) {
1623
+ try {
1624
+ const urlInstance = new URL(navigatedUrl);
1625
+ const dataString = responseMode === 'query'
1626
+ ? urlInstance.search
1627
+ : urlInstance.hash;
1628
+ const params = Object.fromEntries(
1629
+ new URLSearchParams(dataString.slice(1))
1630
+ );
1631
+
1632
+ userCancelled = false;
1633
+ await InAppBrowser.close();
1634
+ resolve(params);
1635
+ } catch (error) {
1636
+ await InAppBrowser.close();
1637
+ reject(error);
1638
+ }
1639
+ }
1640
+ }
1641
+ );
1642
+
1643
+ // Listen for browser close event
1644
+ finishListener = await InAppBrowser.addListener('browserClosed', async () => {
1645
+ await cleanupListeners();
1646
+
1647
+ if (userCancelled) {
1648
+ reject(new Error('InAppBrowser flow cancelled by user.'));
1649
+ }
1650
+ });
1651
+ } catch (error) {
1652
+ await cleanupListeners();
1653
+ reject(error);
1654
+ }
1655
+ });
1656
+ },
1657
+ }}
1658
+ >
1659
+ <App />
1660
+ </StyAuthProvider>,
1661
+ );
1662
+ ```
1663
+
1664
+ > For more details on custom handlers and storage implementations, see the [core SDK advanced section](../sdk-core/README.md#advanced).
1665
+
1666
+ ### Server-side session (BFF) mode
1667
+
1668
+ Set `serverSessionUri` to route login through your own server instead of talking to the identity provider directly from the browser - tokens are then stored server-side and never reach client-side JavaScript. See [Server-side session management](../sdk-core/README.md#server-side-session-management) in the core SDK README for the full explanation and how to read the session on the client.
1669
+
1670
+ ```tsx
1671
+ // main.tsx
1672
+ createRoot(document.getElementById('app')!).render(
1673
+ <StyAuthProvider
1674
+ options={{
1675
+ mode: 'redirect',
1676
+ issuer: 'https://<YOUR_TENANT_DOMAIN>',
1677
+ clientId: 'YOUR_CLIENT_ID',
1678
+ redirectUri: 'https://your-app.example.com/callback',
1679
+ scopes: ['openid', 'profile', 'email'],
1680
+ serverSessionUri: '/api/auth/login', // Requests are routed through your server; tokens are NOT written to client storage
1681
+ }}
1682
+ >
1683
+ <App />
1684
+ </StyAuthProvider>,
1685
+ );
1686
+ ```
1687
+
1688
+ See [`apps/react`](../../apps/react) paired with [`apps/backend`](../../apps/backend) for a working example. See also [Using the backend app with SPA apps](../../README.md#using-the-backend-app-with-spa-apps) in the root README.
1689
+
1690
+ ---
1691
+
1692
+ ## Migration guide
1693
+
1694
+ ### Migrating to v4.0
1695
+
1696
+ v4 replaces the SDK's class-based flow architecture with function-based architecture, and adds first-class support for server-managed (BFF) sessions. `StyAuthProvider`, `useStrivacity()`, `useNativeLogin()`, and the built-in `redirect`/`popup`/`embedded`/`native` modes are unchanged - only apps that used `mode: 'custom'` or drove `native` mode through the old `NativeFlowHandler` need to update their code.
1697
+
1698
+ #### Class-based flows replaced by functions
1699
+
1700
+ In v3, flows were classes (`RedirectFlow`, `PopupFlow`, `NativeFlow`, `EmbeddedFlow`), and the only way to customize behavior beyond the built-in modes - for example, to proxy authentication through your own backend in a bespoke way - was `mode: 'custom'` with a `customFlow` class that extended one of them and override its methods:
1701
+
1702
+ ```ts
1703
+ // v3
1704
+ import { NativeFlow } from '@strivacity/sdk-core/flows/NativeFlow';
1705
+
1706
+ export class CustomNativeFlow extends NativeFlow {
1707
+ override async refresh(): Promise<void> {
1708
+ // ...
1709
+ }
1710
+ }
1711
+ ```
1712
+
1713
+ v4 removes `mode: 'custom'`, the `customFlow` option, and the flow classes entirely. In their place, `createBaseFlow` (from `@strivacity/sdk-core/flows/base`) is a factory function that returns a plain object of methods closing over shared state - build your own flow by composing it, without extending anything:
1714
+
1715
+ ```ts
1716
+ // v4
1717
+ import { createBaseFlow } from '@strivacity/sdk-core/flows/base';
1718
+ import { getDefaultFlowState, getSDKOptions } from '@strivacity/sdk-core/utils';
1719
+ import type { SDKInitConfig, SDKOptions } from '@strivacity/sdk-core/types';
1720
+
1721
+ export function createCustomFlow(initConfig: SDKInitConfig) {
1722
+ const state = getDefaultFlowState();
1723
+ const options = getSDKOptions<SDKOptions>(state, initConfig);
1724
+ const base = createBaseFlow(state, options);
1725
+
1726
+ async function refresh(): Promise<void> {
1727
+ // ...
1728
+ }
1729
+
1730
+ return { ...base, refresh };
1731
+ }
1732
+ ```
1733
+
1734
+ This is a low-level `@strivacity/sdk-core` primitive - it's used the same way no matter which framework package you build on top of it. Wire it up by adding `factory: createCustomFlow` to the `options` prop passed to `StyAuthProvider` - see [Custom flow](../sdk-core/README.md#custom-flow) in the core SDK README for the full pattern and usage example.
1735
+
1736
+ #### Server-managed sessions (BFF) are now built in
1737
+
1738
+ In v3, routing authentication through your own backend meant writing a custom flow class like the one above yourself: manually calling `fetch()` against hand-written endpoints, and reimplementing PKCE/state handling, CSRF protection, and server-side token storage on your own.
1739
+
1740
+ `@strivacity/sdk-react` doesn't ship its own server - it's a client SDK for SPAs, meant to pair with a separate backend. v4 replaces the old hand-rolled approach with a single option, `serverSessionUri`, on `StyAuthProvider`, paired with the Server SDK (`createBaseServerSDK` from `@strivacity/sdk-core/server`) running on that backend - PKCE, state, and session storage are all handled by the Server SDK:
1741
+
1742
+ ```tsx
1743
+ // v4
1744
+ <StyAuthProvider
1745
+ options={{
1746
+ // ...
1747
+ serverSessionUri: '/api/auth/login', // requests are routed through your server; tokens are never written to client storage
1748
+ }}
1749
+ >
1750
+ <App />
1751
+ </StyAuthProvider>
1752
+ ```
1753
+
1754
+ See [`apps/react`](../../apps/react) paired with [`apps/backend`](../../apps/backend) for a working example.
1755
+
1756
+ #### Native mode: no more `NativeFlowHandler`
1757
+
1758
+ In v3, `native` mode's `login()`/`register()` returned a separate `NativeFlowHandler` instance, and the flow was driven through that handler:
1759
+
1760
+ ```ts
1761
+ // v3
1762
+ const handler = await sdk.login();
1763
+ const state = await handler.startSession(sessionId);
1764
+ const nextState = await handler.submitForm('formId', { identifier: 'user@example.com' });
1765
+ await handler.finalizeSession(nextState.finalizeUrl);
1766
+ ```
1767
+
1768
+ v4 moves `startSession()`, `submitForm()`, and `finalizeSession()` directly onto the flow itself - in `@strivacity/sdk-react` this is wrapped for you by the [`useNativeLogin()`](#usenativelogin) hook:
1769
+
1770
+ ```ts
1771
+ import { useNativeLogin } from '@strivacity/sdk-react';
1772
+
1773
+ // v4
1774
+ const { state, forms, messages, submitForm } = useNativeLogin({
1775
+ params: { sessionId },
1776
+ });
1777
+
1778
+ await submitForm('formId');
1779
+ ```
1780
+
1781
+ Update any code that calls `login()`/`register()` and drives the returned handler in `native` mode to use `useNativeLogin()` instead.
640
1782
 
641
1783
  ## Migrating to v3.0
642
1784
 
643
1785
  ### Entry API Major Changes
644
1786
 
645
1787
  Strivacity SDK's `entry()` API now returns a structured object instead of a plain string. Check the example above in the usage section for more details.
1788
+
1789
+ ---
1790
+
1791
+ ## Vulnerability Reporting
1792
+
1793
+ The [Guidelines for responsible disclosure](https://www.strivacity.com/report-a-security-issue) details the procedure for disclosing security issues. Please do not report security vulnerabilities on the public issue tracker.
1794
+
1795
+ ## License
1796
+
1797
+ This package is available under the MIT License. See the [LICENSE](https://github.com/Strivacity/sdk-js/blob/main/LICENSE) file for more info.
1798
+
1799
+ ## Contributing
1800
+
1801
+ Please see our [contributing guide](https://github.com/Strivacity/sdk-js/blob/main/CONTRIBUTING.md).