@hotosm/hanko-auth 0.4.6 โ†’ 0.4.8

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/src/hanko-auth.ts CHANGED
@@ -34,8 +34,7 @@ import chevronUpIcon from "../assets/chevron-up.svg";
34
34
  let hankoRegistered = false;
35
35
  let hankoRegistrationPromise: Promise<void> | null = null;
36
36
 
37
- // Pre-register translations at module load time to prevent 404 errors
38
- // This will be called again with hankoUrl when component initializes
37
+ // Pre-register translations
39
38
  async function ensureHankoRegistered(hankoUrl: string): Promise<void> {
40
39
  if (hankoRegistered) return;
41
40
  if (hankoRegistrationPromise) return hankoRegistrationPromise;
@@ -72,6 +71,7 @@ const sharedAuth = {
72
71
  initialized: false,
73
72
  instances: new Set<any>(),
74
73
  profileDisplayName: "", // Shared profile display name
74
+ profilePictureUrl: "", // Shared profile picture URL
75
75
  hankoReady: false, // used for translations
76
76
  };
77
77
 
@@ -112,26 +112,20 @@ export class HankoAuth extends LitElement {
112
112
  redirectAfterLogout = "";
113
113
  @property({ type: String, attribute: "display-name" })
114
114
  displayNameAttr = "";
115
- // URL to check if user has app mapping (for cross-app auth scenarios)
116
115
  @property({ type: String, attribute: "mapping-check-url" }) mappingCheckUrl =
117
116
  "";
118
- // App identifier for onboarding redirect
119
117
  @property({ type: String, attribute: "app-id" }) appId = "";
120
118
  // Custom login page URL (for standalone mode - overrides ${hankoUrl}/app)
121
119
  @property({ type: String, attribute: "login-url" }) loginUrl = "";
122
- // Language code (en, es, fr, pt, etc.)
123
120
  @property({ type: String, reflect: true }) lang = "en";
124
- // Button variant (filled, outline, plain)
125
121
  @property({ type: String, attribute: "button-variant" }) buttonVariant:
126
122
  | "filled"
127
123
  | "outline"
128
124
  | "plain" = "plain";
129
- // Button color (primary, neutral, danger)
130
125
  @property({ type: String, attribute: "button-color" }) buttonColor:
131
126
  | "primary"
132
127
  | "neutral"
133
128
  | "danger" = "primary";
134
- // Display mode: "default" (compact avatar button) or "bar" (full-width bar with avatar + email + chevron)
135
129
  @property({ type: String, reflect: true }) display: "default" | "bar" =
136
130
  "default";
137
131
 
@@ -144,6 +138,7 @@ export class HankoAuth extends LitElement {
144
138
  @state() private error: string | null = null;
145
139
  @state() private hankoReady = false; // Tracks when Hanko registration is complete
146
140
  @state() private profileDisplayName: string = "";
141
+ @state() private profilePictureUrl: string = "";
147
142
  @state() private hasAppMapping = false; // True if user has mapping in the app
148
143
  @state() private userProfileLanguage: string | null = null; // Language from user profile
149
144
  // dropdown
@@ -268,12 +263,12 @@ export class HankoAuth extends LitElement {
268
263
 
269
264
  // If already initialized or being initialized by another instance, sync state and skip init
270
265
  if (sharedAuth.initialized || sharedAuth.primary) {
271
- this.log("๐Ÿ”„ Using shared state from primary instance");
266
+ this.log("Using shared state from primary instance");
272
267
  this._syncFromShared();
273
268
  this._isPrimary = false;
274
269
  } else {
275
270
  // This is the first/primary instance - claim it immediately to prevent race conditions
276
- this.log("๐Ÿ‘‘ This is the primary instance");
271
+ this.log("This is the primary instance");
277
272
  this._isPrimary = true;
278
273
  sharedAuth.primary = this;
279
274
  sharedAuth.initialized = true; // Mark as initialized immediately to prevent other instances from also initializing
@@ -298,7 +293,7 @@ export class HankoAuth extends LitElement {
298
293
  if (this._isPrimary && sharedAuth.instances.size > 0) {
299
294
  const newPrimary = sharedAuth.instances.values().next().value;
300
295
  if (newPrimary) {
301
- this.log("๐Ÿ‘‘ Promoting new primary instance");
296
+ this.log("Promoting new primary instance");
302
297
  newPrimary._isPrimary = true;
303
298
  sharedAuth.primary = newPrimary;
304
299
  }
@@ -321,6 +316,8 @@ export class HankoAuth extends LitElement {
321
316
  if (this._hanko !== sharedAuth.hanko) this._hanko = sharedAuth.hanko;
322
317
  if (this.profileDisplayName !== sharedAuth.profileDisplayName)
323
318
  this.profileDisplayName = sharedAuth.profileDisplayName;
319
+ if (this.profilePictureUrl !== sharedAuth.profilePictureUrl)
320
+ this.profilePictureUrl = sharedAuth.profilePictureUrl;
324
321
  if (this.hankoReady !== sharedAuth.hankoReady)
325
322
  this.hankoReady = sharedAuth.hankoReady;
326
323
  }
@@ -332,6 +329,7 @@ export class HankoAuth extends LitElement {
332
329
  sharedAuth.osmData = this.osmData;
333
330
  sharedAuth.loading = this.loading;
334
331
  sharedAuth.profileDisplayName = this.profileDisplayName;
332
+ sharedAuth.profilePictureUrl = this.profilePictureUrl;
335
333
  sharedAuth.hankoReady = this.hankoReady;
336
334
 
337
335
  // Sync to all other instances
@@ -349,7 +347,7 @@ export class HankoAuth extends LitElement {
349
347
  if (!document.hidden && !this.showProfile && !this.user) {
350
348
  // Page became visible, we're in header mode, and no user is logged in
351
349
  // Re-check session in case user logged in elsewhere
352
- this.log("๐Ÿ‘๏ธ Page visible, re-checking session...");
350
+ this.log("Page visible, re-checking session...");
353
351
  this.checkSession();
354
352
  }
355
353
  };
@@ -402,13 +400,8 @@ export class HankoAuth extends LitElement {
402
400
  }
403
401
  }
404
402
 
405
- /**
406
- * Get translated string for the current language
407
- * Falls back to English if translation not found
408
- * When user is logged in, uses their profile language instead of the lang prop
409
- */
403
+ /* Translations */
410
404
  private t(key: keyof typeof translations.en): string {
411
- // When user is logged in, use their profile language
412
405
  const effectiveLang =
413
406
  this.user && this.userProfileLanguage
414
407
  ? this.userProfileLanguage
@@ -547,12 +540,12 @@ export class HankoAuth extends LitElement {
547
540
  this.log("๐Ÿ” Checking for existing Hanko session...");
548
541
 
549
542
  if (!this._hanko) {
550
- this.log("โš ๏ธ Hanko instance not initialized yet");
543
+ this.log("Hanko instance not initialized yet");
551
544
  return;
552
545
  }
553
546
 
554
547
  try {
555
- this.log("๐Ÿ“ก Checking session validity via cookie...");
548
+ this.log("Checking session validity via cookie...");
556
549
 
557
550
  // First, try to validate the session cookie directly with Hanko
558
551
  // This works across subdomains because the cookie has domain: .hotosm.test
@@ -574,13 +567,13 @@ export class HankoAuth extends LitElement {
574
567
  // Check if session is actually valid (endpoint returns 200 with is_valid:false when no session)
575
568
  if (sessionData.is_valid === false) {
576
569
  this.log(
577
- "โ„น๏ธ Session validation returned is_valid:false - no valid session",
570
+ "Session validation returned is_valid:false - no valid session",
578
571
  );
579
572
  return;
580
573
  }
581
574
 
582
- this.log("โœ… Valid Hanko session found via cookie");
583
- this.log("๐Ÿ“‹ Session data:", sessionData);
575
+ this.log("Valid Hanko session found via cookie");
576
+ this.log("Session data:", sessionData);
584
577
 
585
578
  // Now get the full user data from the login backend /me endpoint
586
579
  // This endpoint validates the JWT and returns complete user info
@@ -596,7 +589,7 @@ export class HankoAuth extends LitElement {
596
589
  let needsSdkFallback = true;
597
590
  if (meResponse.ok) {
598
591
  const userData = await meResponse.json();
599
- this.log("๐Ÿ‘ค User data retrieved from /me:", userData);
592
+ this.log("User data retrieved from /me:", userData);
600
593
 
601
594
  // Only use /me if it has email (login.hotosm.org has it, Hanko vanilla doesn't)
602
595
  if (userData.email) {
@@ -609,12 +602,12 @@ export class HankoAuth extends LitElement {
609
602
  };
610
603
  needsSdkFallback = false;
611
604
  } else {
612
- this.log("โš ๏ธ /me has no email, will use SDK fallback");
605
+ this.log("/me has no email, will use SDK fallback");
613
606
  }
614
607
  }
615
608
 
616
609
  if (needsSdkFallback) {
617
- this.log("๐Ÿ”„ Using SDK to get user with email");
610
+ this.log("Using SDK to get user with email");
618
611
  // Fallback to SDK method which has email
619
612
  const user = await this._hanko.user.getCurrent();
620
613
  this.user = {
@@ -625,7 +618,7 @@ export class HankoAuth extends LitElement {
625
618
  };
626
619
  }
627
620
  } catch (userError) {
628
- this.log("โš ๏ธ Failed to get user data:", userError);
621
+ this.log("Failed to get user data:", userError);
629
622
  // Last resort: use session data if available
630
623
  if (sessionData.user_id) {
631
624
  this.user = {
@@ -650,7 +643,7 @@ export class HankoAuth extends LitElement {
650
643
  !alreadyVerified
651
644
  ) {
652
645
  this.log(
653
- "๐Ÿ”„ verify-session enabled, redirecting to callback for app verification...",
646
+ "verify-session enabled, redirecting to callback for app verification...",
654
647
  );
655
648
  sessionStorage.setItem(verifyKey, "true");
656
649
  window.location.href = this.redirectAfterLogin;
@@ -687,20 +680,20 @@ export class HankoAuth extends LitElement {
687
680
  // Fetch profile display name
688
681
  await this.fetchProfileDisplayName();
689
682
  if (this.osmRequired && this.autoConnect && !this.osmConnected) {
690
- this.log("๐Ÿ”„ Auto-connecting to OSM (from existing session)...");
683
+ this.log("Auto-connecting to OSM (from existing session)...");
691
684
  this.handleOSMConnect();
692
685
  }
693
686
  }
694
687
  } else {
695
- this.log("โ„น๏ธ No valid session cookie found - user needs to login");
688
+ this.log("No valid session cookie found - user needs to login");
696
689
  }
697
690
  } catch (validateError) {
698
- this.log("โš ๏ธ Session validation failed:", validateError);
699
- this.log("โ„น๏ธ No valid session - user needs to login");
691
+ this.log("Session validation failed:", validateError);
692
+ this.log("No valid session - user needs to login");
700
693
  }
701
694
  } catch (error) {
702
- this.log("โš ๏ธ Session check error:", error);
703
- this.log("โ„น๏ธ No existing session - user needs to login");
695
+ this.log("Session check error:", error);
696
+ this.log("No existing session - user needs to login");
704
697
  } finally {
705
698
  // Broadcast state changes to other instances
706
699
  if (this._isPrimary) {
@@ -712,12 +705,12 @@ export class HankoAuth extends LitElement {
712
705
  private async checkOSMConnection() {
713
706
  // Skip OSM check if not required
714
707
  if (!this.osmRequired) {
715
- this.log("โญ๏ธ OSM not required, skipping connection check");
708
+ this.log("OSM not required, skipping connection check");
716
709
  return;
717
710
  }
718
711
 
719
712
  if (this.osmConnected) {
720
- this.log("โญ๏ธ Already connected to OSM, skipping check");
713
+ this.log("Already connected to OSM, skipping check");
721
714
  return;
722
715
  }
723
716
 
@@ -737,23 +730,23 @@ export class HankoAuth extends LitElement {
737
730
  const statusPath = `${basePath}${authPath}/status`;
738
731
  const statusUrl = `${statusPath}`; // Relative URL for proxy
739
732
 
740
- this.log("๐Ÿ” Checking OSM connection at:", statusUrl);
733
+ this.log("Checking OSM connection at:", statusUrl);
741
734
  this.log(" basePath:", basePath);
742
735
  this.log(" authPath:", authPath);
743
- this.log("๐Ÿช Current cookies:", document.cookie);
736
+ this.log("Current cookies:", document.cookie);
744
737
 
745
738
  const response = await fetch(statusUrl, {
746
739
  credentials: "include",
747
740
  redirect: "follow",
748
741
  });
749
742
 
750
- this.log("๐Ÿ“ก OSM status response:", response.status);
751
- this.log("๐Ÿ“ก Final URL after redirects:", response.url);
752
- this.log("๐Ÿ“ก Response headers:", [...response.headers.entries()]);
743
+ this.log("OSM status response:", response.status);
744
+ this.log("Final URL after redirects:", response.url);
745
+ this.log("Response headers:", [...response.headers.entries()]);
753
746
 
754
747
  if (response.ok) {
755
748
  const text = await response.text();
756
- this.log("๐Ÿ“ก OSM raw response:", text.substring(0, 200));
749
+ this.log("OSM raw response:", text.substring(0, 200));
757
750
 
758
751
  let data;
759
752
  try {
@@ -766,10 +759,10 @@ export class HankoAuth extends LitElement {
766
759
  throw new Error("Invalid JSON response from OSM status endpoint");
767
760
  }
768
761
 
769
- this.log("๐Ÿ“ก OSM status data:", data);
762
+ this.log("OSM status data:", data);
770
763
 
771
764
  if (data.connected) {
772
- this.log("โœ… OSM is connected:", data.osm_username);
765
+ this.log("OSM is connected:", data.osm_username);
773
766
  this.osmConnected = true;
774
767
  this.osmData = data;
775
768
 
@@ -786,7 +779,7 @@ export class HankoAuth extends LitElement {
786
779
  // The Login page's onboarding flow listens for 'osm-connected' event
787
780
  // and handles the redirect to the app's onboarding endpoint
788
781
  } else {
789
- this.log("โŒ OSM is NOT connected");
782
+ this.log("OSM is NOT connected");
790
783
  this.osmConnected = false;
791
784
  this.osmData = null;
792
785
  }
@@ -816,12 +809,12 @@ export class HankoAuth extends LitElement {
816
809
  const onboardingKey = getSessionOnboardingKey(window.location.hostname);
817
810
  const onboardingCompleted = sessionStorage.getItem(onboardingKey);
818
811
  if (onboardingCompleted) {
819
- this.log("โœ… Onboarding already completed this session, skipping check");
812
+ this.log("Onboarding already completed this session, skipping check");
820
813
  this.hasAppMapping = true;
821
814
  return true;
822
815
  }
823
816
 
824
- this.log("๐Ÿ” Checking app mapping at:", this.mappingCheckUrl);
817
+ this.log("Checking app mapping at:", this.mappingCheckUrl);
825
818
 
826
819
  try {
827
820
  const response = await fetch(this.mappingCheckUrl, {
@@ -830,12 +823,12 @@ export class HankoAuth extends LitElement {
830
823
 
831
824
  if (response.ok) {
832
825
  const data = await response.json();
833
- this.log("๐Ÿ“ก Mapping check response:", data);
826
+ this.log("Mapping check response:", data);
834
827
 
835
828
  if (data.needs_onboarding) {
836
829
  // User has Hanko session but no app mapping - redirect to onboarding
837
830
  // Don't set flag here - only set it when onboarding completes
838
- this.log("โš ๏ธ User needs onboarding, redirecting...");
831
+ this.log("User needs onboarding, redirecting...");
839
832
  const returnTo = encodeURIComponent(window.location.origin);
840
833
  const appParam = this.appId ? `onboarding=${this.appId}` : "";
841
834
  window.location.href = `${this.hankoUrl}/app?${appParam}&return_to=${returnTo}`;
@@ -845,11 +838,11 @@ export class HankoAuth extends LitElement {
845
838
  // User has mapping - mark onboarding as completed
846
839
  sessionStorage.setItem(onboardingKey, "true");
847
840
  this.hasAppMapping = true;
848
- this.log("โœ… User has app mapping, onboarding marked complete");
841
+ this.log("User has app mapping, onboarding marked complete");
849
842
  return true;
850
843
  } else if (response.status === 401 || response.status === 403) {
851
844
  // Needs onboarding
852
- this.log("โš ๏ธ 401/403 - User needs onboarding, redirecting...");
845
+ this.log("401/403 - User needs onboarding, redirecting...");
853
846
  const returnTo = encodeURIComponent(window.location.origin);
854
847
  const appParam = this.appId ? `onboarding=${this.appId}` : "";
855
848
  window.location.href = `${this.hankoUrl}/app?${appParam}&return_to=${returnTo}`;
@@ -857,10 +850,10 @@ export class HankoAuth extends LitElement {
857
850
  }
858
851
 
859
852
  // Other status codes - proceed without blocking
860
- this.log("โš ๏ธ Unexpected status from mapping check:", response.status);
853
+ this.log("Unexpected status from mapping check:", response.status);
861
854
  return true;
862
855
  } catch (error) {
863
- this.log("โš ๏ธ App mapping check failed:", error);
856
+ this.log("App mapping check failed:", error);
864
857
  // Don't block the user, just log the error
865
858
  return true;
866
859
  }
@@ -870,7 +863,7 @@ export class HankoAuth extends LitElement {
870
863
  private async fetchProfileDisplayName() {
871
864
  try {
872
865
  const profileUrl = `${this.hankoUrl}/api/profile/me`;
873
- this.log("๐Ÿ‘ค Fetching profile from:", profileUrl);
866
+ this.log("Fetching profile from:", profileUrl);
874
867
 
875
868
  const response = await fetch(profileUrl, {
876
869
  credentials: "include",
@@ -878,22 +871,29 @@ export class HankoAuth extends LitElement {
878
871
 
879
872
  if (response.ok) {
880
873
  const profile = await response.json();
881
- this.log("๐Ÿ‘ค Profile data:", profile);
874
+ this.log("Profile data:", profile);
882
875
 
883
876
  if (profile.first_name || profile.last_name) {
884
877
  this.profileDisplayName =
885
878
  `${profile.first_name || ""} ${profile.last_name || ""}`.trim();
886
- this.log("๐Ÿ‘ค Display name set to:", this.profileDisplayName);
879
+ this.log("Display name set to:", this.profileDisplayName);
880
+ }
881
+
882
+ // picture_url is always set by the backend (Gravatar fallback); osm_avatar_url as secondary
883
+ const picUrl = profile.picture_url || profile.osm_avatar_url;
884
+ if (picUrl) {
885
+ this.profilePictureUrl = picUrl;
886
+ this.log("Profile picture set to:", this.profilePictureUrl);
887
887
  }
888
888
 
889
889
  // Set language from user profile if available
890
890
  if (profile.language) {
891
891
  this.userProfileLanguage = profile.language;
892
- this.log("๐ŸŒ Language set from profile:", this.userProfileLanguage);
892
+ this.log("Language set from profile:", this.userProfileLanguage);
893
893
  }
894
894
  }
895
895
  } catch (error) {
896
- this.log("โš ๏ธ Could not fetch profile:", error);
896
+ this.log("Could not fetch profile:", error);
897
897
  }
898
898
  }
899
899
 
@@ -922,7 +922,7 @@ export class HankoAuth extends LitElement {
922
922
 
923
923
  // Skip if already attached to the same element
924
924
  if (hankoAuth && hankoAuth === this._currentHankoAuthElement) {
925
- this.log("โญ๏ธ Event listeners already attached to this element");
925
+ this.log("Event listeners already attached to this element");
926
926
  return;
927
927
  }
928
928
 
@@ -931,11 +931,11 @@ export class HankoAuth extends LitElement {
931
931
  this.log("๐ŸŽฏ Attaching event listeners to hanko-auth element");
932
932
 
933
933
  hankoAuth.addEventListener("onSessionCreated", (e: any) => {
934
- this.log(`๐ŸŽฏ Hanko event: onSessionCreated`, e.detail);
934
+ this.log(`Hanko event: onSessionCreated`, e.detail);
935
935
 
936
936
  const sessionId = e.detail?.claims?.session_id;
937
937
  if (sessionId && this._lastSessionId === sessionId) {
938
- this.log("โญ๏ธ Skipping duplicate session event");
938
+ this.log("Skipping duplicate session event");
939
939
  return;
940
940
  }
941
941
  this._lastSessionId = sessionId;
@@ -981,7 +981,7 @@ export class HankoAuth extends LitElement {
981
981
 
982
982
  if (meResponse.ok) {
983
983
  const userData = await meResponse.json();
984
- this.log("๐Ÿ‘ค User data retrieved from /me:", userData);
984
+ this.log("User data retrieved from /me:", userData);
985
985
 
986
986
  // Only use /me if it has email (login.hotosm.org has it, Hanko vanilla doesn't)
987
987
  if (userData.email) {
@@ -994,17 +994,15 @@ export class HankoAuth extends LitElement {
994
994
  };
995
995
  userInfoRetrieved = true;
996
996
  } else {
997
- this.log("โš ๏ธ /me has no email, will try SDK fallback");
997
+ this.log("/me has no email, will try SDK fallback");
998
998
  }
999
999
  } else {
1000
- this.log(
1001
- "โš ๏ธ /me endpoint returned non-OK status, will try SDK fallback",
1002
- );
1000
+ this.log("/me endpoint returned non-OK status, will try SDK fallback");
1003
1001
  }
1004
1002
  } catch (error) {
1005
1003
  // NetworkError or timeout on cross-origin fetch is common with mkcert certs
1006
1004
  this.log(
1007
- "โš ๏ธ /me endpoint fetch failed (timeout or cross-origin TLS issue):",
1005
+ "/me endpoint fetch failed (timeout or cross-origin TLS issue):",
1008
1006
  error,
1009
1007
  );
1010
1008
  }
@@ -1012,7 +1010,7 @@ export class HankoAuth extends LitElement {
1012
1010
  // Fallback to SDK method if /me didn't work
1013
1011
  if (!userInfoRetrieved) {
1014
1012
  try {
1015
- this.log("๐Ÿ”„ Trying SDK fallback for user info...");
1013
+ this.log("Trying SDK fallback for user info...");
1016
1014
  // Add timeout to SDK call in case it hangs
1017
1015
  const timeoutPromise = new Promise((_, reject) =>
1018
1016
  setTimeout(() => reject(new Error("SDK timeout")), 5000),
@@ -1028,9 +1026,9 @@ export class HankoAuth extends LitElement {
1028
1026
  emailVerified: user.email_verified || false,
1029
1027
  };
1030
1028
  userInfoRetrieved = true;
1031
- this.log("โœ… User info retrieved via SDK fallback");
1029
+ this.log("User info retrieved via SDK fallback");
1032
1030
  } catch (sdkError) {
1033
- this.log("โš ๏ธ SDK fallback failed, trying JWT claims:", sdkError);
1031
+ this.log("SDK fallback failed, trying JWT claims:", sdkError);
1034
1032
  // Last resort: extract user info from JWT claims in the event
1035
1033
  try {
1036
1034
  const claims = event.detail?.claims;
@@ -1042,7 +1040,7 @@ export class HankoAuth extends LitElement {
1042
1040
  emailVerified: claims.email_verified || false,
1043
1041
  };
1044
1042
  userInfoRetrieved = true;
1045
- this.log("โœ… User info extracted from JWT claims");
1043
+ this.log("User info extracted from JWT claims");
1046
1044
  } else {
1047
1045
  this.logError("No user claims available in event");
1048
1046
  this.user = null;
@@ -1059,7 +1057,7 @@ export class HankoAuth extends LitElement {
1059
1057
  }
1060
1058
  }
1061
1059
 
1062
- this.log("โœ… User state updated:", this.user);
1060
+ this.log("User state updated:", this.user);
1063
1061
 
1064
1062
  // Broadcast state changes to other instances
1065
1063
  if (this._isPrimary) {
@@ -1083,7 +1081,7 @@ export class HankoAuth extends LitElement {
1083
1081
 
1084
1082
  // Auto-connect to OSM if required and auto-connect is enabled
1085
1083
  if (this.osmRequired && this.autoConnect && !this.osmConnected) {
1086
- this.log("๐Ÿ”„ Auto-connecting to OSM...");
1084
+ this.log("Auto-connecting to OSM...");
1087
1085
  this.handleOSMConnect();
1088
1086
  return; // Exit early - redirect will happen after OSM OAuth callback
1089
1087
  }
@@ -1092,7 +1090,7 @@ export class HankoAuth extends LitElement {
1092
1090
  const canRedirect = !this.osmRequired || this.osmConnected;
1093
1091
 
1094
1092
  this.log(
1095
- "๐Ÿ”„ Checking redirect-after-login:",
1093
+ "Checking redirect-after-login:",
1096
1094
  this.redirectAfterLogin,
1097
1095
  "showProfile:",
1098
1096
  this.showProfile,
@@ -1109,13 +1107,13 @@ export class HankoAuth extends LitElement {
1109
1107
  );
1110
1108
 
1111
1109
  if (this.redirectAfterLogin) {
1112
- this.log("โœ… Redirecting to:", this.redirectAfterLogin);
1110
+ this.log("Redirecting to:", this.redirectAfterLogin);
1113
1111
  window.location.href = this.redirectAfterLogin;
1114
1112
  } else {
1115
- this.log("โŒ No redirect (redirectAfterLogin not set)");
1113
+ this.log("No redirect (redirectAfterLogin not set)");
1116
1114
  }
1117
1115
  } else {
1118
- this.log("โธ๏ธ Waiting for OSM connection before redirect");
1116
+ this.log("Waiting for OSM connection before redirect");
1119
1117
  }
1120
1118
  }
1121
1119
 
@@ -1128,7 +1126,7 @@ export class HankoAuth extends LitElement {
1128
1126
  const loginPath = `${basePath}${authPath}/login`;
1129
1127
  const fullUrl = `${loginPath}?scopes=${scopes}`;
1130
1128
 
1131
- this.log("๐Ÿ”— OSM Connect clicked!");
1129
+ this.log("OSM Connect clicked!");
1132
1130
  this.log(" basePath:", basePath);
1133
1131
  this.log(" authPath:", authPath);
1134
1132
  this.log(" Login path:", fullUrl);
@@ -1149,32 +1147,32 @@ export class HankoAuth extends LitElement {
1149
1147
  if (response.status === 0 || response.type === "opaqueredirect") {
1150
1148
  // This is a redirect response
1151
1149
  const redirectUrl = response.headers.get("Location") || response.url;
1152
- this.log(" โœ… Got redirect URL:", redirectUrl);
1150
+ this.log("Got redirect URL:", redirectUrl);
1153
1151
  window.location.href = redirectUrl;
1154
1152
  } else if (response.status >= 300 && response.status < 400) {
1155
1153
  const redirectUrl = response.headers.get("Location");
1156
- this.log(" โœ… Got redirect URL from header:", redirectUrl);
1154
+ this.log("Got redirect URL from header:", redirectUrl);
1157
1155
  if (redirectUrl) {
1158
1156
  window.location.href = redirectUrl;
1159
1157
  }
1160
1158
  } else {
1161
- this.logError(" โŒ Unexpected response:", response.status);
1159
+ this.logError("Unexpected response:", response.status);
1162
1160
  const text = await response.text();
1163
1161
  this.logError(" Response body:", text.substring(0, 200));
1164
1162
  }
1165
1163
  } catch (error) {
1166
- this.logError(" โŒ Failed to fetch redirect URL:", error);
1164
+ this.logError("Failed to fetch redirect URL:", error);
1167
1165
  }
1168
1166
  }
1169
1167
 
1170
1168
  private async handleLogout() {
1171
- this.log("๐Ÿšช Logout initiated");
1172
- this.log("๐Ÿ“Š Current state before logout:", {
1169
+ this.log("Logout initiated");
1170
+ this.log("Current state before logout:", {
1173
1171
  user: this.user,
1174
1172
  osmConnected: this.osmConnected,
1175
1173
  osmData: this.osmData,
1176
1174
  });
1177
- this.log("๐Ÿช Cookies before logout:", document.cookie);
1175
+ this.log("Cookies before logout:", document.cookie);
1178
1176
 
1179
1177
  try {
1180
1178
  const basePath = this.getBasePath();
@@ -1184,25 +1182,25 @@ export class HankoAuth extends LitElement {
1184
1182
  const disconnectUrl = disconnectPath.startsWith("http")
1185
1183
  ? disconnectPath
1186
1184
  : `${window.location.origin}${disconnectPath}`;
1187
- this.log("๐Ÿ”Œ Calling OSM disconnect:", disconnectUrl);
1185
+ this.log("Calling OSM disconnect:", disconnectUrl);
1188
1186
 
1189
1187
  const response = await fetch(disconnectUrl, {
1190
1188
  method: "POST",
1191
1189
  credentials: "include",
1192
1190
  });
1193
1191
 
1194
- this.log("๐Ÿ“ก Disconnect response status:", response.status);
1192
+ this.log("Disconnect response status:", response.status);
1195
1193
  const data = await response.json();
1196
- this.log("๐Ÿ“ก Disconnect response data:", data);
1197
- this.log("โœ… OSM disconnected");
1194
+ this.log("Disconnect response data:", data);
1195
+ this.log("OSM disconnected");
1198
1196
  } catch (error) {
1199
- this.logError("โŒ OSM disconnect failed:", error);
1197
+ this.logError("OSM disconnect failed:", error);
1200
1198
  }
1201
1199
 
1202
1200
  if (this._hanko) {
1203
1201
  try {
1204
1202
  await this._hanko.user.logout();
1205
- this.log("โœ… Hanko logout successful");
1203
+ this.log("Hanko logout successful");
1206
1204
  } catch (error) {
1207
1205
  this.logError("Hanko logout failed:", error);
1208
1206
  }
@@ -1211,19 +1209,17 @@ export class HankoAuth extends LitElement {
1211
1209
  // Use shared cleanup method
1212
1210
  this._clearAuthState();
1213
1211
 
1214
- this.log(
1215
- "โœ… Logout complete - component will re-render with updated state",
1216
- );
1212
+ this.log("Logout complete - component will re-render with updated state");
1217
1213
 
1218
1214
  // Redirect after logout if configured (but not if already there)
1219
1215
  if (this.redirectAfterLogout) {
1220
1216
  const currentUrl = window.location.href.replace(/\/$/, "");
1221
1217
  const targetUrl = this.redirectAfterLogout.replace(/\/$/, "");
1222
1218
  if (currentUrl !== targetUrl && !currentUrl.startsWith(targetUrl + "#")) {
1223
- this.log("๐Ÿ”„ Redirecting after logout to:", this.redirectAfterLogout);
1219
+ this.log("Redirecting after logout to:", this.redirectAfterLogout);
1224
1220
  window.location.href = this.redirectAfterLogout;
1225
1221
  } else {
1226
- this.log("โญ๏ธ Already on logout target, skipping redirect");
1222
+ this.log("Already on logout target, skipping redirect");
1227
1223
  }
1228
1224
  }
1229
1225
  // Otherwise let Lit's reactivity handle the re-render
@@ -1239,14 +1235,14 @@ export class HankoAuth extends LitElement {
1239
1235
  document.cookie = "hanko=; path=/; max-age=0";
1240
1236
  document.cookie = `osm_connection=; path=/; domain=${hostname}; max-age=0`;
1241
1237
  document.cookie = "osm_connection=; path=/; max-age=0";
1242
- this.log("๐Ÿช Cookies cleared");
1238
+ this.log("Cookies cleared");
1243
1239
 
1244
1240
  // Clear session verification and onboarding flags
1245
1241
  const verifyKey = getSessionVerifyKey(hostname);
1246
1242
  const onboardingKey = getSessionOnboardingKey(hostname);
1247
1243
  sessionStorage.removeItem(verifyKey);
1248
1244
  sessionStorage.removeItem(onboardingKey);
1249
- this.log("๐Ÿ”„ Session flags cleared");
1245
+ this.log("Session flags cleared");
1250
1246
 
1251
1247
  // Reset state
1252
1248
  this.user = null;
@@ -1254,6 +1250,7 @@ export class HankoAuth extends LitElement {
1254
1250
  this.osmData = null;
1255
1251
  this.hasAppMapping = false;
1256
1252
  this.userProfileLanguage = null; // Clear user's language preference
1253
+ this.profilePictureUrl = ""; // Clear profile picture
1257
1254
 
1258
1255
  // Broadcast state changes to other instances
1259
1256
  if (this._isPrimary) {
@@ -1270,8 +1267,8 @@ export class HankoAuth extends LitElement {
1270
1267
  }
1271
1268
 
1272
1269
  private async handleSessionExpired() {
1273
- this.log("๐Ÿ•’ Session expired event received");
1274
- this.log("๐Ÿ“Š Current state:", {
1270
+ this.log("Session expired event received");
1271
+ this.log("Current state:", {
1275
1272
  user: this.user,
1276
1273
  osmConnected: this.osmConnected,
1277
1274
  loading: this.loading,
@@ -1280,18 +1277,18 @@ export class HankoAuth extends LitElement {
1280
1277
  // If still loading, wait for session check to complete before acting
1281
1278
  // The SDK may fire this event for old/stale sessions during init
1282
1279
  if (this.loading) {
1283
- this.log("โณ Still loading, ignoring session expired event during init");
1280
+ this.log("Still loading, ignoring session expired event during init");
1284
1281
  return;
1285
1282
  }
1286
1283
 
1287
1284
  // If we have an active user, the session is still valid
1288
1285
  // The SDK may fire this event for old/stale sessions while a new session exists
1289
1286
  if (this.user) {
1290
- this.log("โœ… User is logged in, ignoring stale session expired event");
1287
+ this.log("User is logged in, ignoring stale session expired event");
1291
1288
  return;
1292
1289
  }
1293
1290
 
1294
- this.log("๐Ÿงน No active user - cleaning up state");
1291
+ this.log("No active user - cleaning up state");
1295
1292
 
1296
1293
  // Call OSM disconnect endpoint to clear httpOnly cookie
1297
1294
  try {
@@ -1302,25 +1299,25 @@ export class HankoAuth extends LitElement {
1302
1299
  const disconnectUrl = disconnectPath.startsWith("http")
1303
1300
  ? disconnectPath
1304
1301
  : `${window.location.origin}${disconnectPath}`;
1305
- this.log("๐Ÿ”Œ Calling OSM disconnect (session expired):", disconnectUrl);
1302
+ this.log("Calling OSM disconnect (session expired):", disconnectUrl);
1306
1303
 
1307
1304
  const response = await fetch(disconnectUrl, {
1308
1305
  method: "POST",
1309
1306
  credentials: "include",
1310
1307
  });
1311
1308
 
1312
- this.log("๐Ÿ“ก Disconnect response status:", response.status);
1309
+ this.log("Disconnect response status:", response.status);
1313
1310
  const data = await response.json();
1314
- this.log("๐Ÿ“ก Disconnect response data:", data);
1315
- this.log("โœ… OSM disconnected");
1311
+ this.log("Disconnect response data:", data);
1312
+ this.log("OSM disconnected");
1316
1313
  } catch (error) {
1317
- this.logError("โŒ OSM disconnect failed:", error);
1314
+ this.logError("OSM disconnect failed:", error);
1318
1315
  }
1319
1316
 
1320
1317
  // Use shared cleanup method
1321
1318
  this._clearAuthState();
1322
1319
 
1323
- this.log("โœ… Session cleanup complete");
1320
+ this.log("Session cleanup complete");
1324
1321
 
1325
1322
  // Redirect after session expired if configured (but not if already there)
1326
1323
  if (this.redirectAfterLogout) {
@@ -1328,19 +1325,19 @@ export class HankoAuth extends LitElement {
1328
1325
  const targetUrl = this.redirectAfterLogout.replace(/\/$/, "");
1329
1326
  if (currentUrl !== targetUrl && !currentUrl.startsWith(targetUrl + "#")) {
1330
1327
  this.log(
1331
- "๐Ÿ”„ Redirecting after session expired to:",
1328
+ "Redirecting after session expired to:",
1332
1329
  this.redirectAfterLogout,
1333
1330
  );
1334
1331
  window.location.href = this.redirectAfterLogout;
1335
1332
  } else {
1336
- this.log("โญ๏ธ Already on logout target, skipping redirect");
1333
+ this.log("Already on logout target, skipping redirect");
1337
1334
  }
1338
1335
  }
1339
1336
  // Otherwise component will re-render and show login button
1340
1337
  }
1341
1338
 
1342
1339
  private handleUserLoggedOut() {
1343
- this.log("๐Ÿšช User logged out in another window/tab");
1340
+ this.log("User logged out in another window/tab");
1344
1341
  // Same cleanup as session expired
1345
1342
  this.handleSessionExpired();
1346
1343
  }
@@ -1348,7 +1345,7 @@ export class HankoAuth extends LitElement {
1348
1345
  private handleDropdownSelect(event: Event) {
1349
1346
  const target = event.currentTarget as HTMLElement;
1350
1347
  const action = target.dataset.action;
1351
- this.log("๐ŸŽฏ Dropdown item selected:", action);
1348
+ this.log("Dropdown item selected:", action);
1352
1349
 
1353
1350
  if (action === "profile") {
1354
1351
  const baseUrl = this.hankoUrl;
@@ -1453,7 +1450,18 @@ export class HankoAuth extends LitElement {
1453
1450
  <div class="container">
1454
1451
  <div class="profile">
1455
1452
  <div class="profile-header">
1456
- <div class="profile-avatar">${initial}</div>
1453
+ <div class="profile-avatar">
1454
+ ${this.profilePictureUrl
1455
+ ? html`<img
1456
+ class="avatar-img"
1457
+ src="${this.profilePictureUrl}"
1458
+ alt="${initial}"
1459
+ @error=${(e: Event) => {
1460
+ (e.target as HTMLImageElement).style.display = "none";
1461
+ }}
1462
+ />`
1463
+ : initial}
1464
+ </div>
1457
1465
  <div class="profile-info">
1458
1466
  <div class="profile-email">
1459
1467
  ${this.user.email || this.user.id}
@@ -1538,7 +1546,18 @@ export class HankoAuth extends LitElement {
1538
1546
  class="bar-trigger"
1539
1547
  >
1540
1548
  <div class="bar-info">
1541
- <span class="header-avatar">${initial}</span>
1549
+ <span class="header-avatar">
1550
+ ${this.profilePictureUrl
1551
+ ? html`<img
1552
+ class="avatar-img"
1553
+ src="${this.profilePictureUrl}"
1554
+ alt="${initial}"
1555
+ @error=${(e: Event) => {
1556
+ (e.target as HTMLImageElement).style.display = "none";
1557
+ }}
1558
+ />`
1559
+ : initial}
1560
+ </span>
1542
1561
  <span class="bar-email"
1543
1562
  >${this.user.email || this.user.id}</span
1544
1563
  >
@@ -1563,7 +1582,18 @@ export class HankoAuth extends LitElement {
1563
1582
  aria-haspopup="true"
1564
1583
  class="dropdown-trigger"
1565
1584
  >
1566
- <span class="header-avatar">${initial}</span>
1585
+ <span class="header-avatar">
1586
+ ${this.profilePictureUrl
1587
+ ? html`<img
1588
+ class="avatar-img"
1589
+ src="${this.profilePictureUrl}"
1590
+ alt="${initial}"
1591
+ @error=${(e: Event) => {
1592
+ (e.target as HTMLImageElement).style.display = "none";
1593
+ }}
1594
+ />`
1595
+ : initial}
1596
+ </span>
1567
1597
 
1568
1598
  ${this.osmConnected
1569
1599
  ? html`