@authhero/widget 0.7.2 → 0.8.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 +118 -66
- package/dist/authhero-widget/authhero-widget.esm.js +1 -1
- package/dist/authhero-widget/p-6e32b31d.entry.js +1 -0
- package/dist/cjs/authhero-widget.cjs.entry.js +311 -32
- package/dist/cjs/authhero-widget.cjs.js +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/authhero-widget/authhero-widget.js +470 -34
- package/dist/components/authhero-widget.js +1 -1
- package/dist/esm/authhero-widget.entry.js +311 -32
- package/dist/esm/authhero-widget.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/types/components/authhero-widget/authhero-widget.d.ts +96 -4
- package/dist/types/components.d.ts +66 -4
- package/hydrate/index.js +319 -32
- package/hydrate/index.mjs +319 -32
- package/package.json +1 -1
- package/dist/authhero-widget/p-ea2660b2.entry.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h } from "@stencil/core";
|
|
1
|
+
import { h, } from "@stencil/core";
|
|
2
2
|
import { mergeThemeVars, applyCssVars } from "../../utils/branding";
|
|
3
3
|
export class AuthheroWidget {
|
|
4
4
|
el;
|
|
@@ -11,8 +11,43 @@ export class AuthheroWidget {
|
|
|
11
11
|
/**
|
|
12
12
|
* API endpoint to fetch the initial screen from.
|
|
13
13
|
* If provided, the widget will fetch the screen on load.
|
|
14
|
+
* Can include {screenId} placeholder which will be replaced with the current screen.
|
|
15
|
+
* Example: "/u2/screen/{screenId}" or "https://auth.example.com/u2/screen/{screenId}"
|
|
14
16
|
*/
|
|
15
17
|
apiUrl;
|
|
18
|
+
/**
|
|
19
|
+
* Base URL for all API calls. Used when widget is embedded on a different domain.
|
|
20
|
+
* If not provided, relative URLs are used.
|
|
21
|
+
* Example: "https://auth.example.com"
|
|
22
|
+
*/
|
|
23
|
+
baseUrl;
|
|
24
|
+
/**
|
|
25
|
+
* Login session state token. Required for social login and maintaining session.
|
|
26
|
+
*/
|
|
27
|
+
state;
|
|
28
|
+
/**
|
|
29
|
+
* Current screen ID. Used with apiUrl to fetch screen configuration.
|
|
30
|
+
* When statePersistence is 'url', this is synced with the URL.
|
|
31
|
+
*/
|
|
32
|
+
screenId;
|
|
33
|
+
/**
|
|
34
|
+
* OAuth/OIDC parameters for social login redirects.
|
|
35
|
+
* Can be passed as a JSON string or object.
|
|
36
|
+
*/
|
|
37
|
+
authParams;
|
|
38
|
+
/**
|
|
39
|
+
* Where to persist state and screen ID.
|
|
40
|
+
* - 'url': Updates URL path/query (default for standalone pages)
|
|
41
|
+
* - 'session': Uses sessionStorage (for embedded widgets)
|
|
42
|
+
* - 'memory': No persistence, state only in memory
|
|
43
|
+
* @default 'memory'
|
|
44
|
+
*/
|
|
45
|
+
statePersistence = "memory";
|
|
46
|
+
/**
|
|
47
|
+
* Storage key prefix for session/local storage persistence.
|
|
48
|
+
* @default 'authhero_widget'
|
|
49
|
+
*/
|
|
50
|
+
storageKey = "authhero_widget";
|
|
16
51
|
/**
|
|
17
52
|
* Branding configuration from AuthHero API.
|
|
18
53
|
* Controls logo, primary color, and page background.
|
|
@@ -37,10 +72,21 @@ export class AuthheroWidget {
|
|
|
37
72
|
* @default false
|
|
38
73
|
*/
|
|
39
74
|
autoSubmit = false;
|
|
75
|
+
/**
|
|
76
|
+
* Whether the widget should handle navigation automatically.
|
|
77
|
+
* When true, social login buttons redirect, links navigate, etc.
|
|
78
|
+
* When false, only events are emitted.
|
|
79
|
+
* @default false (same as autoSubmit when not specified)
|
|
80
|
+
*/
|
|
81
|
+
autoNavigate;
|
|
40
82
|
/**
|
|
41
83
|
* Internal parsed screen state.
|
|
42
84
|
*/
|
|
43
85
|
_screen;
|
|
86
|
+
/**
|
|
87
|
+
* Internal parsed auth params state.
|
|
88
|
+
*/
|
|
89
|
+
_authParams;
|
|
44
90
|
/**
|
|
45
91
|
* Internal parsed branding state.
|
|
46
92
|
*/
|
|
@@ -87,12 +133,12 @@ export class AuthheroWidget {
|
|
|
87
133
|
*/
|
|
88
134
|
screenChange;
|
|
89
135
|
watchScreen(newValue) {
|
|
90
|
-
if (typeof newValue ===
|
|
136
|
+
if (typeof newValue === "string") {
|
|
91
137
|
try {
|
|
92
138
|
this._screen = JSON.parse(newValue);
|
|
93
139
|
}
|
|
94
140
|
catch {
|
|
95
|
-
console.error(
|
|
141
|
+
console.error("Failed to parse screen JSON");
|
|
96
142
|
}
|
|
97
143
|
}
|
|
98
144
|
else {
|
|
@@ -103,12 +149,12 @@ export class AuthheroWidget {
|
|
|
103
149
|
}
|
|
104
150
|
}
|
|
105
151
|
watchBranding(newValue) {
|
|
106
|
-
if (typeof newValue ===
|
|
152
|
+
if (typeof newValue === "string") {
|
|
107
153
|
try {
|
|
108
154
|
this._branding = JSON.parse(newValue);
|
|
109
155
|
}
|
|
110
156
|
catch {
|
|
111
|
-
console.error(
|
|
157
|
+
console.error("Failed to parse branding JSON");
|
|
112
158
|
}
|
|
113
159
|
}
|
|
114
160
|
else {
|
|
@@ -117,12 +163,12 @@ export class AuthheroWidget {
|
|
|
117
163
|
this.applyThemeStyles();
|
|
118
164
|
}
|
|
119
165
|
watchTheme(newValue) {
|
|
120
|
-
if (typeof newValue ===
|
|
166
|
+
if (typeof newValue === "string") {
|
|
121
167
|
try {
|
|
122
168
|
this._theme = JSON.parse(newValue);
|
|
123
169
|
}
|
|
124
170
|
catch {
|
|
125
|
-
console.error(
|
|
171
|
+
console.error("Failed to parse theme JSON");
|
|
126
172
|
}
|
|
127
173
|
}
|
|
128
174
|
else {
|
|
@@ -130,6 +176,19 @@ export class AuthheroWidget {
|
|
|
130
176
|
}
|
|
131
177
|
this.applyThemeStyles();
|
|
132
178
|
}
|
|
179
|
+
watchAuthParams(newValue) {
|
|
180
|
+
if (typeof newValue === "string") {
|
|
181
|
+
try {
|
|
182
|
+
this._authParams = JSON.parse(newValue);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
console.error("Failed to parse authParams JSON");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
this._authParams = newValue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
133
192
|
/**
|
|
134
193
|
* Apply branding and theme as CSS custom properties
|
|
135
194
|
*/
|
|
@@ -137,36 +196,164 @@ export class AuthheroWidget {
|
|
|
137
196
|
const vars = mergeThemeVars(this._branding, this._theme);
|
|
138
197
|
applyCssVars(this.el, vars);
|
|
139
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Get the effective autoNavigate value (defaults to autoSubmit if not set)
|
|
201
|
+
*/
|
|
202
|
+
get shouldAutoNavigate() {
|
|
203
|
+
return this.autoNavigate ?? this.autoSubmit;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Build the full URL for API calls
|
|
207
|
+
*/
|
|
208
|
+
buildUrl(path) {
|
|
209
|
+
if (this.baseUrl) {
|
|
210
|
+
return new URL(path, this.baseUrl).toString();
|
|
211
|
+
}
|
|
212
|
+
return path;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Load state from URL or storage based on statePersistence setting
|
|
216
|
+
*/
|
|
217
|
+
loadPersistedState() {
|
|
218
|
+
if (this.statePersistence === "url") {
|
|
219
|
+
const url = new URL(window.location.href);
|
|
220
|
+
const stateParam = url.searchParams.get("state");
|
|
221
|
+
if (stateParam && !this.state) {
|
|
222
|
+
this.state = stateParam;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (this.statePersistence === "session") {
|
|
226
|
+
try {
|
|
227
|
+
const stored = sessionStorage.getItem(`${this.storageKey}_state`);
|
|
228
|
+
if (stored && !this.state) {
|
|
229
|
+
this.state = stored;
|
|
230
|
+
}
|
|
231
|
+
const storedScreenId = sessionStorage.getItem(`${this.storageKey}_screenId`);
|
|
232
|
+
if (storedScreenId && !this.screenId) {
|
|
233
|
+
this.screenId = storedScreenId;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// sessionStorage not available
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Save state to URL or storage based on statePersistence setting
|
|
243
|
+
*/
|
|
244
|
+
persistState() {
|
|
245
|
+
if (this.statePersistence === "url") {
|
|
246
|
+
const url = new URL(window.location.href);
|
|
247
|
+
if (this.state) {
|
|
248
|
+
url.searchParams.set("state", this.state);
|
|
249
|
+
}
|
|
250
|
+
if (this.screenId) {
|
|
251
|
+
url.searchParams.set("screen", this.screenId);
|
|
252
|
+
}
|
|
253
|
+
window.history.replaceState({}, "", url.toString());
|
|
254
|
+
}
|
|
255
|
+
else if (this.statePersistence === "session") {
|
|
256
|
+
try {
|
|
257
|
+
if (this.state) {
|
|
258
|
+
sessionStorage.setItem(`${this.storageKey}_state`, this.state);
|
|
259
|
+
}
|
|
260
|
+
if (this.screenId) {
|
|
261
|
+
sessionStorage.setItem(`${this.storageKey}_screenId`, this.screenId);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// sessionStorage not available
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
140
269
|
async componentWillLoad() {
|
|
141
270
|
// Parse initial props
|
|
142
271
|
this.watchScreen(this.screen);
|
|
143
272
|
this.watchBranding(this.branding);
|
|
144
273
|
this.watchTheme(this.theme);
|
|
274
|
+
this.watchAuthParams(this.authParams);
|
|
275
|
+
// Load persisted state if available
|
|
276
|
+
this.loadPersistedState();
|
|
145
277
|
// Fetch screen from API if URL provided and no screen prop
|
|
146
278
|
if (this.apiUrl && !this._screen) {
|
|
147
|
-
await this.fetchScreen();
|
|
279
|
+
await this.fetchScreen(this.screenId);
|
|
148
280
|
}
|
|
149
281
|
}
|
|
150
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Fetch screen configuration from the API
|
|
284
|
+
* @param screenIdOverride Optional screen ID to fetch (overrides this.screenId)
|
|
285
|
+
* @param nodeId Optional node ID for flow navigation
|
|
286
|
+
*/
|
|
287
|
+
async fetchScreen(screenIdOverride, nodeId) {
|
|
151
288
|
if (!this.apiUrl)
|
|
152
289
|
return;
|
|
290
|
+
const currentScreenId = screenIdOverride || this.screenId;
|
|
291
|
+
// Build the API URL, replacing {screenId} placeholder if present
|
|
292
|
+
let url = this.apiUrl;
|
|
293
|
+
if (currentScreenId && url.includes("{screenId}")) {
|
|
294
|
+
url = url.replace("{screenId}", encodeURIComponent(currentScreenId));
|
|
295
|
+
}
|
|
296
|
+
// Add state and nodeId as query params
|
|
297
|
+
const urlObj = new URL(url, this.baseUrl || window.location.origin);
|
|
298
|
+
if (this.state) {
|
|
299
|
+
urlObj.searchParams.set("state", this.state);
|
|
300
|
+
}
|
|
301
|
+
if (nodeId) {
|
|
302
|
+
urlObj.searchParams.set("nodeId", nodeId);
|
|
303
|
+
}
|
|
153
304
|
this.loading = true;
|
|
154
305
|
try {
|
|
155
|
-
const response = await fetch(this.
|
|
156
|
-
credentials:
|
|
306
|
+
const response = await fetch(this.buildUrl(urlObj.pathname + urlObj.search), {
|
|
307
|
+
credentials: "include",
|
|
157
308
|
headers: {
|
|
158
|
-
Accept:
|
|
309
|
+
Accept: "application/json",
|
|
159
310
|
},
|
|
160
311
|
});
|
|
161
312
|
if (response.ok) {
|
|
162
|
-
|
|
313
|
+
const data = await response.json();
|
|
314
|
+
// Handle different response formats
|
|
315
|
+
if (data.screen) {
|
|
316
|
+
this._screen = data.screen;
|
|
317
|
+
if (data.branding) {
|
|
318
|
+
this._branding = data.branding;
|
|
319
|
+
this.applyThemeStyles();
|
|
320
|
+
}
|
|
321
|
+
// Update state if returned
|
|
322
|
+
if (data.state) {
|
|
323
|
+
this.state = data.state;
|
|
324
|
+
}
|
|
325
|
+
// Update screenId if returned in response
|
|
326
|
+
if (data.screenId) {
|
|
327
|
+
this.screenId = data.screenId;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Response is the screen itself
|
|
332
|
+
this._screen = data;
|
|
333
|
+
}
|
|
163
334
|
if (this._screen) {
|
|
335
|
+
// If we fetched with a screenId override, update our stored screenId
|
|
336
|
+
if (currentScreenId && currentScreenId !== this.screenId) {
|
|
337
|
+
this.screenId = currentScreenId;
|
|
338
|
+
}
|
|
164
339
|
this.screenChange.emit(this._screen);
|
|
340
|
+
this.persistState();
|
|
165
341
|
}
|
|
166
342
|
}
|
|
343
|
+
else {
|
|
344
|
+
const error = await response
|
|
345
|
+
.json()
|
|
346
|
+
.catch(() => ({ message: "Failed to load screen" }));
|
|
347
|
+
this.flowError.emit({
|
|
348
|
+
message: error.message || "Failed to load screen",
|
|
349
|
+
});
|
|
350
|
+
}
|
|
167
351
|
}
|
|
168
352
|
catch (error) {
|
|
169
|
-
console.error(
|
|
353
|
+
console.error("Failed to fetch screen:", error);
|
|
354
|
+
this.flowError.emit({
|
|
355
|
+
message: error instanceof Error ? error.message : "Failed to fetch screen",
|
|
356
|
+
});
|
|
170
357
|
}
|
|
171
358
|
finally {
|
|
172
359
|
this.loading = false;
|
|
@@ -194,17 +381,17 @@ export class AuthheroWidget {
|
|
|
194
381
|
// Submit to the server
|
|
195
382
|
this.loading = true;
|
|
196
383
|
try {
|
|
197
|
-
const response = await fetch(this._screen.action, {
|
|
384
|
+
const response = await fetch(this.buildUrl(this._screen.action), {
|
|
198
385
|
method: this._screen.method,
|
|
199
|
-
credentials:
|
|
386
|
+
credentials: "include",
|
|
200
387
|
headers: {
|
|
201
|
-
|
|
202
|
-
Accept:
|
|
388
|
+
"Content-Type": "application/json",
|
|
389
|
+
Accept: "application/json",
|
|
203
390
|
},
|
|
204
391
|
body: JSON.stringify({ data: this.formData }),
|
|
205
392
|
});
|
|
206
|
-
const contentType = response.headers.get(
|
|
207
|
-
if (contentType?.includes(
|
|
393
|
+
const contentType = response.headers.get("content-type");
|
|
394
|
+
if (contentType?.includes("application/json")) {
|
|
208
395
|
const result = await response.json();
|
|
209
396
|
// Handle different response types
|
|
210
397
|
if (result.redirect) {
|
|
@@ -212,17 +399,31 @@ export class AuthheroWidget {
|
|
|
212
399
|
this.flowComplete.emit({ redirectUrl: result.redirect });
|
|
213
400
|
// Also emit navigate for backwards compatibility
|
|
214
401
|
this.navigate.emit({ url: result.redirect });
|
|
402
|
+
// Auto-navigate if enabled
|
|
403
|
+
if (this.shouldAutoNavigate) {
|
|
404
|
+
window.location.href = result.redirect;
|
|
405
|
+
}
|
|
215
406
|
}
|
|
216
407
|
else if (result.screen) {
|
|
217
408
|
// Next screen
|
|
218
409
|
this._screen = result.screen;
|
|
219
410
|
this.formData = {};
|
|
220
411
|
this.screenChange.emit(result.screen);
|
|
412
|
+
// Update screenId if returned in response
|
|
413
|
+
if (result.screenId) {
|
|
414
|
+
this.screenId = result.screenId;
|
|
415
|
+
}
|
|
416
|
+
this.persistState();
|
|
221
417
|
// Apply branding if included
|
|
222
418
|
if (result.branding) {
|
|
223
419
|
this._branding = result.branding;
|
|
224
420
|
this.applyThemeStyles();
|
|
225
421
|
}
|
|
422
|
+
// Update state if returned
|
|
423
|
+
if (result.state) {
|
|
424
|
+
this.state = result.state;
|
|
425
|
+
this.persistState();
|
|
426
|
+
}
|
|
226
427
|
}
|
|
227
428
|
else if (result.complete) {
|
|
228
429
|
// Flow complete without redirect
|
|
@@ -236,9 +437,9 @@ export class AuthheroWidget {
|
|
|
236
437
|
}
|
|
237
438
|
}
|
|
238
439
|
catch (err) {
|
|
239
|
-
console.error(
|
|
440
|
+
console.error("Form submission failed:", err);
|
|
240
441
|
this.flowError.emit({
|
|
241
|
-
message: err instanceof Error ? err.message :
|
|
442
|
+
message: err instanceof Error ? err.message : "Form submission failed",
|
|
242
443
|
});
|
|
243
444
|
}
|
|
244
445
|
finally {
|
|
@@ -247,14 +448,79 @@ export class AuthheroWidget {
|
|
|
247
448
|
};
|
|
248
449
|
handleButtonClick = (detail) => {
|
|
249
450
|
// If this is a submit button click, trigger form submission
|
|
250
|
-
if (detail.type ===
|
|
451
|
+
if (detail.type === "submit") {
|
|
251
452
|
// Create a synthetic submit event and call handleSubmit
|
|
252
453
|
const syntheticEvent = { preventDefault: () => { } };
|
|
253
454
|
this.handleSubmit(syntheticEvent);
|
|
254
455
|
return;
|
|
255
456
|
}
|
|
457
|
+
// Always emit the event
|
|
256
458
|
this.buttonClick.emit(detail);
|
|
459
|
+
// Handle social login if autoNavigate is enabled
|
|
460
|
+
if (detail.type === "SOCIAL" && detail.value && this.shouldAutoNavigate) {
|
|
461
|
+
this.handleSocialLogin(detail.value);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
// Handle resend button
|
|
465
|
+
if (detail.type === "RESEND_BUTTON" && this.shouldAutoNavigate) {
|
|
466
|
+
this.handleResend();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
257
469
|
};
|
|
470
|
+
/**
|
|
471
|
+
* Handle social login redirect
|
|
472
|
+
*/
|
|
473
|
+
handleSocialLogin(connection) {
|
|
474
|
+
const params = this._authParams || {};
|
|
475
|
+
const queryParams = {
|
|
476
|
+
connection,
|
|
477
|
+
};
|
|
478
|
+
// Add state
|
|
479
|
+
if (this.state) {
|
|
480
|
+
queryParams.state = this.state;
|
|
481
|
+
}
|
|
482
|
+
else if (params.state) {
|
|
483
|
+
queryParams.state = params.state;
|
|
484
|
+
}
|
|
485
|
+
// Add client_id
|
|
486
|
+
if (params.client_id) {
|
|
487
|
+
queryParams.client_id = params.client_id;
|
|
488
|
+
}
|
|
489
|
+
// Add optional params
|
|
490
|
+
if (params.redirect_uri)
|
|
491
|
+
queryParams.redirect_uri = params.redirect_uri;
|
|
492
|
+
if (params.scope)
|
|
493
|
+
queryParams.scope = params.scope;
|
|
494
|
+
if (params.audience)
|
|
495
|
+
queryParams.audience = params.audience;
|
|
496
|
+
if (params.nonce)
|
|
497
|
+
queryParams.nonce = params.nonce;
|
|
498
|
+
if (params.response_type)
|
|
499
|
+
queryParams.response_type = params.response_type;
|
|
500
|
+
const socialUrl = this.buildUrl("/authorize?" + new URLSearchParams(queryParams).toString());
|
|
501
|
+
// Emit navigate event and redirect
|
|
502
|
+
this.navigate.emit({ url: socialUrl });
|
|
503
|
+
window.location.href = socialUrl;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Handle resend button click (e.g., resend OTP code)
|
|
507
|
+
*/
|
|
508
|
+
async handleResend() {
|
|
509
|
+
if (!this._screen?.action)
|
|
510
|
+
return;
|
|
511
|
+
try {
|
|
512
|
+
const url = this._screen.action +
|
|
513
|
+
(this._screen.action.includes("?") ? "&" : "?") +
|
|
514
|
+
"action=resend";
|
|
515
|
+
await fetch(this.buildUrl(url), {
|
|
516
|
+
method: "POST",
|
|
517
|
+
credentials: "include",
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
console.error("Resend failed:", error);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
258
524
|
handleLinkClick = (e, link) => {
|
|
259
525
|
// Emit the event so the consuming app can handle it
|
|
260
526
|
this.linkClick.emit({
|
|
@@ -262,9 +528,9 @@ export class AuthheroWidget {
|
|
|
262
528
|
href: link.href,
|
|
263
529
|
text: link.text,
|
|
264
530
|
});
|
|
265
|
-
// If
|
|
531
|
+
// If autoNavigate is enabled, let the browser handle the navigation
|
|
266
532
|
// Otherwise, prevent default and let the app decide
|
|
267
|
-
if (!this.
|
|
533
|
+
if (!this.shouldAutoNavigate) {
|
|
268
534
|
e.preventDefault();
|
|
269
535
|
}
|
|
270
536
|
};
|
|
@@ -272,13 +538,13 @@ export class AuthheroWidget {
|
|
|
272
538
|
* Get error messages from the screen-level messages array.
|
|
273
539
|
*/
|
|
274
540
|
getScreenErrors() {
|
|
275
|
-
return this._screen?.messages?.filter((m) => m.type ===
|
|
541
|
+
return this._screen?.messages?.filter((m) => m.type === "error") || [];
|
|
276
542
|
}
|
|
277
543
|
/**
|
|
278
544
|
* Get success messages from the screen-level messages array.
|
|
279
545
|
*/
|
|
280
546
|
getScreenSuccesses() {
|
|
281
|
-
return this._screen?.messages?.filter((m) => m.type ===
|
|
547
|
+
return this._screen?.messages?.filter((m) => m.type === "success") || [];
|
|
282
548
|
}
|
|
283
549
|
/**
|
|
284
550
|
* Sort components by order.
|
|
@@ -296,13 +562,13 @@ export class AuthheroWidget {
|
|
|
296
562
|
isSocialComponent(component) {
|
|
297
563
|
// Check the type property directly - FormComponent has a 'type' field
|
|
298
564
|
// SocialField has type 'SOCIAL'
|
|
299
|
-
return component.type ===
|
|
565
|
+
return component.type === "SOCIAL";
|
|
300
566
|
}
|
|
301
567
|
/**
|
|
302
568
|
* Check if a component is a divider.
|
|
303
569
|
*/
|
|
304
570
|
isDividerComponent(component) {
|
|
305
|
-
return component.type ===
|
|
571
|
+
return component.type === "DIVIDER";
|
|
306
572
|
}
|
|
307
573
|
render() {
|
|
308
574
|
if (this.loading && !this._screen) {
|
|
@@ -315,12 +581,22 @@ export class AuthheroWidget {
|
|
|
315
581
|
const screenSuccesses = this.getScreenSuccesses();
|
|
316
582
|
const components = this.getOrderedComponents();
|
|
317
583
|
// Separate social, divider, and field components for layout ordering
|
|
318
|
-
const socialComponents = components.filter(c => this.isSocialComponent(c));
|
|
319
|
-
const fieldComponents = components.filter(c => !this.isSocialComponent(c) && !this.isDividerComponent(c));
|
|
320
|
-
const hasDivider = components.some(c => this.isDividerComponent(c));
|
|
584
|
+
const socialComponents = components.filter((c) => this.isSocialComponent(c));
|
|
585
|
+
const fieldComponents = components.filter((c) => !this.isSocialComponent(c) && !this.isDividerComponent(c));
|
|
586
|
+
const hasDivider = components.some((c) => this.isDividerComponent(c));
|
|
321
587
|
// Get logo URL from theme.widget (takes precedence) or branding
|
|
322
588
|
const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
|
|
323
|
-
return (h("div", { class: "widget-container", part: "container" }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), this._screen.title && (h("h1", { class: "title", part: "title" }, this._screen.title)), this._screen.description && (h("p", { class: "description", part: "description" }, this._screen.description))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, part: "form" }, h("div", { class: "form-content" }, socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading }))))), socialComponents.length > 0 &&
|
|
589
|
+
return (h("div", { class: "widget-container", part: "container" }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), this._screen.title && (h("h1", { class: "title", part: "title" }, this._screen.title)), this._screen.description && (h("p", { class: "description", part: "description" }, this._screen.description))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, part: "form" }, h("div", { class: "form-content" }, socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading }))))), socialComponents.length > 0 &&
|
|
590
|
+
fieldComponents.length > 0 &&
|
|
591
|
+
hasDivider && (h("div", { class: "divider", part: "divider" }, h("span", { class: "divider-text" }, "Or"))), h("div", { class: "fields-section", part: "fields-section" }, fieldComponents.map((component) => (h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading })))))), this._screen.links && this._screen.links.length > 0 && (h("div", { class: "links", part: "links" }, this._screen.links.map((link) => (h("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (h("span", null, link.text, " ", h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
|
|
592
|
+
id: link.id,
|
|
593
|
+
href: link.href,
|
|
594
|
+
text: link.linkText || link.text,
|
|
595
|
+
}) }, link.linkText))) : (h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
|
|
596
|
+
id: link.id,
|
|
597
|
+
href: link.href,
|
|
598
|
+
text: link.text,
|
|
599
|
+
}) }, link.text))))))))));
|
|
324
600
|
}
|
|
325
601
|
static get is() { return "authhero-widget"; }
|
|
326
602
|
static get encapsulation() { return "shadow"; }
|
|
@@ -374,13 +650,147 @@ export class AuthheroWidget {
|
|
|
374
650
|
"optional": true,
|
|
375
651
|
"docs": {
|
|
376
652
|
"tags": [],
|
|
377
|
-
"text": "API endpoint to fetch the initial screen from.\nIf provided, the widget will fetch the screen on load."
|
|
653
|
+
"text": "API endpoint to fetch the initial screen from.\nIf provided, the widget will fetch the screen on load.\nCan include {screenId} placeholder which will be replaced with the current screen.\nExample: \"/u2/screen/{screenId}\" or \"https://auth.example.com/u2/screen/{screenId}\""
|
|
378
654
|
},
|
|
379
655
|
"getter": false,
|
|
380
656
|
"setter": false,
|
|
381
657
|
"reflect": false,
|
|
382
658
|
"attribute": "api-url"
|
|
383
659
|
},
|
|
660
|
+
"baseUrl": {
|
|
661
|
+
"type": "string",
|
|
662
|
+
"mutable": false,
|
|
663
|
+
"complexType": {
|
|
664
|
+
"original": "string",
|
|
665
|
+
"resolved": "string | undefined",
|
|
666
|
+
"references": {}
|
|
667
|
+
},
|
|
668
|
+
"required": false,
|
|
669
|
+
"optional": true,
|
|
670
|
+
"docs": {
|
|
671
|
+
"tags": [],
|
|
672
|
+
"text": "Base URL for all API calls. Used when widget is embedded on a different domain.\nIf not provided, relative URLs are used.\nExample: \"https://auth.example.com\""
|
|
673
|
+
},
|
|
674
|
+
"getter": false,
|
|
675
|
+
"setter": false,
|
|
676
|
+
"reflect": false,
|
|
677
|
+
"attribute": "base-url"
|
|
678
|
+
},
|
|
679
|
+
"state": {
|
|
680
|
+
"type": "string",
|
|
681
|
+
"mutable": true,
|
|
682
|
+
"complexType": {
|
|
683
|
+
"original": "string",
|
|
684
|
+
"resolved": "string | undefined",
|
|
685
|
+
"references": {}
|
|
686
|
+
},
|
|
687
|
+
"required": false,
|
|
688
|
+
"optional": true,
|
|
689
|
+
"docs": {
|
|
690
|
+
"tags": [],
|
|
691
|
+
"text": "Login session state token. Required for social login and maintaining session."
|
|
692
|
+
},
|
|
693
|
+
"getter": false,
|
|
694
|
+
"setter": false,
|
|
695
|
+
"reflect": false,
|
|
696
|
+
"attribute": "state"
|
|
697
|
+
},
|
|
698
|
+
"screenId": {
|
|
699
|
+
"type": "string",
|
|
700
|
+
"mutable": true,
|
|
701
|
+
"complexType": {
|
|
702
|
+
"original": "string",
|
|
703
|
+
"resolved": "string | undefined",
|
|
704
|
+
"references": {}
|
|
705
|
+
},
|
|
706
|
+
"required": false,
|
|
707
|
+
"optional": true,
|
|
708
|
+
"docs": {
|
|
709
|
+
"tags": [],
|
|
710
|
+
"text": "Current screen ID. Used with apiUrl to fetch screen configuration.\nWhen statePersistence is 'url', this is synced with the URL."
|
|
711
|
+
},
|
|
712
|
+
"getter": false,
|
|
713
|
+
"setter": false,
|
|
714
|
+
"reflect": false,
|
|
715
|
+
"attribute": "screen-id"
|
|
716
|
+
},
|
|
717
|
+
"authParams": {
|
|
718
|
+
"type": "string",
|
|
719
|
+
"mutable": false,
|
|
720
|
+
"complexType": {
|
|
721
|
+
"original": "AuthParams | string",
|
|
722
|
+
"resolved": "AuthParams | string | undefined",
|
|
723
|
+
"references": {
|
|
724
|
+
"AuthParams": {
|
|
725
|
+
"location": "local",
|
|
726
|
+
"path": "/home/runner/work/authhero/authhero/packages/ui-widget/src/components/authhero-widget/authhero-widget.tsx",
|
|
727
|
+
"id": "src/components/authhero-widget/authhero-widget.tsx::AuthParams"
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
"required": false,
|
|
732
|
+
"optional": true,
|
|
733
|
+
"docs": {
|
|
734
|
+
"tags": [],
|
|
735
|
+
"text": "OAuth/OIDC parameters for social login redirects.\nCan be passed as a JSON string or object."
|
|
736
|
+
},
|
|
737
|
+
"getter": false,
|
|
738
|
+
"setter": false,
|
|
739
|
+
"reflect": false,
|
|
740
|
+
"attribute": "auth-params"
|
|
741
|
+
},
|
|
742
|
+
"statePersistence": {
|
|
743
|
+
"type": "string",
|
|
744
|
+
"mutable": false,
|
|
745
|
+
"complexType": {
|
|
746
|
+
"original": "StatePersistence",
|
|
747
|
+
"resolved": "\"memory\" | \"session\" | \"url\"",
|
|
748
|
+
"references": {
|
|
749
|
+
"StatePersistence": {
|
|
750
|
+
"location": "local",
|
|
751
|
+
"path": "/home/runner/work/authhero/authhero/packages/ui-widget/src/components/authhero-widget/authhero-widget.tsx",
|
|
752
|
+
"id": "src/components/authhero-widget/authhero-widget.tsx::StatePersistence"
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
"required": false,
|
|
757
|
+
"optional": false,
|
|
758
|
+
"docs": {
|
|
759
|
+
"tags": [{
|
|
760
|
+
"name": "default",
|
|
761
|
+
"text": "'memory'"
|
|
762
|
+
}],
|
|
763
|
+
"text": "Where to persist state and screen ID.\n- 'url': Updates URL path/query (default for standalone pages)\n- 'session': Uses sessionStorage (for embedded widgets)\n- 'memory': No persistence, state only in memory"
|
|
764
|
+
},
|
|
765
|
+
"getter": false,
|
|
766
|
+
"setter": false,
|
|
767
|
+
"reflect": false,
|
|
768
|
+
"attribute": "state-persistence",
|
|
769
|
+
"defaultValue": "\"memory\""
|
|
770
|
+
},
|
|
771
|
+
"storageKey": {
|
|
772
|
+
"type": "string",
|
|
773
|
+
"mutable": false,
|
|
774
|
+
"complexType": {
|
|
775
|
+
"original": "string",
|
|
776
|
+
"resolved": "string",
|
|
777
|
+
"references": {}
|
|
778
|
+
},
|
|
779
|
+
"required": false,
|
|
780
|
+
"optional": false,
|
|
781
|
+
"docs": {
|
|
782
|
+
"tags": [{
|
|
783
|
+
"name": "default",
|
|
784
|
+
"text": "'authhero_widget'"
|
|
785
|
+
}],
|
|
786
|
+
"text": "Storage key prefix for session/local storage persistence."
|
|
787
|
+
},
|
|
788
|
+
"getter": false,
|
|
789
|
+
"setter": false,
|
|
790
|
+
"reflect": false,
|
|
791
|
+
"attribute": "storage-key",
|
|
792
|
+
"defaultValue": "\"authhero_widget\""
|
|
793
|
+
},
|
|
384
794
|
"branding": {
|
|
385
795
|
"type": "string",
|
|
386
796
|
"mutable": false,
|
|
@@ -475,12 +885,35 @@ export class AuthheroWidget {
|
|
|
475
885
|
"reflect": false,
|
|
476
886
|
"attribute": "auto-submit",
|
|
477
887
|
"defaultValue": "false"
|
|
888
|
+
},
|
|
889
|
+
"autoNavigate": {
|
|
890
|
+
"type": "boolean",
|
|
891
|
+
"mutable": false,
|
|
892
|
+
"complexType": {
|
|
893
|
+
"original": "boolean",
|
|
894
|
+
"resolved": "boolean | undefined",
|
|
895
|
+
"references": {}
|
|
896
|
+
},
|
|
897
|
+
"required": false,
|
|
898
|
+
"optional": true,
|
|
899
|
+
"docs": {
|
|
900
|
+
"tags": [{
|
|
901
|
+
"name": "default",
|
|
902
|
+
"text": "false (same as autoSubmit when not specified)"
|
|
903
|
+
}],
|
|
904
|
+
"text": "Whether the widget should handle navigation automatically.\nWhen true, social login buttons redirect, links navigate, etc.\nWhen false, only events are emitted."
|
|
905
|
+
},
|
|
906
|
+
"getter": false,
|
|
907
|
+
"setter": false,
|
|
908
|
+
"reflect": false,
|
|
909
|
+
"attribute": "auto-navigate"
|
|
478
910
|
}
|
|
479
911
|
};
|
|
480
912
|
}
|
|
481
913
|
static get states() {
|
|
482
914
|
return {
|
|
483
915
|
"_screen": {},
|
|
916
|
+
"_authParams": {},
|
|
484
917
|
"_branding": {},
|
|
485
918
|
"_theme": {},
|
|
486
919
|
"formData": {}
|
|
@@ -648,6 +1081,9 @@ export class AuthheroWidget {
|
|
|
648
1081
|
}, {
|
|
649
1082
|
"propName": "theme",
|
|
650
1083
|
"methodName": "watchTheme"
|
|
1084
|
+
}, {
|
|
1085
|
+
"propName": "authParams",
|
|
1086
|
+
"methodName": "watchAuthParams"
|
|
651
1087
|
}];
|
|
652
1088
|
}
|
|
653
1089
|
}
|