@hotosm/hanko-auth 0.5.1 → 0.5.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotosm/hanko-auth",
3
- "version": "0.5.1",
3
+ "version": "0.5.4",
4
4
  "description": "Web component for HOTOSM SSO authentication with Hanko and OSM integration",
5
5
  "type": "module",
6
6
  "main": "dist/hanko-auth.umd.js",
@@ -17,13 +17,6 @@
17
17
  "dist",
18
18
  "src"
19
19
  ],
20
- "scripts": {
21
- "dev": "vite",
22
- "build": "vite build",
23
- "watch": "vite build --watch",
24
- "preview": "vite preview",
25
- "prepublishOnly": "pnpm build"
26
- },
27
20
  "keywords": [
28
21
  "hotosm",
29
22
  "hanko",
@@ -51,5 +44,11 @@
51
44
  },
52
45
  "devDependencies": {
53
46
  "vite": "^5.0.0"
47
+ },
48
+ "scripts": {
49
+ "dev": "vite",
50
+ "build": "vite build",
51
+ "watch": "vite build --watch",
52
+ "preview": "vite preview"
54
53
  }
55
- }
54
+ }
@@ -274,7 +274,7 @@ export const styles = css`
274
274
 
275
275
  .login-link {
276
276
  color: var(--login-btn-text-color, white);
277
- font-size: var(--login-btn-text-size, var(--hot-font-size-medium));
277
+ font-size: var(--hot-font-size-small);
278
278
  border-radius: var(
279
279
  --login-btn-border-radius,
280
280
  var(--hot-border-radius-medium)
@@ -303,11 +303,11 @@ export const styles = css`
303
303
  border: none;
304
304
  }
305
305
  .login-link.filled.primary {
306
- background: var(--login-btn-bg-color, var(--hot-color-primary-1000));
306
+ background: var(--hot-color-gray-950);
307
307
  color: var(--login-btn-text-color, white);
308
308
  }
309
309
  .login-link.filled.primary:hover {
310
- background: var(--login-btn-hover-bg-color, var(--hot-color-primary-900));
310
+ background: var(--hot-color-primary-800);
311
311
  }
312
312
  .login-link.filled.neutral {
313
313
  background: var(--login-btn-bg-color, var(--hot-color-neutral-600));
@@ -330,8 +330,8 @@ export const styles = css`
330
330
  border: 1px solid;
331
331
  }
332
332
  .login-link.outline.primary {
333
- border-color: var(--login-btn-bg-color, var(--hot-color-primary-1000));
334
- color: var(--login-btn-text-color, var(--hot-color-primary-1000));
333
+ border-color: var(--login-btn-bg-color, var(--hot-color-primary-950));
334
+ color: var(--login-btn-text-color, var(--hot-color-primary-950));
335
335
  }
336
336
  .login-link.outline.primary:hover {
337
337
  background: var(--login-btn-hover-bg-color, var(--hot-color-primary-50));
@@ -357,7 +357,7 @@ export const styles = css`
357
357
  border: none;
358
358
  }
359
359
  .login-link.plain.primary {
360
- color: var(--login-btn-text-color, var(--hot-color-primary-1000));
360
+ color: var(--login-btn-text-color, var(--hot-color-primary-950));
361
361
  }
