@hotosm/hanko-auth 0.5.0 → 0.5.1

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.0",
3
+ "version": "0.5.1",
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",
@@ -6,10 +6,14 @@ export const styles = css`
6
6
  font-family: var(--font-family, var(--hot-font-sans));
7
7
  }
8
8
 
9
+ hanko-auth::part(headline1) {
10
+ text-align: center;
11
+ }
12
+
9
13
  .container {
10
14
  max-width: 400px;
11
15
  margin: 0 auto;
12
- padding: var(--hot-spacing-large);
16
+ padding: var(--hot-spacing-x-small) var(--hot-spacing-large);
13
17
  }
14
18
 
15
19
  .loading {
@@ -32,13 +36,12 @@ export const styles = css`
32
36
  }
33
37
 
34
38
  .spinner {
35
- width: clamp(40px, 10%, 60px);
36
- height: clamp(40px, 10%, 60px);
37
- border: 4px solid var(--hot-color-gray-50);
39
+ width: 25px;
40
+ height: 25px;
41
+ border: 4px solid var(--hot-color-red-50);
38
42
  border-top: 4px solid var(--hot-color-red-600);
39
43
  border-radius: 50%;
40
44
  animation: spin 1s linear infinite;
41
- margin: 0 auto;
42
45
  }
43
46
  /* Container that mimics the avatar/dropdown-trigger dimensions */
