@hotosm/hanko-auth 0.4.7 โ†’ 0.4.9

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
 
@@ -85,6 +85,7 @@ interface UserState {
85
85
  email: string | null;
86
86
  username: string | null;
87
87
  emailVerified: boolean;
88
+ avatarUrl?: string;
88
89
  }
89
90
 
90
91
  interface OSMData {
@@ -112,26 +113,20 @@ export class HankoAuth extends LitElement {
112
113
  redirectAfterLogout = "";
113
114
  @property({ type: String, attribute: "display-name" })
114
115
  displayNameAttr = "";
115
- // URL to check if user has app mapping (for cross-app auth scenarios)
116
116
  @property({ type: String, attribute: "mapping-check-url" }) mappingCheckUrl =
117
117
  "";
118
- // App identifier for onboarding redirect
119
118
  @property({ type: String, attribute: "app-id" }) appId = "";
120
119
  // Custom login page URL (for standalone mode - overrides ${hankoUrl}/app)
121
120
  @property({ type: String, attribute: "login-url" }) loginUrl = "";
122
- // Language code (en, es, fr, pt, etc.)
123
121
  @property({ type: String, reflect: true }) lang = "en";
124
- // Button variant (filled, outline, plain)
125
122
  @property({ type: String, attribute: "button-variant" }) buttonVariant:
126
123
  | "filled"
127
124
  | "outline"
128
125
  | "plain" = "plain";
129
- // Button color (primary, neutral, danger)
130
126
  @property({ type: String, attribute: "button-color" }) buttonColor:
131
127
  | "primary"
132
128
  | "neutral"
133
129
  | "danger" = "primary";
134
- // Display mode: "default" (compact avatar button) or "bar" (full-width bar with avatar + email + chevron)
135
130
  @property({ type: String, reflect: true }) display: "default" | "bar" =
136
131
  "default";
137
132
 
@@ -144,6 +139,7 @@ export class HankoAuth extends LitElement {
144
139
  @state() private error: string | null = null;
145
140
  @state() private hankoReady = false; // Tracks when Hanko registration is complete
146
141
  @state() private profileDisplayName: string = "";
142
+ @state() private profilePictureUrl: string = "";
147
143
  @state() private hasAppMapping = false; // True if user has mapping in the app
148
144
  @state() private userProfileLanguage: string | null = null; // Language from user profile
149
145
  // dropdown
@@ -213,6 +209,21 @@ export class HankoAuth extends LitElement {
213
209
  private _hanko: any = null;
214
210
  private _isPrimary = false; // Is this the primary instance?
215
211
 
212
+ constructor() {
213
+ super();
214
+ try {
215
+ const cached = localStorage.getItem("hotosm-auth-user");
216
+ if (cached) {
217
+ const cachedUser: UserState = JSON.parse(cached);
218
+ this.user = cachedUser;
219
+ this.loading = false;
220
+ if (cachedUser.avatarUrl) {
221
+ this.profilePictureUrl = cachedUser.avatarUrl;
222
+ }
223
+ }
224
+ } catch {}
225
+ }
226
+
216
227
  // Get computed hankoUrl (priority: attribute > meta tag > window.HANKO_URL > origin)
217
228
  get hankoUrl(): string {
218
229
  if (this.hankoUrlAttr) {
@@ -268,12 +279,12 @@ export class HankoAuth extends LitElement {
268
279
 
269
280
  // If already initialized or being initialized by another instance, sync state and skip init
270
281
  if (sharedAuth.initialized || sharedAuth.primary) {
271
- this.log("๐Ÿ”„ Using shared state from primary instance");
282
+ this.log("Using shared state from primary instance");
272
283
  this._syncFromShared();
273
284
  this._isPrimary = false;
274
285
  } else {
275
286
  // This is the first/primary instance - claim it immediately to prevent race conditions
276
- this.log("๐Ÿ‘‘ This is the primary instance");
287
+ this.log("This is the primary instance");
277
288
  this._isPrimary = true;
278
289
  sharedAuth.primary = this;
279
290
  sharedAuth.initialized = true; // Mark as initialized immediately to prevent other instances from also initializing
@@ -298,7 +309,7 @@ export class HankoAuth extends LitElement {
298
309
  if (this._isPrimary && sharedAuth.instances.size > 0) {
299
310
  const newPrimary = sharedAuth.instances.values().next().value;
300
311
  if (newPrimary) {
301
- this.log("๐Ÿ‘‘ Promoting new primary instance");
312
+ this.log("Promoting new primary instance");
302
313
  newPrimary._isPrimary = true;
303
314
  sharedAuth.primary = newPrimary;
304
315
  }
@@ -321,6 +332,8 @@ export class HankoAuth extends LitElement {
321
332
  if (this._hanko !== sharedAuth.hanko) this._hanko = sharedAuth.hanko;
322
333
  if (this.profileDisplayName !== sharedAuth.profileDisplayName)
323
334
  this.profileDisplayName = sharedAuth.profileDisplayName;
335
+ if (this.profilePictureUrl !== sharedAuth.profilePictureUrl)
336
+ this.profilePictureUrl = sharedAuth.profilePictureUrl;
324
337
  if (this.hankoReady !== sharedAuth.hankoReady)
325
338
  this.hankoReady = sharedAuth.hankoReady;
326
339
  }
@@ -332,6 +345,7 @@ export class HankoAuth extends LitElement {
332
345
  sharedAuth.osmData = this.osmData;
333
346
  sharedAuth.loading = this.loading;
334
347
  sharedAuth.profileDisplayName = this.profileDisplayName;
348
+ sharedAuth.profilePictureUrl = this.profilePictureUrl;
335
349
  sharedAuth.hankoReady = this.hankoReady;
336
350
 
337
351
  // Sync to all other instances
@@ -349,7 +363,7 @@ export class HankoAuth extends LitElement {
349
363
  if (!document.hidden && !this.showProfile && !this.user) {
350
364
  // Page became visible, we're in header mode, and no user is logged in
351
365
  // Re-check session in case user logged in elsewhere
352
- this.log("๐Ÿ‘๏ธ Page visible, re-checking session...");
366
+ this.log("Page visible, re-checking session...");
353
367
  this.checkSession();
354
368
  }
355
369
  };
@@ -402,13 +416,8 @@ export class HankoAuth extends LitElement {
402
416
  }
403
417
  }
404
418
 
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
- */
419
+ /* Translations */
410
420
  private t(key: keyof typeof translations.en): string {
411
- // When user is logged in, use their profile language
412
421
  const effectiveLang =
413
422
  this.user && this.userProfileLanguage
414
423
  ? this.userProfileLanguage
@@ -547,12 +556,12 @@ export class HankoAuth extends LitElement {
547
556
  this.log("๐Ÿ” Checking for existing Hanko session...");
548
557
 
549
558
  if (!this._hanko) {
550
- this.log("โš ๏ธ Hanko instance not initialized yet");
559
+ this.log("Hanko instance not initialized yet");
551
560
  return;
552
561
  }
553
562
 
554
563
  try {
555
- this.log("๐Ÿ“ก Checking session validity via cookie...");
564
+ this.log("Checking session validity via cookie...");
556
565
 
557
566
  // First, try to validate the session cookie directly with Hanko
558
567
  // This works across subdomains because the cookie has domain: .hotosm.test
@@ -574,13 +583,13 @@ export class HankoAuth extends LitElement {
574
583
  // Check if session is actually valid (endpoint returns 200 with is_valid:false when no session)
575
584
  if (sessionData.is_valid === false) {
576
585
  this.log(
577
- "โ„น๏ธ Session validation returned is_valid:false - no valid session",
586
+ "Session validation returned is_valid:false - no valid session",
578
587
  );
579
588
  return;
580
589
  }
581
590
 
582
- this.log("โœ… Valid Hanko session found via cookie");
583
- this.log("๐Ÿ“‹ Session data:", sessionData);
591
+ this.log("Valid Hanko session found via cookie");
592
+ this.log("Session data:", sessionData);
584
593
 
585
594
  // Now get the full user data from the login backend /me endpoint
586
595
  // This endpoint validates the JWT and returns complete user info
@@ -596,7 +605,7 @@ export class HankoAuth extends LitElement {
596
605
  let needsSdkFallback = true;
597
606
  if (meResponse.ok) {
598
607
  const userData = await meResponse.json();
599
- this.log("๐Ÿ‘ค User data retrieved from /me:", userData);
608
+ this.log("User data retrieved from /me:", userData);
600
609
 
601
610
  // Only use /me if it has email (login.hotosm.org has it, Hanko vanilla doesn't)
602
611
  if (userData.email) {
@@ -609,12 +618,12 @@ export class HankoAuth extends LitElement {
609
618
  };
610
619
  needsSdkFallback = false;
611
620
  } else {
612
- this.log("โš ๏ธ /me has no email, will use SDK fallback");
621
+ this.log("/me has no email, will use SDK fallback");
613
622
  }
614
623
  }
615
624
 
616
625
  if (needsSdkFallback) {
617
- this.log("๐Ÿ”„ Using SDK to get user with email");
626
+ this.log("Using SDK to get user with email");
618
627
  // Fallback to SDK method which has email
619
628
  const user = await this._hanko.user.getCurrent();
620
629
  this.user = {
@@ -625,7 +634,7 @@ export class HankoAuth extends LitElement {
625
634
  };
626
635
  }
627
636
  } catch (userError) {
628
- this.log("โš ๏ธ Failed to get user data:", userError);
637
+ this.log("Failed to get user data:", userError);
629
638
  // Last resort: use session data if available
630
639
  if (sessionData.user_id) {
631
640
  this.user = {
@@ -650,7 +659,7 @@ export class HankoAuth extends LitElement {
650
659
  !alreadyVerified
651
660
  ) {
652
661
  this.log(
653
- "๐Ÿ”„ verify-session enabled, redirecting to callback for app verification...",
662
+ "verify-session enabled, redirecting to callback for app verification...",
654
663
  );
655
664
  sessionStorage.setItem(verifyKey, "true");
656
665
  window.location.href = this.redirectAfterLogin;
@@ -664,6 +673,10 @@ export class HankoAuth extends LitElement {
664
673
  // Redirect to onboarding in progress, don't proceed
665
674
  return;
666
675
  }
676
+ await this.fetchProfileDisplayName();
677
+ if (this.user && this.profilePictureUrl) {
678
+ this.user = { ...this.user, avatarUrl: this.profilePictureUrl };
679
+ }
667
680
 
668
681
  this.dispatchEvent(
669
682
  new CustomEvent("hanko-login", {
@@ -684,23 +697,21 @@ export class HankoAuth extends LitElement {
684
697
  if (this.osmRequired) {
685
698
  await this.checkOSMConnection();
686
699
  }
687
- // Fetch profile display name
688
- await this.fetchProfileDisplayName();
689
700
  if (this.osmRequired && this.autoConnect && !this.osmConnected) {
690
- this.log("๐Ÿ”„ Auto-connecting to OSM (from existing session)...");
701
+ this.log("Auto-connecting to OSM (from existing session)...");
691
702
  this.handleOSMConnect();
692
703
  }
693
704
  }
694
705
  } else {
695
- this.log("โ„น๏ธ No valid session cookie found - user needs to login");
706
+ this.log("No valid session cookie found - user needs to login");
696
707
  }
697
708
  } catch (validateError) {
698
- this.log("โš ๏ธ Session validation failed:", validateError);
699
- this.log("โ„น๏ธ No valid session - user needs to login");
709
+ this.log("Session validation failed:", validateError);
710
+ this.log("No valid session - user needs to login");
700
711
  }
701
712
  } catch (error) {
702
- this.log("โš ๏ธ Session check error:", error);
703
- this.log("โ„น๏ธ No existing session - user needs to login");
713
+ this.log("Session check error:", error);
714
+ this.log("No existing session - user needs to login");
704
715
  } finally {
705
716
  // Broadcast state changes to other instances
706
717
  if (this._isPrimary) {
@@ -712,12 +723,12 @@ export class HankoAuth extends LitElement {
712
723
  private async checkOSMConnection() {
713
724
  // Skip OSM check if not required
714
725
  if (!this.osmRequired) {
715
- this.log("โญ๏ธ OSM not required, skipping connection check");
726
+ this.log("OSM not required, skipping connection check");
716
727
  return;
717
728
  }
718
729
 
719
730
  if (this.osmConnected) {
720
- this.log("โญ๏ธ Already connected to OSM, skipping check");
731
+ this.log("Already connected to OSM, skipping check");
721
732
  return;
722
733
  }
723
734
 
@@ -737,23 +748,23 @@ export class HankoAuth extends LitElement {
737
748
  const statusPath = `${basePath}${authPath}/status`;
738
749
  const statusUrl = `${statusPath}`; // Relative URL for proxy
739
750
 
740
- this.log("๐Ÿ” Checking OSM connection at:", statusUrl);
751
+ this.log("Checking OSM connection at:", statusUrl);
741
752
  this.log(" basePath:", basePath);
742
753
  this.log(" authPath:", authPath);
743
- this.log("๐Ÿช Current cookies:", document.cookie);
754
+ this.log("Current cookies:", document.cookie);
744
755
 
745
756
  const response = await fetch(statusUrl, {
746
757
  credentials: "include",
747
758
  redirect: "follow",
748
759
  });
749
760
 
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()]);
761
+ this.log("OSM status response:", response.status);
762
+ this.log("Final URL after redirects:", response.url);
763
+ this.log("Response headers:", [...response.headers.entries()]);
753
764
 
754
765
  if (response.ok) {
755
766
  const text = await response.text();
756
- this.log("๐Ÿ“ก OSM raw response:", text.substring(0, 200));
767
+ this.log("OSM raw response:", text.substring(0, 200));
757
768
 
758
769
  let data;
759
770
  try {
@@ -766,10 +777,10 @@ export class HankoAuth extends LitElement {
766
777
  throw new Error("Invalid JSON response from OSM status endpoint");
767
778
  }
768
779
 
769
- this.log("๐Ÿ“ก OSM status data:", data);
780
+ this.log("OSM status data:", data);
770
781
 
771
782
  if (data.connected) {
772
- this.log("โœ… OSM is connected:", data.osm_username);
783
+ this.log("OSM is connected:", data.osm_username);
773
784
  this.osmConnected = true;
774
785
  this.osmData = data;
775
786
 
@@ -786,7 +797,7 @@ export class HankoAuth extends LitElement {
786
797
  // The Login page's onboarding flow listens for 'osm-connected' event
787
798
  // and handles the redirect to the app's onboarding endpoint
788
799
  } else {
789
- this.log("โŒ OSM is NOT connected");
800
+ this.log("OSM is NOT connected");
790
801
  this.osmConnected = false;
791
802
  this.osmData = null;
792
803
  }
@@ -816,12 +827,12 @@ export class HankoAuth extends LitElement {
816
827
  const onboardingKey = getSessionOnboardingKey(window.location.hostname);
817
828
  const onboardingCompleted = sessionStorage.getItem(onboardingKey);
818
829
  if (onboardingCompleted) {
819
- this.log("โœ… Onboarding already completed this session, skipping check");
830
+ this.log("Onboarding already completed this session, skipping check");
820
831
  this.hasAppMapping = true;
821
832
  return true;
822
833
  }
823
834
 
824
- this.log("๐Ÿ” Checking app mapping at:", this.mappingCheckUrl);
835
+ this.log("Checking app mapping at:", this.mappingCheckUrl);
825
836
 
826
837
  try {
827
838
  const response = await fetch(this.mappingCheckUrl, {
@@ -830,12 +841,12 @@ export class HankoAuth extends LitElement {
830
841
 
831
842
  if (response.ok) {
832
843
  const data = await response.json();
833
- this.log("๐Ÿ“ก Mapping check response:", data);
844
+ this.log("Mapping check response:", data);
834
845
 
835
846
  if (data.needs_onboarding) {
836
847
  // User has Hanko session but no app mapping - redirect to onboarding
837
848
  // Don't set flag here - only set it when onboarding completes
838
- this.log("โš ๏ธ User needs onboarding, redirecting...");
849
+ this.log("User needs onboarding, redirecting...");
839
850
  const returnTo = encodeURIComponent(window.location.origin);
840
851
  const appParam = this.appId ? `onboarding=${this.appId}` : "";
841
852
  window.location.href = `${this.hankoUrl}/app?${appParam}&return_to=${returnTo}`;
@@ -845,11 +856,11 @@ export class HankoAuth extends LitElement {
845
856
  // User has mapping - mark onboarding as completed
846
857
  sessionStorage.setItem(onboardingKey, "true");
847
858
  this.hasAppMapping = true;
848
- this.log("โœ… User has app mapping, onboarding marked complete");
859
+ this.log("User has app mapping, onboarding marked complete");
849
860
  return true;
850
861
  } else if (response.status === 401 || response.status === 403) {
851
862
  // Needs onboarding
852
- this.log("โš ๏ธ 401/403 - User needs onboarding, redirecting...");
863
+ this.log("401/403 - User needs onboarding, redirecting...");
853
864
  const returnTo = encodeURIComponent(window.location.origin);
854
865
  const appParam = this.appId ? `onboarding=${this.appId}` : "";
855
866
  window.location.href = `${this.hankoUrl}/app?${appParam}&return_to=${returnTo}`;
@@ -857,10 +868,10 @@ export class HankoAuth extends LitElement {
857
868
  }
858
869
 
859
870
  // Other status codes - proceed without blocking
860
- this.log("โš ๏ธ Unexpected status from mapping check:", response.status);
871
+ this.log("Unexpected status from mapping check:", response.status);
861
872
  return true;
862
873
  } catch (error) {
863
- this.log("โš ๏ธ App mapping check failed:", error);
874
+ this.log("App mapping check failed:", error);
864
875
  // Don't block the user, just log the error
865
876
  return true;
866
877
  }
@@ -870,7 +881,7 @@ export class HankoAuth extends LitElement {
870
881
  private async fetchProfileDisplayName() {
871
882
  try {
872
883
  const profileUrl = `${this.hankoUrl}/api/profile/me`;
873
- this.log("๐Ÿ‘ค Fetching profile from:", profileUrl);
884
+ this.log("Fetching profile from:", profileUrl);
874
885
 
875
886
  const response = await fetch(profileUrl, {
876
887
  credentials: "include",
@@ -878,22 +889,29 @@ export class HankoAuth extends LitElement {
878
889
 
879
890
  if (response.ok) {
880
891
  const profile = await response.json();
881
- this.log("๐Ÿ‘ค Profile data:", profile);
892
+ this.log("Profile data:", profile);
882
893
 
883
894
  if (profile.first_name || profile.last_name) {
884
895
  this.profileDisplayName =
885
896
  `${profile.first_name || ""} ${profile.last_name || ""}`.trim();
886
- this.log("๐Ÿ‘ค Display name set to:", this.profileDisplayName);
897
+ this.log("Display name set to:", this.profileDisplayName);
898
+ }
899
+
900
+ // picture_url is always set by the backend (Gravatar fallback); osm_avatar_url as secondary
901
+ const picUrl = profile.picture_url || profile.osm_avatar_url;
902
+ if (picUrl) {
903
+ this.profilePictureUrl = picUrl;
904
+ this.log("Profile picture set to:", this.profilePictureUrl);
887
905
  }
888
906
 
889
907
  // Set language from user profile if available
890
908
  if (profile.language) {
891
909
  this.userProfileLanguage = profile.language;
892
- this.log("๐ŸŒ Language set from profile:", this.userProfileLanguage);
910
+ this.log("Language set from profile:", this.userProfileLanguage);
893
911
  }
894
912
  }
895
913
  } catch (error) {
896
- this.log("โš ๏ธ Could not fetch profile:", error);
914
+ this.log("Could not fetch profile:", error);
897
915
  }
898
916
  }
899
917
 
@@ -922,7 +940,7 @@ export class HankoAuth extends LitElement {
922
940
 
923
941
  // Skip if already attached to the same element
924
942
  if (hankoAuth && hankoAuth === this._currentHankoAuthElement) {
925
- this.log("โญ๏ธ Event listeners already attached to this element");
943
+ this.log("Event listeners already attached to this element");
926
944
  return;
927
945
  }
928
946
 
@@ -931,11 +949,11 @@ export class HankoAuth extends LitElement {
931
949
  this.log("๐ŸŽฏ Attaching event listeners to hanko-auth element");
932
950
 
933
951
  hankoAuth.addEventListener("onSessionCreated", (e: any) => {
934
- this.log(`๐ŸŽฏ Hanko event: onSessionCreated`, e.detail);
952
+ this.log(`Hanko event: onSessionCreated`, e.detail);
935
953
 
936
954
  const sessionId = e.detail?.claims?.session_id;
937
955
  if (sessionId && this._lastSessionId === sessionId) {
938
- this.log("โญ๏ธ Skipping duplicate session event");
956
+ this.log("Skipping duplicate session event");
939
957
  return;
940
958
  }
941
959
  this._lastSessionId = sessionId;
@@ -981,7 +999,7 @@ export class HankoAuth extends LitElement {
981
999
 
982
1000
  if (meResponse.ok) {
983
1001
  const userData = await meResponse.json();
984
- this.log("๐Ÿ‘ค User data retrieved from /me:", userData);
1002
+ this.log("User data retrieved from /me:", userData);
985
1003
 
986
1004
  // Only use /me if it has email (login.hotosm.org has it, Hanko vanilla doesn't)
987
1005
  if (userData.email) {
@@ -994,17 +1012,15 @@ export class HankoAuth extends LitElement {
994
1012
  };
995
1013
  userInfoRetrieved = true;
996
1014
  } else {
997
- this.log("โš ๏ธ /me has no email, will try SDK fallback");
1015
+ this.log("/me has no email, will try SDK fallback");
998
1016
  }
999
1017
  } else {
1000
- this.log(
1001
- "โš ๏ธ /me endpoint returned non-OK status, will try SDK fallback",
1002
- );
1018
+ this.log("/me endpoint returned non-OK status, will try SDK fallback");
1003
1019
  }
1004
1020
  } catch (error) {
1005
1021
  // NetworkError or timeout on cross-origin fetch is common with mkcert certs
1006
1022
  this.log(
1007
- "โš ๏ธ /me endpoint fetch failed (timeout or cross-origin TLS issue):",
1023
+ "/me endpoint fetch failed (timeout or cross-origin TLS issue):",
1008
1024
  error,
1009
1025
  );
1010
1026
  }
@@ -1012,7 +1028,7 @@ export class HankoAuth extends LitElement {
1012
1028
  // Fallback to SDK method if /me didn't work
1013
1029
  if (!userInfoRetrieved) {
1014
1030
  try {
1015
- this.log("๐Ÿ”„ Trying SDK fallback for user info...");
1031
+ this.log("Trying SDK fallback for user info...");
1016
1032
  // Add timeout to SDK call in case it hangs
1017
1033
  const timeoutPromise = new Promise((_, reject) =>
1018
1034
  setTimeout(() => reject(new Error("SDK timeout")), 5000),
@@ -1028,9 +1044,9 @@ export class HankoAuth extends LitElement {
1028
1044
  emailVerified: user.email_verified || false,
1029
1045
  };
1030
1046
  userInfoRetrieved = true;
1031
- this.log("โœ… User info retrieved via SDK fallback");
1047
+ this.log("User info retrieved via SDK fallback");
1032
1048
  } catch (sdkError) {
1033
- this.log("โš ๏ธ SDK fallback failed, trying JWT claims:", sdkError);
1049
+ this.log("SDK fallback failed, trying JWT claims:", sdkError);
1034
1050
  // Last resort: extract user info from JWT claims in the event
1035
1051
  try {
1036
1052
  const claims = event.detail?.claims;
@@ -1042,7 +1058,7 @@ export class HankoAuth extends LitElement {
1042
1058
  emailVerified: claims.email_verified || false,
1043
1059
  };
1044
1060
  userInfoRetrieved = true;
1045
- this.log("โœ… User info extracted from JWT claims");
1061
+ this.log("User info extracted from JWT claims");
1046
1062
  } else {
1047
1063
  this.logError("No user claims available in event");
1048
1064
  this.user = null;
@@ -1059,7 +1075,11 @@ export class HankoAuth extends LitElement {
1059
1075
  }
1060
1076
  }
1061
1077
 
1062
- this.log("โœ… User state updated:", this.user);
1078
+ this.log("User state updated:", this.user);
1079
+ await this.fetchProfileDisplayName();
1080
+ if (this.user && this.profilePictureUrl) {
1081
+ this.user = { ...this.user, avatarUrl: this.profilePictureUrl };
1082
+ }
1063
1083
 
1064
1084
  // Broadcast state changes to other instances
1065
1085
  if (this._isPrimary) {
@@ -1078,12 +1098,10 @@ export class HankoAuth extends LitElement {
1078
1098
  if (this.osmRequired) {
1079
1099
  await this.checkOSMConnection();
1080
1100
  }
1081
- // Fetch profile display name (only works with login.hotosm.org backend)
1082
- await this.fetchProfileDisplayName();
1083
1101
 
1084
1102
  // Auto-connect to OSM if required and auto-connect is enabled
1085
1103
  if (this.osmRequired && this.autoConnect && !this.osmConnected) {
1086
- this.log("๐Ÿ”„ Auto-connecting to OSM...");
1104
+ this.log("Auto-connecting to OSM...");
1087
1105
  this.handleOSMConnect();
1088
1106
  return; // Exit early - redirect will happen after OSM OAuth callback
1089
1107
  }
@@ -1092,7 +1110,7 @@ export class HankoAuth extends LitElement {
1092
1110
  const canRedirect = !this.osmRequired || this.osmConnected;
1093
1111
 
1094
1112
  this.log(
1095
- "๐Ÿ”„ Checking redirect-after-login:",
1113
+ "Checking redirect-after-login:",
1096
1114
  this.redirectAfterLogin,
1097
1115
  "showProfile:",
1098
1116
  this.showProfile,
@@ -1109,13 +1127,13 @@ export class HankoAuth extends LitElement {
1109
1127
  );
1110
1128
 
1111
1129
  if (this.redirectAfterLogin) {
1112
- this.log("โœ… Redirecting to:", this.redirectAfterLogin);
1130
+ this.log("Redirecting to:", this.redirectAfterLogin);
1113
1131
  window.location.href = this.redirectAfterLogin;
1114
1132
  } else {
1115
- this.log("โŒ No redirect (redirectAfterLogin not set)");
1133
+ this.log("No redirect (redirectAfterLogin not set)");
1116
1134
  }
1117
1135
  } else {
1118
- this.log("โธ๏ธ Waiting for OSM connection before redirect");
1136
+ this.log("Waiting for OSM connection before redirect");
1119
1137
  }
1120
1138
  }
1121
1139
 
@@ -1128,7 +1146,7 @@ export class HankoAuth extends LitElement {
1128
1146
  const loginPath = `${basePath}${authPath}/login`;
1129
1147
  const fullUrl = `${loginPath}?scopes=${scopes}`;
1130
1148
 
1131
- this.log("๐Ÿ”— OSM Connect clicked!");
1149
+ this.log("OSM Connect clicked!");
1132
1150
  this.log(" basePath:", basePath);
1133
1151
  this.log(" authPath:", authPath);
1134
1152
  this.log(" Login path:", fullUrl);
@@ -1149,32 +1167,32 @@ export class HankoAuth extends LitElement {
1149
1167
  if (response.status === 0 || response.type === "opaqueredirect") {
1150
1168
  // This is a redirect response
1151
1169
  const redirectUrl = response.headers.get("Location") || response.url;
1152
- this.log(" โœ… Got redirect URL:", redirectUrl);
1170
+ this.log("Got redirect URL:", redirectUrl);
1153
1171
  window.location.href = redirectUrl;
1154
1172
  } else if (response.status >= 300 && response.status < 400) {
1155
1173
  const redirectUrl = response.headers.get("Location");
1156
- this.log(" โœ… Got redirect URL from header:", redirectUrl);
1174
+ this.log("Got redirect URL from header:", redirectUrl);
1157
1175
  if (redirectUrl) {
1158
1176
  window.location.href = redirectUrl;
1159
1177
  }
1160
1178
  } else {
1161
- this.logError(" โŒ Unexpected response:", response.status);
1179
+ this.logError("Unexpected response:", response.status);
1162
1180
  const text = await response.text();
1163
1181
  this.logError(" Response body:", text.substring(0, 200));
1164
1182
  }
1165
1183
  } catch (error) {
1166
- this.logError(" โŒ Failed to fetch redirect URL:", error);
1184
+ this.logError("Failed to fetch redirect URL:", error);
1167
1185
  }
1168
1186
  }
1169
1187
 
1170
1188
  private async handleLogout() {
1171
- this.log("๐Ÿšช Logout initiated");
1172
- this.log("๐Ÿ“Š Current state before logout:", {
1189
+ this.log("Logout initiated");
1190
+ this.log("Current state before logout:", {
1173
1191
  user: this.user,
1174
1192
  osmConnected: this.osmConnected,
1175
1193
  osmData: this.osmData,
1176
1194
  });
1177
- this.log("๐Ÿช Cookies before logout:", document.cookie);
1195
+ this.log("Cookies before logout:", document.cookie);
1178
1196
 
1179
1197
  try {
1180
1198
  const basePath = this.getBasePath();
@@ -1184,25 +1202,25 @@ export class HankoAuth extends LitElement {
1184
1202
  const disconnectUrl = disconnectPath.startsWith("http")
1185
1203
  ? disconnectPath
1186
1204
  : `${window.location.origin}${disconnectPath}`;
1187
- this.log("๐Ÿ”Œ Calling OSM disconnect:", disconnectUrl);
1205
+ this.log("Calling OSM disconnect:", disconnectUrl);
1188
1206
 
1189
1207
  const response = await fetch(disconnectUrl, {
1190
1208
  method: "POST",
1191
1209
  credentials: "include",
1192
1210
  });
1193
1211
 
1194
- this.log("๐Ÿ“ก Disconnect response status:", response.status);
1212
+ this.log("Disconnect response status:", response.status);
1195
1213
  const data = await response.json();
1196
- this.log("๐Ÿ“ก Disconnect response data:", data);
1197
- this.log("โœ… OSM disconnected");
1214
+ this.log("Disconnect response data:", data);
1215
+ this.log("OSM disconnected");
1198
1216
  } catch (error) {
1199
- this.logError("โŒ OSM disconnect failed:", error);
1217
+ this.logError("OSM disconnect failed:", error);
1200
1218
  }
1201
1219
 
1202
1220
  if (this._hanko) {
1203
1221
  try {
1204
1222
  await this._hanko.user.logout();
1205
- this.log("โœ… Hanko logout successful");
1223
+ this.log("Hanko logout successful");
1206
1224
  } catch (error) {
1207
1225
  this.logError("Hanko logout failed:", error);
1208
1226
  }
@@ -1211,19 +1229,17 @@ export class HankoAuth extends LitElement {
1211
1229
  // Use shared cleanup method
1212
1230
  this._clearAuthState();
1213
1231
 
1214
- this.log(
1215
- "โœ… Logout complete - component will re-render with updated state",
1216
- );
1232
+ this.log("Logout complete - component will re-render with updated state");
1217
1233
 
1218
1234
  // Redirect after logout if configured (but not if already there)
1219
1235
  if (this.redirectAfterLogout) {
1220
1236
  const currentUrl = window.location.href.replace(/\/$/, "");
1221
1237
  const targetUrl = this.redirectAfterLogout.replace(/\/$/, "");
1222
1238
  if (currentUrl !== targetUrl && !currentUrl.startsWith(targetUrl + "#")) {
1223
- this.log("๐Ÿ”„ Redirecting after logout to:", this.redirectAfterLogout);
1239
+ this.log("Redirecting after logout to:", this.redirectAfterLogout);
1224
1240
  window.location.href = this.redirectAfterLogout;
1225
1241
  } else {
1226
- this.log("โญ๏ธ Already on logout target, skipping redirect");
1242
+ this.log("Already on logout target, skipping redirect");
1227
1243
  }
1228
1244
  }
1229
1245
  // Otherwise let Lit's reactivity handle the re-render
@@ -1239,14 +1255,14 @@ export class HankoAuth extends LitElement {
1239
1255
  document.cookie = "hanko=; path=/; max-age=0";
1240
1256
  document.cookie = `osm_connection=; path=/; domain=${hostname}; max-age=0`;
1241
1257
  document.cookie = "osm_connection=; path=/; max-age=0";
1242
- this.log("๐Ÿช Cookies cleared");
1258
+ this.log("Cookies cleared");
1243
1259
 
1244
1260
  // Clear session verification and onboarding flags
1245
1261
  const verifyKey = getSessionVerifyKey(hostname);
1246
1262
  const onboardingKey = getSessionOnboardingKey(hostname);
1247
1263
  sessionStorage.removeItem(verifyKey);
1248
1264
  sessionStorage.removeItem(onboardingKey);
1249
- this.log("๐Ÿ”„ Session flags cleared");
1265
+ this.log("Session flags cleared");
1250
1266
 
1251
1267
  // Reset state
1252
1268
  this.user = null;
@@ -1254,6 +1270,7 @@ export class HankoAuth extends LitElement {
1254
1270
  this.osmData = null;
1255
1271
  this.hasAppMapping = false;
1256
1272
  this.userProfileLanguage = null; // Clear user's language preference
1273
+ this.profilePictureUrl = ""; // Clear profile picture
1257
1274
 
1258
1275
  // Broadcast state changes to other instances
1259
1276
  if (this._isPrimary) {
@@ -1270,8 +1287,8 @@ export class HankoAuth extends LitElement {
1270
1287
  }
1271
1288
 
1272
1289
  private async handleSessionExpired() {
1273
- this.log("๐Ÿ•’ Session expired event received");
1274
- this.log("๐Ÿ“Š Current state:", {
1290
+ this.log("Session expired event received");
1291
+ this.log("Current state:", {
1275
1292
  user: this.user,
1276
1293
  osmConnected: this.osmConnected,
1277
1294
  loading: this.loading,
@@ -1280,18 +1297,18 @@ export class HankoAuth extends LitElement {
1280
1297
  // If still loading, wait for session check to complete before acting
1281
1298
  // The SDK may fire this event for old/stale sessions during init
1282
1299
  if (this.loading) {
1283
- this.log("โณ Still loading, ignoring session expired event during init");
1300
+ this.log("Still loading, ignoring session expired event during init");
1284
1301
  return;
1285
1302
  }
1286
1303
 
1287
1304
  // If we have an active user, the session is still valid
1288
1305
  // The SDK may fire this event for old/stale sessions while a new session exists
1289
1306
  if (this.user) {
1290
- this.log("โœ… User is logged in, ignoring stale session expired event");
1307
+ this.log("User is logged in, ignoring stale session expired event");
1291
1308
  return;
1292
1309
  }
1293
1310
 
1294
- this.log("๐Ÿงน No active user - cleaning up state");
1311
+ this.log("No active user - cleaning up state");
1295
1312
 
1296
1313
  // Call OSM disconnect endpoint to clear httpOnly cookie
1297
1314
  try {
@@ -1302,25 +1319,25 @@ export class HankoAuth extends LitElement {
1302
1319
  const disconnectUrl = disconnectPath.startsWith("http")
1303
1320
  ? disconnectPath
1304
1321
  : `${window.location.origin}${disconnectPath}`;
1305
- this.log("๐Ÿ”Œ Calling OSM disconnect (session expired):", disconnectUrl);
1322
+ this.log("Calling OSM disconnect (session expired):", disconnectUrl);
1306
1323
 
1307
1324
  const response = await fetch(disconnectUrl, {
1308
1325
  method: "POST",
1309
1326
  credentials: "include",
1310
1327
  });
1311
1328
 
1312
- this.log("๐Ÿ“ก Disconnect response status:", response.status);
1329
+ this.log("Disconnect response status:", response.status);
1313
1330
  const data = await response.json();
1314
- this.log("๐Ÿ“ก Disconnect response data:", data);
1315
- this.log("โœ… OSM disconnected");
1331
+ this.log("Disconnect response data:", data);
1332
+ this.log("OSM disconnected");
1316
1333
  } catch (error) {
1317
- this.logError("โŒ OSM disconnect failed:", error);
1334
+ this.logError("OSM disconnect failed:", error);
1318
1335
  }
1319
1336
 
1320
1337
  // Use shared cleanup method
1321
1338
  this._clearAuthState();
1322
1339
 
1323
- this.log("โœ… Session cleanup complete");
1340
+ this.log("Session cleanup complete");
1324
1341
 
1325
1342
  // Redirect after session expired if configured (but not if already there)
1326
1343
  if (this.redirectAfterLogout) {
@@ -1328,19 +1345,19 @@ export class HankoAuth extends LitElement {
1328
1345
  const targetUrl = this.redirectAfterLogout.replace(/\/$/, "");
1329
1346
  if (currentUrl !== targetUrl && !currentUrl.startsWith(targetUrl + "#")) {
1330
1347
  this.log(
1331
- "๐Ÿ”„ Redirecting after session expired to:",
1348
+ "Redirecting after session expired to:",
1332
1349
  this.redirectAfterLogout,
1333
1350
  );
1334
1351
  window.location.href = this.redirectAfterLogout;
1335
1352
  } else {
1336
- this.log("โญ๏ธ Already on logout target, skipping redirect");
1353
+ this.log("Already on logout target, skipping redirect");
1337
1354
  }
1338
1355
  }
1339
1356
  // Otherwise component will re-render and show login button
1340
1357
  }
1341
1358
 
1342
1359
  private handleUserLoggedOut() {
1343
- this.log("๐Ÿšช User logged out in another window/tab");
1360
+ this.log("User logged out in another window/tab");
1344
1361
  // Same cleanup as session expired
1345
1362
  this.handleSessionExpired();
1346
1363
  }
@@ -1348,7 +1365,7 @@ export class HankoAuth extends LitElement {
1348
1365
  private handleDropdownSelect(event: Event) {
1349
1366
  const target = event.currentTarget as HTMLElement;
1350
1367
  const action = target.dataset.action;
1351
- this.log("๐ŸŽฏ Dropdown item selected:", action);
1368
+ this.log("Dropdown item selected:", action);
1352
1369
 
1353
1370
  if (action === "profile") {
1354
1371
  const baseUrl = this.hankoUrl;
@@ -1453,7 +1470,18 @@ export class HankoAuth extends LitElement {
1453
1470
  <div class="container">
1454
1471
  <div class="profile">
1455
1472
  <div class="profile-header">
1456
- <div class="profile-avatar">${initial}</div>
1473
+ <div class="profile-avatar">
1474
+ ${this.profilePictureUrl
1475
+ ? html`<img
1476
+ class="avatar-img"
1477
+ src="${this.profilePictureUrl}"
1478
+ alt="${initial}"
1479
+ @error=${(e: Event) => {
1480
+ (e.target as HTMLImageElement).style.display = "none";
1481
+ }}
1482
+ />`
1483
+ : initial}
1484
+ </div>
1457
1485
  <div class="profile-info">
1458
1486
  <div class="profile-email">
1459
1487
  ${this.user.email || this.user.id}
@@ -1538,7 +1566,18 @@ export class HankoAuth extends LitElement {
1538
1566
  class="bar-trigger"
1539
1567
  >
1540
1568
  <div class="bar-info">
1541
- <span class="header-avatar">${initial}</span>
1569
+ <span class="header-avatar">
1570
+ ${this.profilePictureUrl
1571
+ ? html`<img
1572
+ class="avatar-img"
1573
+ src="${this.profilePictureUrl}"
1574
+ alt="${initial}"
1575
+ @error=${(e: Event) => {
1576
+ (e.target as HTMLImageElement).style.display = "none";
1577
+ }}
1578
+ />`
1579
+ : initial}
1580
+ </span>
1542
1581
  <span class="bar-email"
1543
1582
  >${this.user.email || this.user.id}</span
1544
1583
  >
@@ -1563,7 +1602,18 @@ export class HankoAuth extends LitElement {
1563
1602
  aria-haspopup="true"
1564
1603
  class="dropdown-trigger"
1565
1604
  >
1566
- <span class="header-avatar">${initial}</span>
1605
+ <span class="header-avatar">
1606
+ ${this.profilePictureUrl
1607
+ ? html`<img
1608
+ class="avatar-img"
1609
+ src="${this.profilePictureUrl}"
1610
+ alt="${initial}"
1611
+ @error=${(e: Event) => {
1612
+ (e.target as HTMLImageElement).style.display = "none";
1613
+ }}
1614
+ />`
1615
+ : initial}
1616
+ </span>
1567
1617
 
1568
1618
  ${this.osmConnected
1569
1619
  ? html`