362
362
  .login-link.plain.primary:hover {
363
363
  background: var(--login-btn-hover-bg-color, var(--hot-color-primary-50));
package/src/hanko-auth.ts CHANGED
@@ -195,6 +195,8 @@ export class HankoAuth extends LitElement {
195
195
  private _lastSessionId: string | null = null;
196
196
  private _hanko: any = null;
197
197
  private _isPrimary = false; // Is this the primary instance?
198
+ private _sessionCheckFailures = 0;
199
+ private _sessionCheckBackoffTimer: ReturnType<typeof setTimeout> | null = null;
198
200
  private _hankoObserver: MutationObserver | null = null;
199
201
 
200
202
  // Hanko signup headline text across all supported languages (used for subtitle injection)
@@ -368,6 +370,8 @@ export class HankoAuth extends LitElement {
368
370
  private _handleVisibilityChange = () => {
369
371
  // Only primary instance should handle visibility changes to prevent race conditions
370
372
  if (!this._isPrimary) return;
373
+ // Don't re-check if a backoff retry is already scheduled
374
+ if (this._sessionCheckBackoffTimer) return;
371
375
 
372
376
  if (!document.hidden && !this.showProfile && !this.user) {
373
377
  // Page became visible, we're in header mode, and no user is logged in
@@ -380,6 +384,8 @@ export class HankoAuth extends LitElement {
380
384
  private _handleWindowFocus = () => {
381
385
  // Only primary instance should handle window focus to prevent race conditions
382
386
  if (!this._isPrimary) return;
387
+ // Don't re-check if a backoff retry is already scheduled
388
+ if (this._sessionCheckBackoffTimer) return;
383
389
 
384
390
  if (!this.showProfile && !this.user) {
385
391
  // Window focused, we're in header mode, and no user is logged in
@@ -549,6 +555,16 @@ export class HankoAuth extends LitElement {
549
555
  }
550
556
  }
551
557
 
558
+ private _scheduleSessionRetry() {
559
+ if (this._sessionCheckBackoffTimer) return; // already scheduled
560
+ const delay = Math.min(1000 * 2 ** this._sessionCheckFailures, 60000);
561
+ this.log(`Session check failed, retrying in ${delay / 1000}s (attempt ${this._sessionCheckFailures})`);
562
+ this._sessionCheckBackoffTimer = setTimeout(() => {
563
+ this._sessionCheckBackoffTimer = null;
564
+ this.checkSession();
565
+ }, delay);
566
+ }
567
+
552
568
  private async checkSession() {
553
569
  this.log("Checking for existing Hanko session...");
554
570
 
@@ -592,6 +608,7 @@ export class HankoAuth extends LitElement {
592
608
  return;
593
609
  }
594
610
 
611
+ this._sessionCheckFailures = 0; // reset backoff on success
595
612
  this.log("Valid Hanko session found via cookie");
596
613
  this.log("Session data:", sessionData);
597
614
 
@@ -710,12 +727,16 @@ export class HankoAuth extends LitElement {
710
727
  this.log("No valid session cookie found - user needs to login");
711
728
  }
712
729
  } catch (validateError) {
730
+ this._sessionCheckFailures++;
713
731
  this.log("Session validation failed:", validateError);
714
- this.log("No valid session - user needs to login");
732
+ this._scheduleSessionRetry();
733
+ return;
715
734
  }
716
735
  } catch (error) {
736
+ this._sessionCheckFailures++;
717
737
  this.log("Session check error:", error);
718
- this.log("No existing session - user needs to login");
738
+ this._scheduleSessionRetry();
739
+ return;
719
740
  } finally {
720
741
  // Broadcast state changes to other instances
721
742
  if (this._isPrimary) {
@@ -921,33 +942,36 @@ export class HankoAuth extends LitElement {
921
942
 
922
943
  updated(changedProperties: Map<string, any>) {
923
944
  super.updated(changedProperties);
924
- // Re-attach event listeners when user becomes null (after logout)
925
- // because a new <hanko-auth> element is created
926
945
  if (
927
- changedProperties.has("user") &&
928
- this.user === null &&
929
- this.showProfile
946
+ (changedProperties.has("user") && this.user === null && this.showProfile) ||
947
+ changedProperties.has("lang")
930
948
  ) {
931
- this.log("User logged out, re-attaching event listeners...");
932
949
  this._currentHankoAuthElement = null;
933
950
  this.setupEventListeners();
934
951
  }
935
952
  }
936
953
 
937
954
  private setupEventListeners() {
938
- // Use updateComplete to ensure DOM is ready
939
955
  this.updateComplete.then(() => {
940
956
  const hankoAuth = this.shadowRoot?.querySelector("hanko-auth");
941
957
 
942
- // Skip if already attached to the same element
943
958
  if (hankoAuth && hankoAuth === this._currentHankoAuthElement) {
944
- this.log("Event listeners already attached to this element");
945
959
  return;
946
960
  }
947
-
961
+ // no exports available for adding css, so injecting it
948
962
  if (hankoAuth) {
949
963
  this._currentHankoAuthElement = hankoAuth;
950
- this.log("Attaching event listeners to hanko-auth element");
964
+ const hankoShadow = (hankoAuth as HTMLElement & { shadowRoot: ShadowRoot | null }).shadowRoot;
965
+ if (hankoShadow && !hankoShadow.querySelector("#hot-hanko-overrides")) {
966
+ const style = document.createElement("style");
967
+ style.id = "hot-hanko-overrides";
968
+ style.textContent = `
969
+ .hanko_lastUsed { margin-left: 8px; }
970
+ .hanko_form .hanko_li { min-width: 100%; }
971
+ `;
972
+ hankoShadow.appendChild(style);
973
+ }
974
+
951
975
  this._setupSignUpSubtitleObserver(hankoAuth);
952
976
 
953
977
  hankoAuth.addEventListener("onSessionCreated", (e: any) => {
@@ -969,7 +993,7 @@ export class HankoAuth extends LitElement {
969
993
  }
970
994
  });
971
995
  }
972
-
996
+ // subtitle handling
973
997
  private _setupSignUpSubtitleObserver(hankoAuth: Element) {
974
998
  const injectSubtitle = () => {
975
999
  const hankoShadow = (hankoAuth as HTMLElement & { shadowRoot: ShadowRoot | null }).shadowRoot;
@@ -978,13 +1002,11 @@ export class HankoAuth extends LitElement {
978
1002
  const h1 = hankoShadow.querySelector<HTMLElement>("h1[part='headline1']");
979
1003
  const headlineText = h1?.textContent?.trim() ?? "";
980
1004
 
981
- // Remove any existing subtitle first
982
1005
  const existing = hankoShadow.querySelector(".hot-subtitle");
983
1006
 
984
1007
  const isSignUp = this._signUpHeadlines.has(headlineText);
985
1008
  const isLogin = this._loginHeadlines.has(headlineText);
986
1009
 
987
- // Only show subtitle on the two initial screens
988
1010
  if (!isSignUp && !isLogin) {
989
1011
  if (existing) {
990
1012
  this._hankoObserver?.disconnect();
@@ -998,11 +1020,9 @@ export class HankoAuth extends LitElement {
998
1020
  ? this.t("signUpSubtitle")
999
1021
  : this.t("loginSubtitle");
1000
1022
 
1001
- // Skip if subtitle already has correct text
1002
1023
  if (existing && existing.textContent === subtitleText) return;
1003
1024
  if (!h1) return;
1004
1025
 
1005
- // Disconnect before modifying DOM to avoid re-triggering the observer
1006
1026
  this._hankoObserver?.disconnect();
1007
1027
 
1008
1028
  if (existing) existing.remove();
@@ -1011,11 +1031,10 @@ export class HankoAuth extends LitElement {
1011
1031
  subtitle.className = "hot-subtitle";
1012
1032
  subtitle.textContent = subtitleText;
1013
1033
  subtitle.style.cssText =
1014
- "margin: -4px 0 16px; text-align: center; font-size: var(--hot-font-size-base, 16px); color: var(--hot-color-gray-600, #6b7280); font-weight: normal;";
1034
+ "margin: -4px 0 16px; text-align: center; font-size: var(--hot-font-size-large); color: var(--hot-color-gray-600, #6b7280); font-weight: normal;";
1015
1035
 
1016
1036
  h1.insertAdjacentElement("afterend", subtitle);
1017
1037
 
1018
- // Re-observe after injection
1019
1038
  this._hankoObserver?.observe(hankoShadow, { childList: true, subtree: true });
1020
1039
  };
1021
1040
 
@@ -9,9 +9,9 @@ export const enOverrides = {
9
9
  signUp: "Create an account",
10
10
  },
11
11
  labels: {
12
- signUp: "Create an account",
12
+ signUp: "Sign up here",
13
13
  alreadyHaveAnAccount: "Already have a HOT account?",
14
- signIn: "Sign in here",
14
+ dontHaveAnAccount: "Don't have a HOT account?",
15
15
  },
16
16
  texts: {
17
17
  enterPasscode:
@@ -120,7 +120,7 @@ export const es = {
120
120
  signInPasskey: "Iniciar sesión con llave de acceso",
121
121
  registerAuthenticator: "Crear una llave de acceso",
122
122
  signIn: "Iniciar sesión",
123
- signUp: "Crear cuenta",
123
+ signUp: "Regístrese aquí",
124
124
  sendNewPasscode: "Enviar nuevo código",
125
125
  passwordRetryAfter: "Reintentar en {passwordRetryAfter}",
126
126
  passcodeResendAfter: "Solicitar nuevo código en {passcodeResendAfter}",
@@ -142,7 +142,7 @@ export const es = {
142
142
  emailOrUsername: "Correo o nombre de usuario",
143
143
  username: "Nombre de usuario",
144
144
  optional: "opcional",
145
- dontHaveAnAccount: "¿No tiene una cuenta?",
145
+ dontHaveAnAccount: "¿No tiene una cuenta HOT?",
146
146
  alreadyHaveAnAccount: "¿Ya tiene una cuenta?",
147
147
  changeUsername: "Cambiar nombre de usuario",
148
148
  setUsername: "Establecer nombre de usuario",
@@ -120,7 +120,7 @@ export const fr = {
120
120
  signInPasskey: "Se connecter avec clé d'accès",
121
121
  registerAuthenticator: "Créer une clé d'accès",
122
122
  signIn: "Se connecter",
123
- signUp: "Créer un compte",
123
+ signUp: "S'inscrire ici",
124
124
  sendNewPasscode: "Envoyer un nouveau code",
125
125
  passwordRetryAfter: "Réessayer dans {passwordRetryAfter}",
126
126
  passcodeResendAfter: "Demander un nouveau code dans {passcodeResendAfter}",
@@ -142,7 +142,7 @@ export const fr = {
142
142
  emailOrUsername: "E-mail ou nom d'utilisateur",
143
143
  username: "Nom d'utilisateur",
144
144
  optional: "optionnel",
145
- dontHaveAnAccount: "Vous n'avez pas de compte ?",
145
+ dontHaveAnAccount: "Vous n'avez pas de compte HOT ?",
146
146
  alreadyHaveAnAccount: "Vous avez déjà un compte ?",
147
147
  changeUsername: "Changer le nom d'utilisateur",
148
148
  setUsername: "Définir le nom d'utilisateur",
@@ -119,7 +119,7 @@ export const pt = {
119
119
  signInPasskey: "Entrar com chave de acesso",
120
120
  registerAuthenticator: "Criar uma chave de acesso",
121
121
  signIn: "Entrar",
122
- signUp: "Criar conta",
122
+ signUp: "Cadastre-se aqui",
123
123
  sendNewPasscode: "Enviar novo código",
124
124
  passwordRetryAfter: "Tentar novamente em {passwordRetryAfter}",
125
125
  passcodeResendAfter: "Solicitar novo código em {passcodeResendAfter}",
@@ -141,7 +141,7 @@ export const pt = {
141
141
  emailOrUsername: "E-mail ou nome de usuário",
142
142
  username: "Nome de usuário",
143
143
  optional: "opcional",
144
- dontHaveAnAccount: "Não tem uma conta?",
144
+ dontHaveAnAccount: "Não tem uma conta HOT?",
145
145
  alreadyHaveAnAccount: "Já tem uma conta?",
146
146
  changeUsername: "Alterar nome de usuário",
147
147
  setUsername: "Definir nome de usuário",
@@ -1,15 +1,30 @@
1
- /* overrides for Hanko's UI */
2
-
1
+ // Hanko's UI overwrites
3
2
  import { en } from "@teamhanko/hanko-elements/i18n/en";
3
+ import { fr as hankoFr } from "@teamhanko/hanko-elements/i18n/fr";
4
4
  import { enOverrides } from "./hanko-i18n-en";
5
- import { es } from "./hanko-i18n-es";
6
- import { fr } from "./hanko-i18n-fr";
7
- import { pt } from "./hanko-i18n-pt";
5
+ import { es as esOverrides } from "./hanko-i18n-es";
6
+ import { fr as frOverrides } from "./hanko-i18n-fr";
7
+ import { pt as ptOverrides } from "./hanko-i18n-pt";
8
8
 
9
9
  Object.assign(en.headlines, enOverrides.headlines);
10
10
  Object.assign(en.labels, enOverrides.labels);
11
11
  Object.assign(en.texts, enOverrides.texts);
12
12
 
13
+ const fr = JSON.parse(JSON.stringify(hankoFr));
14
+ Object.assign(fr.headlines, frOverrides.headlines);
15
+ Object.assign(fr.labels, frOverrides.labels);
16
+ Object.assign(fr.texts, frOverrides.texts);
17
+
18
+ const es = JSON.parse(JSON.stringify(en));
19
+ Object.assign(es.headlines, esOverrides.headlines);
20
+ Object.assign(es.labels, esOverrides.labels);
21
+ Object.assign(es.texts, esOverrides.texts);
22
+
23
+ const pt = JSON.parse(JSON.stringify(en));
24
+ Object.assign(pt.headlines, ptOverrides.headlines);
25
+ Object.assign(pt.labels, ptOverrides.labels);
26
+ Object.assign(pt.texts, ptOverrides.texts);
27
+
13
28
  export function getTranslations() {
14
29
  return { en, es, fr, pt };
15
30
  }