44
47
  .loading-placeholder {
@@ -521,5 +524,6 @@ export const styles = css`
521
524
  /* Style Hanko's internal link button (e.g. "Create account") */
522
525
  hanko-auth::part(link) {
523
526
  font-weight: bold;
527
+ text-decoration: underline;
524
528
  }
525
529
  `;
package/src/hanko-auth.ts CHANGED
@@ -6,10 +6,7 @@ import { keyed } from "lit/directives/keyed.js";
6
6
  import { register } from "@teamhanko/hanko-elements";
7
7
  import { styles } from "./hanko-auth.styles";
8
8
  // hanko ui translations
9
- import { en } from "@teamhanko/hanko-elements/i18n/en";
10
- import { es } from "./hanko-i18n-es";
11
- import { fr } from "./hanko-i18n-fr";
12
- import { pt } from "./hanko-i18n-pt";
9
+ import { getTranslations } from "./hanko-translations";
13
10
  // custom component translations
14
11
  import { translations } from "./translations";
15
12
 
@@ -36,7 +33,7 @@ async function ensureHankoRegistered(hankoUrl: string): Promise<void> {
36
33
  await register(hankoUrl, {
37
34
  enablePasskeys: false,
38
35
  hidePasskeyButtonOnLogin: true,
39
- translations: { en, es, fr, pt },
36
+ translations: getTranslations(),
40
37
  fallbackLanguage: "en",
41
38
  });
42
39
  hankoRegistered = true;
@@ -198,6 +195,28 @@ export class HankoAuth extends LitElement {
198
195
  private _lastSessionId: string | null = null;
199
196
  private _hanko: any = null;
200
197
  private _isPrimary = false; // Is this the primary instance?
198
+ private _hankoObserver: MutationObserver | null = null;
199
+
200
+ // Hanko signup headline text across all supported languages (used for subtitle injection)
201
+ private _signUpHeadlines = new Set([
202
+ "Create an account", // en (our override)
203
+ "Crear cuenta", // es
204
+ "Créer un compte", // fr
205
+ "Criar conta", // pt
206
+ ]);
207
+
208
+ // Hanko initial login (email entry) headline text across all supported languages
209
+ // Covers both loginEmail and loginEmailNoSignup variants
210
+ private _loginHeadlines = new Set([
211
+ "Sign in or create account", // en loginEmail
212
+ "Sign in", // en loginEmailNoSignup
213
+ "Iniciar sesión o crear cuenta", // es loginEmail
214
+ "Iniciar sesión", // es loginEmailNoSignup
215
+ "Se connecter ou s'inscrire", // fr loginEmail
216
+ "Se connecter", // fr loginEmailNoSignup
217
+ "Entrar ou criar conta", // pt loginEmail
218
+ "Entrar", // pt loginEmailNoSignup
219
+ ]);
201
220
 
202
221
  constructor() {
203
222
  super();
@@ -929,6 +948,7 @@ export class HankoAuth extends LitElement {
929
948
  if (hankoAuth) {
930
949
  this._currentHankoAuthElement = hankoAuth;
931
950
  this.log("Attaching event listeners to hanko-auth element");
951
+ this._setupSignUpSubtitleObserver(hankoAuth);
932
952
 
933
953
  hankoAuth.addEventListener("onSessionCreated", (e: any) => {
934
954
  this.log(`Hanko event: onSessionCreated`, e.detail);
@@ -950,6 +970,67 @@ export class HankoAuth extends LitElement {
950
970
  });
951
971
  }
952
972
 
973
+ private _setupSignUpSubtitleObserver(hankoAuth: Element) {
974
+ const injectSubtitle = () => {
975
+ const hankoShadow = (hankoAuth as HTMLElement & { shadowRoot: ShadowRoot | null }).shadowRoot;
976
+ if (!hankoShadow) return;
977
+
978
+ const h1 = hankoShadow.querySelector<HTMLElement>("h1[part='headline1']");
979
+ const headlineText = h1?.textContent?.trim() ?? "";
980
+
981
+ // Remove any existing subtitle first
982
+ const existing = hankoShadow.querySelector(".hot-subtitle");
983
+
984
+ const isSignUp = this._signUpHeadlines.has(headlineText);
985
+ const isLogin = this._loginHeadlines.has(headlineText);
986
+
987
+ // Only show subtitle on the two initial screens
988
+ if (!isSignUp && !isLogin) {
989
+ if (existing) {
990
+ this._hankoObserver?.disconnect();
991
+ existing.remove();
992
+ this._hankoObserver?.observe(hankoShadow, { childList: true, subtree: true });
993
+ }
994
+ return;
995
+ }
996
+
997
+ const subtitleText = isSignUp
998
+ ? this.t("signUpSubtitle")
999
+ : this.t("loginSubtitle");
1000
+
1001
+ // Skip if subtitle already has correct text
1002
+ if (existing && existing.textContent === subtitleText) return;
1003
+ if (!h1) return;
1004
+
1005
+ // Disconnect before modifying DOM to avoid re-triggering the observer
1006
+ this._hankoObserver?.disconnect();
1007
+
1008
+ if (existing) existing.remove();
1009
+
1010
+ const subtitle = document.createElement("p");
1011
+ subtitle.className = "hot-subtitle";
1012
+ subtitle.textContent = subtitleText;
1013
+ 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;";
1015
+
1016
+ h1.insertAdjacentElement("afterend", subtitle);
1017
+
1018
+ // Re-observe after injection
1019
+ this._hankoObserver?.observe(hankoShadow, { childList: true, subtree: true });
1020
+ };
1021
+
1022
+ if (this._hankoObserver) {
1023
+ this._hankoObserver.disconnect();
1024
+ }
1025
+
1026
+ const hankoShadow = (hankoAuth as HTMLElement & { shadowRoot: ShadowRoot | null }).shadowRoot;
1027
+ if (hankoShadow) {
1028
+ this._hankoObserver = new MutationObserver(() => injectSubtitle());
1029
+ this._hankoObserver.observe(hankoShadow, { childList: true, subtree: true });
1030
+ injectSubtitle();
1031
+ }
1032
+ }
1033
+
953
1034
  private async handleHankoSuccess(event: any) {
954
1035
  this.log("Hanko auth success:", event.detail);
955
1036
 
@@ -1444,7 +1525,6 @@ window.location.href = redirectUrl;
1444
1525
  <div class="osm-section">
1445
1526
  <div class="osm-connected">
1446
1527
  <div class="osm-badge">
1447
- <span class="osm-badge-icon">🗺️</span>
1448
1528
  <div>
1449
1529
  <div>${this.t("connectedToOpenStreetMap")}</div>
1450
1530
  ${this.osmData?.osm_username
@@ -1633,7 +1713,7 @@ window.location.href = redirectUrl;
1633
1713
  --color: var(--hot-color-gray-900);
1634
1714
  --color-shade-1: var(--hot-color-gray-700);
1635
1715
  --color-shade-2: var(--hot-color-gray-100);
1636
- --brand-color: var(--hot-color-gray-800);
1716
+ --brand-color: var(--hot-color-gray-1000);
1637
1717
  --brand-color-shade-1: var(--hot-color-gray-900);
1638
1718
  --brand-contrast-color: white;
1639
1719
  --background-color: white;
@@ -1645,7 +1725,7 @@ window.location.href = redirectUrl;
1645
1725
  --item-height: 2.75rem;
1646
1726
  --item-margin: var(--hot-spacing-small) 0;
1647
1727
  --container-padding: 0;
1648
- --headline1-font-size: var(--hot-font-size-large);
1728
+ --headline1-font-size: var(--hot-font-size-xl);
1649
1729
  --headline1-font-weight: var(--hot-font-weight-semibold);
1650
1730
  --headline2-font-size: var(--hot-font-size-medium);
1651
1731
  --headline2-font-weight: var(--hot-font-weight-semibold);
@@ -1653,7 +1733,7 @@ window.location.href = redirectUrl;
1653
1733
  >
1654
1734
  ${keyed(
1655
1735
  this.lang,
1656
- html`<hanko-auth lang="${this.lang}"></hanko-auth>`,
1736
+ html`<hanko-auth lang="${this.lang}" exportparts="link"></hanko-auth>`,
1657
1737
  )}
1658
1738
  </div>
1659
1739
  `;
@@ -1698,5 +1778,5 @@ declare global {
1698
1778
  }
1699
1779
  }
1700
1780
 
1701
- // Re-export Hanko translations for use by consuming apps
1702
- export { en, es, fr, pt };
1781
+ // Re-export for use by consuming apps
1782
+ export { getTranslations } from "./hanko-translations";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * English (en) overrides for Hanko Elements
3
+ * Only include keys that differ from the default Hanko English translations.
4
+ * These are merged on top of enBase in hanko-auth.ts.
5
+ */
6
+
7
+ export const enOverrides = {
8
+ headlines: {
9
+ signUp: "Create an account",
10
+ },
11
+ labels: {
12
+ signUp: "Create an account",
13
+ alreadyHaveAnAccount: "Already have a HOT account?",
14
+ signIn: "Sign in here",
15
+ },
16
+ texts: {
17
+ enterPasscode:
18
+ "Please enter below the passcode we’ve sent to your email address:",
19
+ },
20
+ };
@@ -48,12 +48,13 @@ export const es = {
48
48
  deleteSecurityKey: "Eliminar clave de seguridad",
49
49
  securityKeys: "Claves de seguridad",
50
50
  authenticatorApp: "Aplicación de autenticación",
51
- authenticatorAppAlreadySetUp: "La aplicación de autenticación está configurada",
51
+ authenticatorAppAlreadySetUp:
52
+ "La aplicación de autenticación está configurada",
52
53
  authenticatorAppNotSetUp: "Configurar aplicación de autenticación",
53
54
  trustDevice: "¿Confiar en este navegador?",
54
55
  },
55
56
  texts: {
56
- enterPasscode: 'Ingrese el código que se envió a "{emailAddress}".',
57
+ enterPasscode: "Ingrese el código que se envió a su correo electrónico:",
57
58
  enterPasscodeNoEmail:
58
59
  "Ingrese el código que se envió a su dirección de correo principal.",
59
60
  setupPasskey:
@@ -172,8 +173,7 @@ export const es = {
172
173
  "El código se ha ingresado incorrectamente demasiadas veces. Por favor, solicite un nuevo código.",
173
174
  tooManyRequests:
174
175
  "Se han realizado demasiadas solicitudes. Por favor, espere para repetir la operación solicitada.",
175
- unauthorized:
176
- "Su sesión ha expirado. Por favor, inicie sesión nuevamente.",
176
+ unauthorized: "Su sesión ha expirado. Por favor, inicie sesión nuevamente.",
177
177
  invalidWebauthnCredential: "Esta llave de acceso ya no se puede usar.",
178
178
  passcodeExpired: "El código ha expirado. Por favor, solicite uno nuevo.",
179
179
  userVerification:
@@ -48,12 +48,13 @@ export const fr = {
48
48
  deleteSecurityKey: "Supprimer la clé de sécurité",
49
49
  securityKeys: "Clés de sécurité",
50
50
  authenticatorApp: "Application d'authentification",
51
- authenticatorAppAlreadySetUp: "L'application d'authentification est configurée",
51
+ authenticatorAppAlreadySetUp:
52
+ "L'application d'authentification est configurée",
52
53
  authenticatorAppNotSetUp: "Configurer l'application d'authentification",
53
54
  trustDevice: "Faire confiance à ce navigateur ?",
54
55
  },
55
56
  texts: {
56
- enterPasscode: 'Entrez le code envoyé à "{emailAddress}".',
57
+ enterPasscode: "Entrez le code envoyé à votre adresse e-mail:",
57
58
  enterPasscodeNoEmail:
58
59
  "Entrez le code envoyé à votre adresse e-mail principale.",
59
60
  setupPasskey:
@@ -172,8 +173,7 @@ export const fr = {
172
173
  "Le code a été saisi incorrectement trop de fois. Veuillez demander un nouveau code.",
173
174
  tooManyRequests:
174
175
  "Trop de demandes ont été effectuées. Veuillez attendre avant de répéter l'opération demandée.",
175
- unauthorized:
176
- "Votre session a expiré. Veuillez vous reconnecter.",
176
+ unauthorized: "Votre session a expiré. Veuillez vous reconnecter.",
177
177
  invalidWebauthnCredential: "Cette clé d'accès ne peut plus être utilisée.",
178
178
  passcodeExpired: "Le code a expiré. Veuillez en demander un nouveau.",
179
179
  userVerification:
@@ -48,12 +48,13 @@ export const pt = {
48
48
  deleteSecurityKey: "Excluir chave de segurança",
49
49
  securityKeys: "Chaves de segurança",
50
50
  authenticatorApp: "Aplicativo de autenticação",
51
- authenticatorAppAlreadySetUp: "O aplicativo de autenticação está configurado",
51
+ authenticatorAppAlreadySetUp:
52
+ "O aplicativo de autenticação está configurado",
52
53
  authenticatorAppNotSetUp: "Configurar aplicativo de autenticação",
53
54
  trustDevice: "Confiar neste navegador?",
54
55
  },
55
56
  texts: {
56
- enterPasscode: 'Digite o código enviado para "{emailAddress}".',
57
+ enterPasscode: "Digite o código enviado para o seu endereço de e-mail:",
57
58
  enterPasscodeNoEmail:
58
59
  "Digite o código enviado para seu endereço de e-mail principal.",
59
60
  setupPasskey:
@@ -65,8 +66,7 @@ export const pt = {
65
66
  otpScanQRCode:
66
67
  "Digitalize o código QR usando seu aplicativo de autenticação (como Google Authenticator ou qualquer outro aplicativo TOTP). Alternativamente, você pode inserir manualmente a chave secreta OTP no aplicativo.",
67
68
  otpSecretKey: "Chave secreta OTP",
68
- passwordFormatHint:
69
- "Deve ter entre {minLength} e {maxLength} caracteres.",
69
+ passwordFormatHint: "Deve ter entre {minLength} e {maxLength} caracteres.",
70
70
  securityKeySetUp:
71
71
  "Use uma chave de segurança dedicada via USB, Bluetooth ou NFC, ou seu telefone celular. Conecte ou ative sua chave de segurança, depois clique no botão abaixo e siga as instruções para concluir o registro.",
72
72
  setPrimaryEmail:
@@ -172,8 +172,7 @@ export const pt = {
172
172
  "O código foi inserido incorretamente muitas vezes. Por favor, solicite um novo código.",
173
173
  tooManyRequests:
174
174
  "Muitas solicitações foram feitas. Por favor, aguarde antes de repetir a operação solicitada.",
175
- unauthorized:
176
- "Sua sessão expirou. Por favor, faça login novamente.",
175
+ unauthorized: "Sua sessão expirou. Por favor, faça login novamente.",
177
176
  invalidWebauthnCredential: "Esta chave de acesso não pode mais ser usada.",
178
177
  passcodeExpired: "O código expirou. Por favor, solicite um novo.",
179
178
  userVerification:
@@ -194,8 +193,7 @@ export const pt = {
194
193
  flowErrors: {
195
194
  technical_error:
196
195
  "Ocorreu um erro técnico. Por favor, tente novamente mais tarde.",
197
- flow_expired_error:
198
- "A sessão expirou, clique no botão para reiniciar.",
196
+ flow_expired_error: "A sessão expirou, clique no botão para reiniciar.",
199
197
  value_invalid_error: "O valor inserido não é válido.",
200
198
  passcode_invalid: "O código fornecido não está correto.",
201
199
  passkey_invalid: "Esta chave de acesso não pode mais ser usada.",
@@ -213,8 +211,7 @@ export const pt = {
213
211
  operation_not_permitted_error: "A operação não é permitida.",
214
212
  flow_discontinuity_error:
215
213
  "O processo não pode continuar devido à configuração do usuário ou do provedor.",
216
- form_data_invalid_error:
217
- "Os dados do formulário enviados contêm erros.",
214
+ form_data_invalid_error: "Os dados do formulário enviados contêm erros.",
218
215
  unauthorized: "Sua sessão expirou. Por favor, faça login novamente.",
219
216
  value_missing_error: "O valor está faltando.",
220
217
  value_too_long_error: "O valor é muito longo.",
@@ -0,0 +1,15 @@
1
+ /* overrides for Hanko's UI */
2
+
3
+ import { en } from "@teamhanko/hanko-elements/i18n/en";
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";
8
+
9
+ Object.assign(en.headlines, enOverrides.headlines);
10
+ Object.assign(en.labels, enOverrides.labels);
11
+ Object.assign(en.texts, enOverrides.texts);
12
+
13
+ export function getTranslations() {
14
+ return { en, es, fr, pt };
15
+ }
@@ -17,6 +17,8 @@ export interface Translations {
17
17
  openAccountMenu: string;
18
18
  connectedToOsmAs: string;
19
19
  osmConnectionRequired: string;
20
+ signUpSubtitle: string;
21
+ loginSubtitle: string;
20
22
  }
21
23
 
22
24
  export const translations: Record<string, Translations> = {
@@ -35,6 +37,8 @@ export const translations: Record<string, Translations> = {
35
37
  openAccountMenu: "Open account menu",
36
38
  connectedToOsmAs: "Connected to OSM as",
37
39
  osmConnectionRequired: "OSM connection required",
40
+ signUpSubtitle: "Access all HOT tools and services",
41
+ loginSubtitle: "With your HOT account",
38
42
  },
39
43
  es: {
40
44
  logIn: "Iniciar sesión",
@@ -51,6 +55,8 @@ export const translations: Record<string, Translations> = {
51
55
  openAccountMenu: "Abrir menú de cuenta",
52
56
  connectedToOsmAs: "Conectado a OSM como",
53
57
  osmConnectionRequired: "Se requiere conexión OSM",
58
+ signUpSubtitle: "Accede a todas las herramientas y servicios de HOT",
59
+ loginSubtitle: "Con tu cuenta HOT",
54
60
  },
55
61
  fr: {
56
62
  logIn: "Se connecter",
@@ -67,6 +73,8 @@ export const translations: Record<string, Translations> = {
67
73
  openAccountMenu: "Ouvrir le menu du compte",
68
74
  connectedToOsmAs: "Connecté à OSM en tant que",
69
75
  osmConnectionRequired: "Connexion OSM requise",
76
+ signUpSubtitle: "Accédez à tous les outils et services HOT",
77
+ loginSubtitle: "Avec votre compte HOT",
70
78
  },
71
79
  pt: {
72
80
  logIn: "Entrar",
@@ -83,5 +91,7 @@ export const translations: Record<string, Translations> = {
83
91
  openAccountMenu: "Abrir menu da conta",
84
92
  connectedToOsmAs: "Conectado ao OSM como",
85
93
  osmConnectionRequired: "Conexão OSM necessária",
94
+ signUpSubtitle: "Acesse todas as ferramentas e serviços HOT",
95
+ loginSubtitle: "Com a sua conta HOT",
86
96
  },
87
97
  };