@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/hydrate/index.mjs CHANGED
@@ -5614,8 +5614,43 @@ class AuthheroWidget {
5614
5614
  /**
5615
5615
  * API endpoint to fetch the initial screen from.
5616
5616
  * If provided, the widget will fetch the screen on load.
5617
+ * Can include {screenId} placeholder which will be replaced with the current screen.
5618
+ * Example: "/u2/screen/{screenId}" or "https://auth.example.com/u2/screen/{screenId}"
5617
5619
  */
5618
5620
  apiUrl;
5621
+ /**
5622
+ * Base URL for all API calls. Used when widget is embedded on a different domain.
5623
+ * If not provided, relative URLs are used.
5624
+ * Example: "https://auth.example.com"
5625
+ */
5626
+ baseUrl;
5627
+ /**
5628
+ * Login session state token. Required for social login and maintaining session.
5629
+ */
5630
+ state;
5631
+ /**
5632
+ * Current screen ID. Used with apiUrl to fetch screen configuration.
5633
+ * When statePersistence is 'url', this is synced with the URL.
5634
+ */
5635
+ screenId;
5636
+ /**
5637
+ * OAuth/OIDC parameters for social login redirects.
5638
+ * Can be passed as a JSON string or object.
5639
+ */
5640
+ authParams;
5641
+ /**
5642
+ * Where to persist state and screen ID.
5643
+ * - 'url': Updates URL path/query (default for standalone pages)
5644
+ * - 'session': Uses sessionStorage (for embedded widgets)
5645
+ * - 'memory': No persistence, state only in memory
5646
+ * @default 'memory'
5647
+ */
5648
+ statePersistence = "memory";
5649
+ /**
5650
+ * Storage key prefix for session/local storage persistence.
5651
+ * @default 'authhero_widget'
5652
+ */
5653
+ storageKey = "authhero_widget";
5619
5654
  /**
5620
5655
  * Branding configuration from AuthHero API.
5621
5656
  * Controls logo, primary color, and page background.
@@ -5640,10 +5675,21 @@ class AuthheroWidget {
5640
5675
  * @default false
5641
5676
  */
5642
5677
  autoSubmit = false;
5678
+ /**
5679
+ * Whether the widget should handle navigation automatically.
5680
+ * When true, social login buttons redirect, links navigate, etc.
5681
+ * When false, only events are emitted.
5682
+ * @default false (same as autoSubmit when not specified)
5683
+ */
5684
+ autoNavigate;
5643
5685
  /**
5644
5686
  * Internal parsed screen state.
5645
5687
  */
5646
5688
  _screen;
5689
+ /**
5690
+ * Internal parsed auth params state.
5691
+ */
5692
+ _authParams;
5647
5693
  /**
5648
5694
  * Internal parsed branding state.
5649
5695
  */
@@ -5690,12 +5736,12 @@ class AuthheroWidget {
5690
5736
  */
5691
5737
  screenChange;
5692
5738
  watchScreen(newValue) {
5693
- if (typeof newValue === 'string') {
5739
+ if (typeof newValue === "string") {
5694
5740
  try {
5695
5741
  this._screen = JSON.parse(newValue);
5696
5742
  }
5697
5743
  catch {
5698
- console.error('Failed to parse screen JSON');
5744
+ console.error("Failed to parse screen JSON");
5699
5745
  }
5700
5746
  }
5701
5747
  else {
@@ -5706,12 +5752,12 @@ class AuthheroWidget {
5706
5752
  }
5707
5753
  }
5708
5754
  watchBranding(newValue) {
5709
- if (typeof newValue === 'string') {
5755
+ if (typeof newValue === "string") {
5710
5756
  try {
5711
5757
  this._branding = JSON.parse(newValue);
5712
5758
  }
5713
5759
  catch {
5714
- console.error('Failed to parse branding JSON');
5760
+ console.error("Failed to parse branding JSON");
5715
5761
  }
5716
5762
  }
5717
5763
  else {
@@ -5720,12 +5766,12 @@ class AuthheroWidget {
5720
5766
  this.applyThemeStyles();
5721
5767
  }
5722
5768
  watchTheme(newValue) {
5723
- if (typeof newValue === 'string') {
5769
+ if (typeof newValue === "string") {
5724
5770
  try {
5725
5771
  this._theme = JSON.parse(newValue);
5726
5772
  }
5727
5773
  catch {
5728
- console.error('Failed to parse theme JSON');
5774
+ console.error("Failed to parse theme JSON");
5729
5775
  }
5730
5776
  }
5731
5777
  else {
@@ -5733,6 +5779,19 @@ class AuthheroWidget {
5733
5779
  }
5734
5780
  this.applyThemeStyles();
5735
5781
  }
5782
+ watchAuthParams(newValue) {
5783
+ if (typeof newValue === "string") {
5784
+ try {
5785
+ this._authParams = JSON.parse(newValue);
5786
+ }
5787
+ catch {
5788
+ console.error("Failed to parse authParams JSON");
5789
+ }
5790
+ }
5791
+ else {
5792
+ this._authParams = newValue;
5793
+ }
5794
+ }
5736
5795
  /**
5737
5796
  * Apply branding and theme as CSS custom properties
5738
5797
  */
@@ -5740,36 +5799,164 @@ class AuthheroWidget {
5740
5799
  const vars = mergeThemeVars(this._branding, this._theme);
5741
5800
  applyCssVars(this.el, vars);
5742
5801
  }
5802
+ /**
5803
+ * Get the effective autoNavigate value (defaults to autoSubmit if not set)
5804
+ */
5805
+ get shouldAutoNavigate() {
5806
+ return this.autoNavigate ?? this.autoSubmit;
5807
+ }
5808
+ /**
5809
+ * Build the full URL for API calls
5810
+ */
5811
+ buildUrl(path) {
5812
+ if (this.baseUrl) {
5813
+ return new URL(path, this.baseUrl).toString();
5814
+ }
5815
+ return path;
5816
+ }
5817
+ /**
5818
+ * Load state from URL or storage based on statePersistence setting
5819
+ */
5820
+ loadPersistedState() {
5821
+ if (this.statePersistence === "url") {
5822
+ const url = new URL(window.location.href);
5823
+ const stateParam = url.searchParams.get("state");
5824
+ if (stateParam && !this.state) {
5825
+ this.state = stateParam;
5826
+ }
5827
+ }
5828
+ else if (this.statePersistence === "session") {
5829
+ try {
5830
+ const stored = sessionStorage.getItem(`${this.storageKey}_state`);
5831
+ if (stored && !this.state) {
5832
+ this.state = stored;
5833
+ }
5834
+ const storedScreenId = sessionStorage.getItem(`${this.storageKey}_screenId`);
5835
+ if (storedScreenId && !this.screenId) {
5836
+ this.screenId = storedScreenId;
5837
+ }
5838
+ }
5839
+ catch {
5840
+ // sessionStorage not available
5841
+ }
5842
+ }
5843
+ }
5844
+ /**
5845
+ * Save state to URL or storage based on statePersistence setting
5846
+ */
5847
+ persistState() {
5848
+ if (this.statePersistence === "url") {
5849
+ const url = new URL(window.location.href);
5850
+ if (this.state) {
5851
+ url.searchParams.set("state", this.state);
5852
+ }
5853
+ if (this.screenId) {
5854
+ url.searchParams.set("screen", this.screenId);
5855
+ }
5856
+ window.history.replaceState({}, "", url.toString());
5857
+ }
5858
+ else if (this.statePersistence === "session") {
5859
+ try {
5860
+ if (this.state) {
5861
+ sessionStorage.setItem(`${this.storageKey}_state`, this.state);
5862
+ }
5863
+ if (this.screenId) {
5864
+ sessionStorage.setItem(`${this.storageKey}_screenId`, this.screenId);
5865
+ }
5866
+ }
5867
+ catch {
5868
+ // sessionStorage not available
5869
+ }
5870
+ }
5871
+ }
5743
5872
  async componentWillLoad() {
5744
5873
  // Parse initial props
5745
5874
  this.watchScreen(this.screen);
5746
5875
  this.watchBranding(this.branding);
5747
5876
  this.watchTheme(this.theme);
5877
+ this.watchAuthParams(this.authParams);
5878
+ // Load persisted state if available
5879
+ this.loadPersistedState();
5748
5880
  // Fetch screen from API if URL provided and no screen prop
5749
5881
  if (this.apiUrl && !this._screen) {
5750
- await this.fetchScreen();
5882
+ await this.fetchScreen(this.screenId);
5751
5883
  }
5752
5884
  }
5753
- async fetchScreen() {
5885
+ /**
5886
+ * Fetch screen configuration from the API
5887
+ * @param screenIdOverride Optional screen ID to fetch (overrides this.screenId)
5888
+ * @param nodeId Optional node ID for flow navigation
5889
+ */
5890
+ async fetchScreen(screenIdOverride, nodeId) {
5754
5891
  if (!this.apiUrl)
5755
5892
  return;
5893
+ const currentScreenId = screenIdOverride || this.screenId;
5894
+ // Build the API URL, replacing {screenId} placeholder if present
5895
+ let url = this.apiUrl;
5896
+ if (currentScreenId && url.includes("{screenId}")) {
5897
+ url = url.replace("{screenId}", encodeURIComponent(currentScreenId));
5898
+ }
5899
+ // Add state and nodeId as query params
5900
+ const urlObj = new URL(url, this.baseUrl || window.location.origin);
5901
+ if (this.state) {
5902
+ urlObj.searchParams.set("state", this.state);
5903
+ }
5904
+ if (nodeId) {
5905
+ urlObj.searchParams.set("nodeId", nodeId);
5906
+ }
5756
5907
  this.loading = true;
5757
5908
  try {
5758
- const response = await fetch(this.apiUrl, {
5759
- credentials: 'include',
5909
+ const response = await fetch(this.buildUrl(urlObj.pathname + urlObj.search), {
5910
+ credentials: "include",
5760
5911
  headers: {
5761
- Accept: 'application/json',
5912
+ Accept: "application/json",
5762
5913
  },
5763
5914
  });
5764
5915
  if (response.ok) {
5765
- this._screen = await response.json();
5916
+ const data = await response.json();
5917
+ // Handle different response formats
5918
+ if (data.screen) {
5919
+ this._screen = data.screen;
5920
+ if (data.branding) {
5921
+ this._branding = data.branding;
5922
+ this.applyThemeStyles();
5923
+ }
5924
+ // Update state if returned
5925
+ if (data.state) {
5926
+ this.state = data.state;
5927
+ }
5928
+ // Update screenId if returned in response
5929
+ if (data.screenId) {
5930
+ this.screenId = data.screenId;
5931
+ }
5932
+ }
5933
+ else {
5934
+ // Response is the screen itself
5935
+ this._screen = data;
5936
+ }
5766
5937
  if (this._screen) {
5938
+ // If we fetched with a screenId override, update our stored screenId
5939
+ if (currentScreenId && currentScreenId !== this.screenId) {
5940
+ this.screenId = currentScreenId;
5941
+ }
5767
5942
  this.screenChange.emit(this._screen);
5943
+ this.persistState();
5768
5944
  }
5769
5945
  }
5946
+ else {
5947
+ const error = await response
5948
+ .json()
5949
+ .catch(() => ({ message: "Failed to load screen" }));
5950
+ this.flowError.emit({
5951
+ message: error.message || "Failed to load screen",
5952
+ });
5953
+ }
5770
5954
  }
5771
5955
  catch (error) {
5772
- console.error('Failed to fetch screen:', error);
5956
+ console.error("Failed to fetch screen:", error);
5957
+ this.flowError.emit({
5958
+ message: error instanceof Error ? error.message : "Failed to fetch screen",
5959
+ });
5773
5960
  }
5774
5961
  finally {
5775
5962
  this.loading = false;
@@ -5797,17 +5984,17 @@ class AuthheroWidget {
5797
5984
  // Submit to the server
5798
5985
  this.loading = true;
5799
5986
  try {
5800
- const response = await fetch(this._screen.action, {
5987
+ const response = await fetch(this.buildUrl(this._screen.action), {
5801
5988
  method: this._screen.method,
5802
- credentials: 'include',
5989
+ credentials: "include",
5803
5990
  headers: {
5804
- 'Content-Type': 'application/json',
5805
- Accept: 'application/json',
5991
+ "Content-Type": "application/json",
5992
+ Accept: "application/json",
5806
5993
  },
5807
5994
  body: JSON.stringify({ data: this.formData }),
5808
5995
  });
5809
- const contentType = response.headers.get('content-type');
5810
- if (contentType?.includes('application/json')) {
5996
+ const contentType = response.headers.get("content-type");
5997
+ if (contentType?.includes("application/json")) {
5811
5998
  const result = await response.json();
5812
5999
  // Handle different response types
5813
6000
  if (result.redirect) {
@@ -5815,17 +6002,31 @@ class AuthheroWidget {
5815
6002
  this.flowComplete.emit({ redirectUrl: result.redirect });
5816
6003
  // Also emit navigate for backwards compatibility
5817
6004
  this.navigate.emit({ url: result.redirect });
6005
+ // Auto-navigate if enabled
6006
+ if (this.shouldAutoNavigate) {
6007
+ window.location.href = result.redirect;
6008
+ }
5818
6009
  }
5819
6010
  else if (result.screen) {
5820
6011
  // Next screen
5821
6012
  this._screen = result.screen;
5822
6013
  this.formData = {};
5823
6014
  this.screenChange.emit(result.screen);
6015
+ // Update screenId if returned in response
6016
+ if (result.screenId) {
6017
+ this.screenId = result.screenId;
6018
+ }
6019
+ this.persistState();
5824
6020
  // Apply branding if included
5825
6021
  if (result.branding) {
5826
6022
  this._branding = result.branding;
5827
6023
  this.applyThemeStyles();
5828
6024
  }
6025
+ // Update state if returned
6026
+ if (result.state) {
6027
+ this.state = result.state;
6028
+ this.persistState();
6029
+ }
5829
6030
  }
5830
6031
  else if (result.complete) {
5831
6032
  // Flow complete without redirect
@@ -5839,9 +6040,9 @@ class AuthheroWidget {
5839
6040
  }
5840
6041
  }
5841
6042
  catch (err) {
5842
- console.error('Form submission failed:', err);
6043
+ console.error("Form submission failed:", err);
5843
6044
  this.flowError.emit({
5844
- message: err instanceof Error ? err.message : 'Form submission failed',
6045
+ message: err instanceof Error ? err.message : "Form submission failed",
5845
6046
  });
5846
6047
  }
5847
6048
  finally {
@@ -5850,14 +6051,79 @@ class AuthheroWidget {
5850
6051
  };
5851
6052
  handleButtonClick = (detail) => {
5852
6053
  // If this is a submit button click, trigger form submission
5853
- if (detail.type === 'submit') {
6054
+ if (detail.type === "submit") {
5854
6055
  // Create a synthetic submit event and call handleSubmit
5855
6056
  const syntheticEvent = { preventDefault: () => { } };
5856
6057
  this.handleSubmit(syntheticEvent);
5857
6058
  return;
5858
6059
  }
6060
+ // Always emit the event
5859
6061
  this.buttonClick.emit(detail);
6062
+ // Handle social login if autoNavigate is enabled
6063
+ if (detail.type === "SOCIAL" && detail.value && this.shouldAutoNavigate) {
6064
+ this.handleSocialLogin(detail.value);
6065
+ return;
6066
+ }
6067
+ // Handle resend button
6068
+ if (detail.type === "RESEND_BUTTON" && this.shouldAutoNavigate) {
6069
+ this.handleResend();
6070
+ return;
6071
+ }
5860
6072
  };
6073
+ /**
6074
+ * Handle social login redirect
6075
+ */
6076
+ handleSocialLogin(connection) {
6077
+ const params = this._authParams || {};
6078
+ const queryParams = {
6079
+ connection,
6080
+ };
6081
+ // Add state
6082
+ if (this.state) {
6083
+ queryParams.state = this.state;
6084
+ }
6085
+ else if (params.state) {
6086
+ queryParams.state = params.state;
6087
+ }
6088
+ // Add client_id
6089
+ if (params.client_id) {
6090
+ queryParams.client_id = params.client_id;
6091
+ }
6092
+ // Add optional params
6093
+ if (params.redirect_uri)
6094
+ queryParams.redirect_uri = params.redirect_uri;
6095
+ if (params.scope)
6096
+ queryParams.scope = params.scope;
6097
+ if (params.audience)
6098
+ queryParams.audience = params.audience;
6099
+ if (params.nonce)
6100
+ queryParams.nonce = params.nonce;
6101
+ if (params.response_type)
6102
+ queryParams.response_type = params.response_type;
6103
+ const socialUrl = this.buildUrl("/authorize?" + new URLSearchParams(queryParams).toString());
6104
+ // Emit navigate event and redirect
6105
+ this.navigate.emit({ url: socialUrl });
6106
+ window.location.href = socialUrl;
6107
+ }
6108
+ /**
6109
+ * Handle resend button click (e.g., resend OTP code)
6110
+ */
6111
+ async handleResend() {
6112
+ if (!this._screen?.action)
6113
+ return;
6114
+ try {
6115
+ const url = this._screen.action +
6116
+ (this._screen.action.includes("?") ? "&" : "?") +
6117
+ "action=resend";
6118
+ await fetch(this.buildUrl(url), {
6119
+ method: "POST",
6120
+ credentials: "include",
6121
+ });
6122
+ }
6123
+ catch (error) {
6124
+ console.error("Resend failed:", error);
6125
+ }
6126
+ }
5861
6127
  handleLinkClick = (e, link) => {
5862
6128
  // Emit the event so the consuming app can handle it
5863
6129
  this.linkClick.emit({
@@ -5865,9 +6131,9 @@ class AuthheroWidget {
5865
6131
  href: link.href,
5866
6132
  text: link.text,
5867
6133
  });
5868
- // If autoSubmit is enabled, let the browser handle the navigation
6134
+ // If autoNavigate is enabled, let the browser handle the navigation
5869
6135
  // Otherwise, prevent default and let the app decide
5870
- if (!this.autoSubmit) {
6136
+ if (!this.shouldAutoNavigate) {
5871
6137
  e.preventDefault();
5872
6138
  }
5873
6139
  };
@@ -5875,13 +6141,13 @@ class AuthheroWidget {
5875
6141
  * Get error messages from the screen-level messages array.
5876
6142
  */
5877
6143
  getScreenErrors() {
5878
- return this._screen?.messages?.filter((m) => m.type === 'error') || [];
6144
+ return this._screen?.messages?.filter((m) => m.type === "error") || [];
5879
6145
  }
5880
6146
  /**
5881
6147
  * Get success messages from the screen-level messages array.
5882
6148
  */
5883
6149
  getScreenSuccesses() {
5884
- return this._screen?.messages?.filter((m) => m.type === 'success') || [];
6150
+ return this._screen?.messages?.filter((m) => m.type === "success") || [];
5885
6151
  }
5886
6152
  /**
5887
6153
  * Sort components by order.
@@ -5899,13 +6165,13 @@ class AuthheroWidget {
5899
6165
  isSocialComponent(component) {
5900
6166
  // Check the type property directly - FormComponent has a 'type' field
5901
6167
  // SocialField has type 'SOCIAL'
5902
- return component.type === 'SOCIAL';
6168
+ return component.type === "SOCIAL";
5903
6169
  }
5904
6170
  /**
5905
6171
  * Check if a component is a divider.
5906
6172
  */
5907
6173
  isDividerComponent(component) {
5908
- return component.type === 'DIVIDER';
6174
+ return component.type === "DIVIDER";
5909
6175
  }
5910
6176
  render() {
5911
6177
  if (this.loading && !this._screen) {
@@ -5918,12 +6184,22 @@ class AuthheroWidget {
5918
6184
  const screenSuccesses = this.getScreenSuccesses();
5919
6185
  const components = this.getOrderedComponents();
5920
6186
  // Separate social, divider, and field components for layout ordering
5921
- const socialComponents = components.filter(c => this.isSocialComponent(c));
5922
- const fieldComponents = components.filter(c => !this.isSocialComponent(c) && !this.isDividerComponent(c));
5923
- const hasDivider = components.some(c => this.isDividerComponent(c));
6187
+ const socialComponents = components.filter((c) => this.isSocialComponent(c));
6188
+ const fieldComponents = components.filter((c) => !this.isSocialComponent(c) && !this.isDividerComponent(c));
6189
+ const hasDivider = components.some((c) => this.isDividerComponent(c));
5924
6190
  // Get logo URL from theme.widget (takes precedence) or branding
5925
6191
  const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
5926
- return (hAsync("div", { class: "widget-container", part: "container" }, hAsync("header", { class: "widget-header", part: "header" }, logoUrl && (hAsync("div", { class: "logo-wrapper", part: "logo-wrapper" }, hAsync("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), this._screen.title && (hAsync("h1", { class: "title", part: "title" }, this._screen.title)), this._screen.description && (hAsync("p", { class: "description", part: "description" }, this._screen.description))), hAsync("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (hAsync("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (hAsync("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), hAsync("form", { onSubmit: this.handleSubmit, part: "form" }, hAsync("div", { class: "form-content" }, socialComponents.length > 0 && (hAsync("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (hAsync("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 && fieldComponents.length > 0 && hasDivider && (hAsync("div", { class: "divider", part: "divider" }, hAsync("span", { class: "divider-text" }, "Or"))), hAsync("div", { class: "fields-section", part: "fields-section" }, fieldComponents.map((component) => (hAsync("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 && (hAsync("div", { class: "links", part: "links" }, this._screen.links.map((link) => (hAsync("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (hAsync("span", null, link.text, ' ', hAsync("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, { id: link.id, href: link.href, text: link.linkText || link.text }) }, link.linkText))) : (hAsync("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, { id: link.id, href: link.href, text: link.text }) }, link.text))))))))));
6192
+ return (hAsync("div", { class: "widget-container", part: "container" }, hAsync("header", { class: "widget-header", part: "header" }, logoUrl && (hAsync("div", { class: "logo-wrapper", part: "logo-wrapper" }, hAsync("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), this._screen.title && (hAsync("h1", { class: "title", part: "title" }, this._screen.title)), this._screen.description && (hAsync("p", { class: "description", part: "description" }, this._screen.description))), hAsync("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (hAsync("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (hAsync("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), hAsync("form", { onSubmit: this.handleSubmit, part: "form" }, hAsync("div", { class: "form-content" }, socialComponents.length > 0 && (hAsync("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (hAsync("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 &&
6193
+ fieldComponents.length > 0 &&
6194
+ hasDivider && (hAsync("div", { class: "divider", part: "divider" }, hAsync("span", { class: "divider-text" }, "Or"))), hAsync("div", { class: "fields-section", part: "fields-section" }, fieldComponents.map((component) => (hAsync("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 && (hAsync("div", { class: "links", part: "links" }, this._screen.links.map((link) => (hAsync("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (hAsync("span", null, link.text, " ", hAsync("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
6195
+ id: link.id,
6196
+ href: link.href,
6197
+ text: link.linkText || link.text,
6198
+ }) }, link.linkText))) : (hAsync("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
6199
+ id: link.id,
6200
+ href: link.href,
6201
+ text: link.text,
6202
+ }) }, link.text))))))))));
5927
6203
  }
5928
6204
  static get watchers() { return {
5929
6205
  "screen": [{
@@ -5934,6 +6210,9 @@ class AuthheroWidget {
5934
6210
  }],
5935
6211
  "theme": [{
5936
6212
  "watchTheme": 0
6213
+ }],
6214
+ "authParams": [{
6215
+ "watchAuthParams": 0
5937
6216
  }]
5938
6217
  }; }
5939
6218
  static get style() { return authheroWidgetCss(); }
@@ -5943,11 +6222,19 @@ class AuthheroWidget {
5943
6222
  "$members$": {
5944
6223
  "screen": [1],
5945
6224
  "apiUrl": [1, "api-url"],
6225
+ "baseUrl": [1, "base-url"],
6226
+ "state": [1025],
6227
+ "screenId": [1025, "screen-id"],
6228
+ "authParams": [1, "auth-params"],
6229
+ "statePersistence": [1, "state-persistence"],
6230
+ "storageKey": [1, "storage-key"],
5946
6231
  "branding": [1],
5947
6232
  "theme": [1],
5948
6233
  "loading": [1028],
5949
6234
  "autoSubmit": [4, "auto-submit"],
6235
+ "autoNavigate": [4, "auto-navigate"],
5950
6236
  "_screen": [32],
6237
+ "_authParams": [32],
5951
6238
  "_branding": [32],
5952
6239
  "_theme": [32],
5953
6240
  "formData": [32]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authhero/widget",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Server-Driven UI widget for AuthHero authentication flows",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1 +0,0 @@
1
- import{r as t,c as i,g as e,h as r}from"./p-BzFenraS.js";function s(t,i){if(void 0!==i)return`${i}px`;switch(t){case"pill":return"9999px";case"rounded":return"8px";case"sharp":return"0";default:return}}function a(t){if(!t)return{};const i={};if(t.colors?.primary&&(i["--ah-color-primary"]=t.colors.primary,i["--ah-color-primary-hover"]=t.colors.primary),t.colors?.page_background){const e=t.colors.page_background;"solid"===e.type&&e.start?i["--ah-page-bg"]=e.start:"gradient"===e.type&&e.start&&e.end&&(i["--ah-page-bg"]=`linear-gradient(${e.angle_deg??180}deg, ${e.start}, ${e.end})`)}return t.logo_url&&(i["--ah-logo-url"]=`url(${t.logo_url})`),t.font?.url&&(i["--ah-font-url"]=t.font.url),i}function o(t){if(!t)return{};const i={};if(t.borders){const e=t.borders;void 0!==e.widget_corner_radius&&(i["--ah-widget-radius"]=`${e.widget_corner_radius}px`),void 0!==e.widget_border_weight&&(i["--ah-widget-border-width"]=`${e.widget_border_weight}px`),!1===e.show_widget_shadow&&(i["--ah-widget-shadow"]="none");const r=s(e.buttons_style,e.button_border_radius);r&&(i["--ah-btn-radius"]=r),void 0!==e.button_border_weight&&(i["--ah-btn-border-width"]=`${e.button_border_weight}px`);const a=s(e.inputs_style,e.input_border_radius);a&&(i["--ah-input-radius"]=a),void 0!==e.input_border_weight&&(i["--ah-input-border-width"]=`${e.input_border_weight}px`)}if(t.colors){const e=t.colors;e.primary_button&&(i["--ah-color-primary"]=e.primary_button,i["--ah-color-primary-hover"]=e.primary_button),e.primary_button_label&&(i["--ah-btn-primary-text"]=e.primary_button_label),e.secondary_button_border&&(i["--ah-btn-secondary-border"]=e.secondary_button_border),e.secondary_button_label&&(i["--ah-btn-secondary-text"]=e.secondary_button_label),e.body_text&&(i["--ah-color-text"]=e.body_text),e.header&&(i["--ah-color-text-header"]=e.header),e.input_labels_placeholders&&(i["--ah-color-text-label"]=e.input_labels_placeholders,i["--ah-color-text-muted"]=e.input_labels_placeholders),e.input_filled_text&&(i["--ah-color-input-text"]=e.input_filled_text),e.widget_background&&(i["--ah-color-bg"]=e.widget_background),e.input_background&&(i["--ah-color-input-bg"]=e.input_background),e.widget_border&&(i["--ah-widget-border-color"]=e.widget_border),e.input_border&&(i["--ah-color-border"]=e.input_border),e.links_focused_components&&(i["--ah-color-link"]=e.links_focused_components),e.base_focus_color&&(i["--ah-color-focus-ring"]=e.base_focus_color),e.base_hover_color&&(i["--ah-color-primary-hover"]=e.base_hover_color),e.error&&(i["--ah-color-error"]=e.error),e.success&&(i["--ah-color-success"]=e.success),e.icons&&(i["--ah-color-icon"]=e.icons)}if(t.fonts){const e=t.fonts,r=e.reference_text_size||16;if(e.font_url&&(i["--ah-font-url"]=e.font_url),e.reference_text_size&&(i["--ah-font-size-base"]=`${e.reference_text_size}px`),e.title?.size){const t=Math.round(e.title.size/100*r);i["--ah-font-size-title"]=`${t}px`}if(e.subtitle?.size){const t=Math.round(e.subtitle.size/100*r);i["--ah-font-size-subtitle"]=`${t}px`}if(e.body_text?.size){const t=Math.round(e.body_text.size/100*r);i["--ah-font-size-body"]=`${t}px`}if(e.input_labels?.size){const t=Math.round(e.input_labels.size/100*r);i["--ah-font-size-label"]=`${t}px`}if(e.buttons_text?.size){const t=Math.round(e.buttons_text.size/100*r);i["--ah-font-size-btn"]=`${t}px`}if(e.links?.size){const t=Math.round(e.links.size/100*r);i["--ah-font-size-link"]=`${t}px`}"underlined"===e.links_style&&(i["--ah-link-decoration"]="underline"),void 0!==e.title?.bold&&(i["--ah-font-weight-title"]=e.title.bold?"700":"400"),void 0!==e.subtitle?.bold&&(i["--ah-font-weight-subtitle"]=e.subtitle.bold?"700":"400"),void 0!==e.body_text?.bold&&(i["--ah-font-weight-body"]=e.body_text.bold?"700":"400"),void 0!==e.input_labels?.bold&&(i["--ah-font-weight-label"]=e.input_labels.bold?"700":"400"),void 0!==e.buttons_text?.bold&&(i["--ah-font-weight-btn"]=e.buttons_text.bold?"600":"400"),void 0!==e.links?.bold&&(i["--ah-font-weight-link"]=e.links.bold?"700":"400")}if(t.widget){const e=t.widget;if(e.header_text_alignment&&(i["--ah-title-align"]=e.header_text_alignment),e.logo_height&&(i["--ah-logo-height"]=`${e.logo_height}px`),e.logo_position){const t={center:"center",left:"flex-start",right:"flex-end"};"none"===e.logo_position?i["--ah-logo-display"]="none":i["--ah-logo-align"]=t[e.logo_position]??"center"}e.social_buttons_layout&&("top"===e.social_buttons_layout?(i["--ah-social-order"]="0",i["--ah-divider-order"]="1",i["--ah-fields-order"]="2"):(i["--ah-social-order"]="2",i["--ah-divider-order"]="1",i["--ah-fields-order"]="0"))}if(t.page_background){const e=t.page_background;e.background_color&&(i["--ah-page-bg"]=e.background_color),e.background_image_url&&(i["--ah-page-bg-image"]=`url(${e.background_image_url})`)}return i}const n=class{constructor(e){t(this,e),this.formSubmit=i(this,"formSubmit"),this.buttonClick=i(this,"buttonClick"),this.linkClick=i(this,"linkClick"),this.navigate=i(this,"navigate"),this.flowComplete=i(this,"flowComplete"),this.flowError=i(this,"flowError"),this.screenChange=i(this,"screenChange")}get el(){return e(this)}screen;apiUrl;branding;theme;loading=!1;autoSubmit=!1;_screen;_branding;_theme;formData={};formSubmit;buttonClick;linkClick;navigate;flowComplete;flowError;screenChange;watchScreen(t){if("string"==typeof t)try{this._screen=JSON.parse(t)}catch{console.error("Failed to parse screen JSON")}else this._screen=t;this._screen&&this.screenChange.emit(this._screen)}watchBranding(t){if("string"==typeof t)try{this._branding=JSON.parse(t)}catch{console.error("Failed to parse branding JSON")}else this._branding=t;this.applyThemeStyles()}watchTheme(t){if("string"==typeof t)try{this._theme=JSON.parse(t)}catch{console.error("Failed to parse theme JSON")}else this._theme=t;this.applyThemeStyles()}applyThemeStyles(){const t=(i=this._theme,{...a(this._branding),...o(i)});var i;!function(t,i){Object.entries(i).forEach((([i,e])=>{t.style.setProperty(i,e)}))}(this.el,t)}async componentWillLoad(){this.watchScreen(this.screen),this.watchBranding(this.branding),this.watchTheme(this.theme),this.apiUrl&&!this._screen&&await this.fetchScreen()}async fetchScreen(){if(this.apiUrl){this.loading=!0;try{const t=await fetch(this.apiUrl,{credentials:"include",headers:{Accept:"application/json"}});t.ok&&(this._screen=await t.json(),this._screen&&this.screenChange.emit(this._screen))}catch(t){console.error("Failed to fetch screen:",t)}finally{this.loading=!1}}}handleInputChange=(t,i)=>{this.formData={...this.formData,[t]:i}};handleSubmit=async t=>{if(t.preventDefault(),this._screen&&(this.formSubmit.emit({screen:this._screen,data:this.formData}),this.autoSubmit)){this.loading=!0;try{const t=await fetch(this._screen.action,{method:this._screen.method,credentials:"include",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({data:this.formData})}),i=t.headers.get("content-type");if(i?.includes("application/json")){const i=await t.json();i.redirect?(this.flowComplete.emit({redirectUrl:i.redirect}),this.navigate.emit({url:i.redirect})):i.screen?(this._screen=i.screen,this.formData={},this.screenChange.emit(i.screen),i.branding&&(this._branding=i.branding,this.applyThemeStyles())):i.complete&&this.flowComplete.emit({}),!t.ok&&i.screen&&(this._screen=i.screen,this.screenChange.emit(i.screen))}}catch(t){console.error("Form submission failed:",t),this.flowError.emit({message:t instanceof Error?t.message:"Form submission failed"})}finally{this.loading=!1}}};handleButtonClick=t=>{"submit"!==t.type?this.buttonClick.emit(t):this.handleSubmit({preventDefault:()=>{}})};handleLinkClick=(t,i)=>{this.linkClick.emit({id:i.id,href:i.href,text:i.text}),this.autoSubmit||t.preventDefault()};getScreenErrors(){return this._screen?.messages?.filter((t=>"error"===t.type))||[]}getScreenSuccesses(){return this._screen?.messages?.filter((t=>"success"===t.type))||[]}getOrderedComponents(){return this._screen?[...this._screen.components].filter((t=>!1!==t.visible)).sort(((t,i)=>(t.order??0)-(i.order??0))):[]}isSocialComponent(t){return"SOCIAL"===t.type}isDividerComponent(t){return"DIVIDER"===t.type}render(){if(this.loading&&!this._screen)return r("div",{class:"widget-container"},r("div",{class:"loading-spinner"}));if(!this._screen)return r("div",{class:"widget-container"},r("div",{class:"error-message"},"No screen configuration provided"));const t=this.getScreenErrors(),i=this.getScreenSuccesses(),e=this.getOrderedComponents(),s=e.filter((t=>this.isSocialComponent(t))),a=e.filter((t=>!this.isSocialComponent(t)&&!this.isDividerComponent(t))),o=e.some((t=>this.isDividerComponent(t))),n=this._theme?.widget?.logo_url||this._branding?.logo_url;return r("div",{class:"widget-container",part:"container"},r("header",{class:"widget-header",part:"header"},n&&r("div",{class:"logo-wrapper",part:"logo-wrapper"},r("img",{class:"logo",part:"logo",src:n,alt:"Logo"})),this._screen.title&&r("h1",{class:"title",part:"title"},this._screen.title),this._screen.description&&r("p",{class:"description",part:"description"},this._screen.description)),r("div",{class:"widget-body",part:"body"},t.map((t=>r("div",{class:"message message-error",part:"message message-error",key:t.id??t.text},t.text))),i.map((t=>r("div",{class:"message message-success",part:"message message-success",key:t.id??t.text},t.text))),r("form",{onSubmit:this.handleSubmit,part:"form"},r("div",{class:"form-content"},s.length>0&&r("div",{class:"social-section",part:"social-section"},s.map((t=>r("authhero-node",{key:t.id,component:t,value:this.formData[t.id],onFieldChange:t=>this.handleInputChange(t.detail.id,t.detail.value),onButtonClick:t=>this.handleButtonClick(t.detail),disabled:this.loading})))),s.length>0&&a.length>0&&o&&r("div",{class:"divider",part:"divider"},r("span",{class:"divider-text"},"Or")),r("div",{class:"fields-section",part:"fields-section"},a.map((t=>r("authhero-node",{key:t.id,component:t,value:this.formData[t.id],onFieldChange:t=>this.handleInputChange(t.detail.id,t.detail.value),onButtonClick:t=>this.handleButtonClick(t.detail),disabled:this.loading})))))),this._screen.links&&this._screen.links.length>0&&r("div",{class:"links",part:"links"},this._screen.links.map((t=>r("span",{class:"link-wrapper",part:"link-wrapper",key:t.id??t.href},t.linkText?r("span",null,t.text," ",r("a",{href:t.href,class:"link",part:"link",onClick:i=>this.handleLinkClick(i,{id:t.id,href:t.href,text:t.linkText||t.text})},t.linkText)):r("a",{href:t.href,class:"link",part:"link",onClick:i=>this.handleLinkClick(i,{id:t.id,href:t.href,text:t.text})},t.text)))))))}static get watchers(){return{screen:[{watchScreen:0}],branding:[{watchBranding:0}],theme:[{watchTheme:0}]}}};n.style=":host{display:block;font-family:var(--ah-font-family, 'ulp-font', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, sans-serif);font-size:var(--ah-font-size-base, 14px);line-height:var(--ah-line-height-base, 1.5);color:var(--ah-color-text, #1e212a);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.widget-container{max-width:var(--ah-widget-max-width, 400px);width:100%;margin:0 auto;background-color:var(--ah-color-bg, #ffffff);border-radius:var(--ah-widget-radius, 5px);box-shadow:var(--ah-widget-shadow, 0 4px 22px 0 rgba(0, 0, 0, 0.11));box-sizing:border-box}.widget-header{padding:var(--ah-header-padding, 40px 48px 24px)}.widget-body{padding:var(--ah-body-padding, 0 48px 40px)}.logo-wrapper{display:var(--ah-logo-display, flex);justify-content:var(--ah-logo-align, center);margin-bottom:8px}.logo{display:block;height:var(--ah-logo-height, 52px);max-width:100%;width:auto;object-fit:contain}.title{font-size:var(--ah-font-size-title, 24px);font-weight:var(--ah-font-weight-title, 700);text-align:var(--ah-title-align, center);margin:var(--ah-title-margin, 24px 0 8px);color:var(--ah-color-header, #1e212a);line-height:1.2}.description{font-size:var(--ah-font-size-description, 14px);text-align:var(--ah-title-align, center);margin:var(--ah-description-margin, 0 0 8px);color:var(--ah-color-text, #1e212a);line-height:1.5}.message{padding:12px 16px;border-radius:4px;margin-bottom:16px;font-size:14px;line-height:1.5}.message-error{background-color:var(--ah-color-error-bg, #ffeaea);color:var(--ah-color-error, #d03c38);border-left:3px solid var(--ah-color-error, #d03c38)}.message-success{background-color:var(--ah-color-success-bg, #e6f9f1);color:var(--ah-color-success, #13a769);border-left:3px solid var(--ah-color-success, #13a769)}form{display:flex;flex-direction:column}.form-content{display:flex;flex-direction:column}.social-section{display:flex;flex-direction:column;gap:8px;order:var(--ah-social-order, 2)}.fields-section{display:flex;flex-direction:column;order:var(--ah-fields-order, 0)}.divider{display:flex;align-items:center;text-align:center;margin:16px 0;order:var(--ah-divider-order, 1)}.divider::before,.divider::after{content:'';flex:1;border-bottom:1px solid var(--ah-color-border-muted, #c9cace)}.divider-text{padding:0 10px;font-size:12px;font-weight:400;color:var(--ah-color-text-muted, #65676e);text-transform:uppercase;letter-spacing:0}.links{display:flex;flex-direction:column;align-items:center;gap:8px;margin-top:16px}.link-wrapper{font-size:14px;color:var(--ah-color-text, #1e212a)}.link{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:14px;font-weight:var(--ah-font-weight-link, 400);transition:color 150ms ease}.link:hover{text-decoration:underline}.link:focus-visible{outline:2px solid var(--ah-color-link, #635dff);outline-offset:2px;border-radius:2px}.loading-spinner{width:32px;height:32px;margin:24px auto;border:3px solid var(--ah-color-border-muted, #e0e1e3);border-top-color:var(--ah-color-primary, #635dff);border-radius:50%;animation:spin 0.8s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.error-message{text-align:center;color:var(--ah-color-error, #d03c38);padding:16px;font-size:14px}@media (max-width: 480px){:host{display:block;width:100%;min-height:100vh;background-color:var(--ah-color-bg, #ffffff)}.widget-container{box-shadow:none;border-radius:0;max-width:none;width:100%;margin:0}.widget-header{padding:24px 16px 16px}.widget-body{padding:0 16px 24px}}";export{n as authhero_widget}