@hotosm/hanko-auth 0.3.5 β†’ 0.4.2

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
@@ -12,6 +12,12 @@
12
12
  import { LitElement, html, css } from "lit";
13
13
  import { customElement, property, state } from "lit/decorators.js";
14
14
  import { register } from "@teamhanko/hanko-elements";
15
+ import { en } from "@teamhanko/hanko-elements/i18n/en";
16
+ import { es } from "./hanko-i18n-es";
17
+ import { fr } from "./hanko-i18n-fr";
18
+ import { pt } from "./hanko-i18n-pt";
19
+ import { styles } from "./hanko-auth.styles";
20
+ import { translations } from "./translations";
15
21
  //Icons
16
22
  import accountIcon from "../assets/icon-account.svg";
17
23
  import logoutIcon from "../assets/icon-logout.svg";
@@ -50,6 +56,7 @@ interface OSMData {
50
56
 
51
57
  @customElement("hotosm-auth")
52
58
  export class HankoAuth extends LitElement {
59
+ static styles = styles;
53
60
  // Properties (from attributes)
54
61
  @property({ type: String, attribute: "hanko-url" }) hankoUrlAttr = "";
55
62
  @property({ type: String, attribute: "base-path" }) basePath = "";
@@ -74,6 +81,18 @@ export class HankoAuth extends LitElement {
74
81
  @property({ type: String, attribute: "app-id" }) appId = "";
75
82
  // Custom login page URL (for standalone mode - overrides ${hankoUrl}/app)
76
83
  @property({ type: String, attribute: "login-url" }) loginUrl = "";
84
+ // Language code (en, es, fr, pt, etc.)
85
+ @property({ type: String }) lang = "en";
86
+ // Button variant (filled, outline, plain)
87
+ @property({ type: String, attribute: "button-variant" }) buttonVariant:
88
+ | "filled"
89
+ | "outline"
90
+ | "plain" = "plain";
91
+ // Button color (primary, neutral, danger)
92
+ @property({ type: String, attribute: "button-color" }) buttonColor:
93
+ | "primary"
94
+ | "neutral"
95
+ | "danger" = "primary";
77
96
 
78
97
  // Internal state
79
98
  @state() private user: UserState | null = null;
@@ -84,15 +103,26 @@ export class HankoAuth extends LitElement {
84
103
  @state() private error: string | null = null;
85
104
  @state() private profileDisplayName: string = "";
86
105
  @state() private hasAppMapping = false; // True if user has mapping in the app
106
+ @state() private userProfileLanguage: string | null = null; // Language from user profile
87
107
  // dropdown
88
108
  @state() private isOpen = false;
89
109
 
90
110
  private toggleDropdown() {
91
111
  this.isOpen = !this.isOpen;
112
+ if (this.isOpen) {
113
+ // Add listener when dropdown opens
114
+ setTimeout(() => {
115
+ document.addEventListener("click", this.handleOutsideClick);
116
+ }, 0);
117
+ } else {
118
+ // Remove listener when dropdown closes
119
+ document.removeEventListener("click", this.handleOutsideClick);
120
+ }
92
121
  }
93
122
 
94
123
  private closeDropdown() {
95
124
  this.isOpen = false;
125
+ document.removeEventListener("click", this.handleOutsideClick);
96
126
  }
97
127
  private handleOutsideClick = (event: MouseEvent) => {
98
128
  if (!this.contains(event.target as Node)) {
@@ -107,332 +137,6 @@ export class HankoAuth extends LitElement {
107
137
  private _hanko: any = null;
108
138
  private _isPrimary = false; // Is this the primary instance?
109
139
 
110
- static styles = css`
111
- :host {
112
- display: block;
113
- font-family: var(--hot-font-sans);
114
- }
115
-
116
- .container {
117
- max-width: 400px;
118
- margin: 0 auto;
119
- padding: var(--hot-spacing-large);
120
- }
121
-
122
- .loading {
123
- text-align: center;
124
- padding: var(--hot-spacing-3x-large);
125
- color: var(--hot-color-gray-600);
126
- }
127
-
128
- .osm-connecting {
129
- display: flex;
130
- flex-direction: column;
131
- align-items: center;
132
- gap: var(--hot-spacing-small);
133
- padding: var(--hot-spacing-large);
134
- }
135
-
136
- .spinner {
137
- width: var(--hot-spacing-3x-large);
138
- height: var(--hot-spacing-3x-large);
139
- border: var(--hot-spacing-2x-small) solid var(--hot-color-gray-50);
140
- border-top: var(--hot-spacing-2x-small) solid var(--hot-color-red-600);
141
- border-radius: 50%;
142
- animation: spin 1s linear infinite;
143
- }
144
-
145
- @keyframes spin {
146
- 0% {
147
- transform: rotate(0deg);
148
- }
149
- 100% {
150
- transform: rotate(360deg);
151
- }
152
- }
153
-
154
- .connecting-text {
155
- font-size: var(--hot-font-size-small);
156
- color: var(--hot-color-gray-600);
157
- font-weight: var(--hot-font-weight-semibold);
158
- }
159
-
160
- button {
161
- width: 100%;
162
- padding: 12px 20px;
163
- border: none;
164
- border-radius: 6px;
165
- font-size: 14px;
166
- font-weight: 500;
167
- cursor: pointer;
168
- transition: all 0.2s;
169
- }
170
-
171
- .btn-primary {
172
- background: var(--hot-color-gray-700);
173
- color: white;
174
- }
175
-
176
- .btn-primary:hover {
177
- background: var(--hot-color-gray-600);
178
- }
179
-
180
- .btn-secondary {
181
- border: 1px solid var(--hot-color-gray-700);
182
- border-radius: var(--hot-border-radius-medium);
183
- background-color: white;
184
- color: var(--hot-color-gray-700);
185
- margin-top: 8px;
186
- }
187
-
188
- .btn-secondary:hover {
189
- background: var(--hot-color-gray-50);
190
- }
191
-
192
- .error {
193
- background: var(--hot-color-red-50);
194
- border: var(--hot-border-width, 1px) solid var(--hot-color-red-200);
195
- border-radius: var(--hot-border-radius-medium);
196
- padding: var(--hot-spacing-small);
197
- color: var(--hot-color-red-700);
198
- margin-bottom: var(--hot-spacing-medium);
199
- }
200
-
201
- .profile {
202
- background: var(--hot-color-gray-50);
203
- border-radius: var(--hot-border-radius-large);
204
- padding: var(--hot-spacing-large);
205
- margin-bottom: var(--hot-spacing-medium);
206
- }
207
-
208
- .profile-header {
209
- display: flex;
210
- align-items: center;
211
- gap: var(--hot-spacing-small);
212
- margin-bottom: var(--hot-spacing-medium);
213
- }
214
-
215
- .profile-avatar {
216
- width: var(--hot-spacing-3x-large);
217
- height: var(--hot-spacing-3x-large);
218
- border-radius: 50%;
219
- background: var(--hot-color-gray-200);
220
- display: flex;
221
- align-items: center;
222
- justify-content: center;
223
- font-size: var(--hot-font-size-large);
224
- font-weight: var(--hot-font-weight-bold);
225
- color: var(--hot-color-gray-600);
226
- }
227
-
228
- .profile-info {
229
- padding: var(--hot-spacing-x-small) var(--hot-spacing-medium);
230
- }
231
-
232
- .profile-email {
233
- font-size: var(--hot-font-size-small);
234
- font-weight: var(--hot-font-weight-bold);
235
- }
236
-
237
- .osm-section {
238
- border-top: var(--hot-border-width, 1px) solid var(--hot-color-gray-100);
239
- padding-top: var(--hot-spacing-medium);
240
- padding-bottom: var(--hot-spacing-small);
241
- margin-top: var(--hot-spacing-medium);
242
- text-align: center;
243
- }
244
-
245
- .osm-connected {
246
- display: flex;
247
- align-items: center;
248
- justify-content: center;
249
- padding: var(--hot-spacing-small);
250
- background: linear-gradient(
251
- 135deg,
252
- var(--hot-color-success-50) 0%,
253
- var(--hot-color-success-50) 100%
254
- );
255
- border-radius: var(--hot-border-radius-large);
256
- border: var(--hot-border-width, 1px) solid var(--hot-color-success-200);
257
- }
258
-
259
- .osm-badge {
260
- display: flex;
261
- align-items: center;
262
- gap: var(--hot-spacing-x-small);
263
- color: var(--hot-color-success-800);
264
- font-weight: var(--hot-font-weight-semibold);
265
- font-size: var(--hot-font-size-small);
266
- text-align: left;
267
- }
268
-
269
- .osm-badge-icon {
270
- font-size: var(--hot-font-size-medium);
271
- }
272
-
273
- .osm-username {
274
- font-size: var(--hot-font-size-x-small);
275
- color: var(--hot-color-success-700);
276
- margin-top: var(--hot-spacing-2x-small);
277
- }
278
- .osm-prompt {
279
- background: var(--hot-color-warning-50);
280
- border: var(--hot-border-width, 1px) solid var(--hot-color-warning-200);
281
- border-radius: var(--hot-border-radius-large);
282
- padding: var(--hot-spacing-large);
283
- margin-bottom: var(--hot-spacing-medium);
284
- text-align: center;
285
- }
286
-
287
- .osm-prompt-title {
288
- font-weight: var(--hot-font-weight-semibold);
289
- font-size: var(--hot-font-size-medium);
290
- margin-bottom: var(--hot-spacing-small);
291
- color: var(--hot-color-gray-900);
292
- text-align: center;
293
- }
294
-
295
- .osm-prompt-text {
296
- font-size: var(--hot-font-size-small);
297
- color: var(--hot-color-gray-600);
298
- margin-bottom: var(--hot-spacing-medium);
299
- line-height: var(--hot-line-height-normal);
300
- text-align: center;
301
- }
302
-
303
- .osm-status-badge {
304
- position: absolute;
305
- top: 2px;
306
- right: 2px;
307
- width: var(--hot-font-size-small);
308
- height: var(--hot-font-size-small);
309
- border-radius: 50%;
310
- border: var(--hot-spacing-3x-small) solid white;
311
- display: flex;
312
- align-items: center;
313
- justify-content: center;
314
- font-size: var(--hot-font-size-2x-small);
315
- color: white;
316
- font-weight: var(--hot-font-weight-bold);
317
- }
318
-
319
- .osm-status-badge.connected {
320
- background-color: var(--hot-color-success-600);
321
- }
322
-
323
- .osm-status-badge.required {
324
- background-color: var(--hot-color-warning-600);
325
- }
326
- .header-avatar {
327
- width: var(--hot-spacing-2x-large);
328
- height: var(--hot-spacing-2x-large);
329
- border-radius: 50%;
330
- background: var(--hot-color-gray-800);
331
- display: inline-flex;
332
- align-items: center;
333
- justify-content: center;
334
- font-size: var(--hot-font-size-small);
335
- font-weight: var(--hot-font-weight-semibold);
336
- color: white;
337
- }
338
-
339
- .login-link {
340
- color: var(--hot-color-neutral-950);
341
- font-size: var(--hot-font-size-small);
342
- border-radius: var(--hot-border-radius-medium);
343
- text-decoration: none;
344
- padding: 14px;
345
- }
346
- .login-link:hover {
347
- background: var(--hot-color-gray-50);
348
- }
349
- /* Dropdown styles */
350
- .dropdown {
351
- position: relative;
352
- display: inline-block;
353
- }
354
- .dropdown-trigger {
355
- background: none;
356
- border: none;
357
- padding: var(--hot-spacing-x-small);
358
- cursor: pointer;
359
- position: relative;
360
- }
361
-
362
- .dropdown-trigger.no-hover:hover,
363
- .dropdown-trigger.no-hover:active,
364
- .dropdown-trigger.no-hover:focus {
365
- background: none;
366
- outline: none;
367
- }
368
- .dropdown-content {
369
- position: absolute;
370
- right: 0;
371
- background: white;
372
- border: 1px solid var(--hot-color-gray-100);
373
- border-radius: var(--hot-border-radius-medium);
374
- z-index: 1000;
375
- opacity: 0;
376
- visibility: hidden;
377
- transform: translateY(-10px);
378
- transition:
379
- opacity 0.2s ease,
380
- visibility 0.2s ease,
381
- transform 0.2s ease;
382
- }
383
- @media (max-width: 768px) {
384
- .dropdown-content {
385
- position: fixed;
386
- width: 100%;
387
- }
388
- }
389
-
390
- .dropdown-content.open {
391
- opacity: 1;
392
- visibility: visible;
393
- transform: translateY(0);
394
- }
395
-
396
- .dropdown-content button {
397
- display: flex;
398
- align-items: center;
399
- width: 100%;
400
- padding: var(--hot-spacing-small) var(--hot-spacing-medium);
401
- background: none;
402
- border: none;
403
- cursor: pointer;
404
- text-align: left;
405
- transition: background-color 0.2s ease;
406
- gap: var(--hot-spacing-small);
407
- font-size: var(--hot-font-size-small);
408
- color: var(--hot-color-gray-900);
409
- }
410
-
411
- .dropdown-content button:hover {
412
- background-color: var(--hot-color-gray-50);
413
- }
414
-
415
- .dropdown-content button:focus {
416
- background-color: var(--hot-color-gray-50);
417
- outline: 2px solid var(--hot-color-gray-500);
418
- outline-offset: -2px;
419
- }
420
-
421
- .dropdown-content .profile-info {
422
- padding: var(--hot-spacing-small) var(--hot-spacing-medium);
423
- }
424
-
425
- .dropdown-content .profile-email {
426
- font-size: var(--hot-font-size-small);
427
- font-weight: var(--hot-font-weight-bold);
428
- }
429
-
430
- .icon {
431
- width: 20px;
432
- height: 20px;
433
- }
434
- `;
435
-
436
140
  // Get computed hankoUrl (priority: attribute > meta tag > window.HANKO_URL > origin)
437
141
  get hankoUrl(): string {
438
142
  if (this.hankoUrlAttr) {
@@ -619,6 +323,21 @@ export class HankoAuth extends LitElement {
619
323
  }
620
324
  }
621
325
 
326
+ /**
327
+ * Get translated string for the current language
328
+ * Falls back to English if translation not found
329
+ * When user is logged in, uses their profile language instead of the lang prop
330
+ */
331
+ private t(key: keyof typeof translations.en): string {
332
+ // When user is logged in, use their profile language
333
+ const effectiveLang =
334
+ this.user && this.userProfileLanguage
335
+ ? this.userProfileLanguage
336
+ : this.lang;
337
+ const langTranslations = translations[effectiveLang] || translations.en;
338
+ return langTranslations[key] || translations.en[key] || key;
339
+ }
340
+
622
341
  private warn(...args: any[]) {
623
342
  console.warn(...args);
624
343
  }
@@ -648,11 +367,12 @@ export class HankoAuth extends LitElement {
648
367
  return path;
649
368
  }
650
369
 
370
+ // styles injected to ensure global availability
651
371
  private injectHotStyles() {
652
372
  const stylesheets = [
653
373
  {
654
374
  id: "hot-design-system",
655
- href: "https://cdn.jsdelivr.net/npm/hotosm-ui-design@latest/dist/hot.css",
375
+ href: "https://cdn.jsdelivr.net/npm/@hotosm/ui-design@latest/dist/hot.css",
656
376
  },
657
377
  {
658
378
  id: "google-font-archivo",
@@ -678,10 +398,16 @@ export class HankoAuth extends LitElement {
678
398
  return;
679
399
  }
680
400
 
401
+ // DEBUG: Add delay to see loading state longer (remove in production)
402
+ await new Promise((resolve) => setTimeout(resolve, 2000));
403
+
681
404
  try {
682
405
  await register(this.hankoUrl, {
683
406
  enablePasskeys: false,
684
407
  hidePasskeyButtonOnLogin: true,
408
+ translations: { en, es, fr, pt },
409
+ translationsLocation: null,
410
+ fallbackLanguage: "en",
685
411
  });
686
412
 
687
413
  // Create persistent Hanko instance and set up session event listeners
@@ -1072,7 +798,7 @@ export class HankoAuth extends LitElement {
1072
798
  }
1073
799
  }
1074
800
 
1075
- // Fetch profile display name from login backend
801
+ // Fetch profile display name and language from login backend
1076
802
  private async fetchProfileDisplayName() {
1077
803
  try {
1078
804
  const profileUrl = `${this.hankoUrl}/api/profile/me`;
@@ -1091,6 +817,12 @@ export class HankoAuth extends LitElement {
1091
817
  `${profile.first_name || ""} ${profile.last_name || ""}`.trim();
1092
818
  this.log("πŸ‘€ Display name set to:", this.profileDisplayName);
1093
819
  }
820
+
821
+ // Set language from user profile if available
822
+ if (profile.language) {
823
+ this.userProfileLanguage = profile.language;
824
+ this.log("🌐 Language set from profile:", this.userProfileLanguage);
825
+ }
1094
826
  }
1095
827
  } catch (error) {
1096
828
  this.log("⚠️ Could not fetch profile:", error);
@@ -1426,6 +1158,7 @@ export class HankoAuth extends LitElement {
1426
1158
  this.osmConnected = false;
1427
1159
  this.osmData = null;
1428
1160
  this.hasAppMapping = false;
1161
+ this.userProfileLanguage = null; // Clear user's language preference
1429
1162
 
1430
1163
  // Broadcast state changes to other instances
1431
1164
  if (this._isPrimary) {
@@ -1590,7 +1323,7 @@ export class HankoAuth extends LitElement {
1590
1323
  );
1591
1324
 
1592
1325
  if (this.loading) {
1593
- return html` <button disabled>Log in</button> `;
1326
+ return html`<span class="loading-placeholder"><span class="loading-placeholder-text">${this.t("logIn")}</span><span class="spinner-small"></span></span>`;
1594
1327
  }
1595
1328
 
1596
1329
  if (this.error) {
@@ -1631,7 +1364,9 @@ export class HankoAuth extends LitElement {
1631
1364
  ${this.osmRequired && this.osmLoading
1632
1365
  ? html`
1633
1366
  <div class="osm-section">
1634
- <div class="loading">Checking OSM connection...</div>
1367
+ <div class="loading">
1368
+ ${this.t("checkingOsmConnection")}
1369
+ </div>
1635
1370
  </div>
1636
1371
  `
1637
1372
  : this.osmRequired && this.osmConnected
@@ -1641,7 +1376,7 @@ export class HankoAuth extends LitElement {
1641
1376
  <div class="osm-badge">
1642
1377
  <span class="osm-badge-icon">πŸ—ΊοΈ</span>
1643
1378
  <div>
1644
- <div>Connected to OpenStreetMap</div>
1379
+ <div>${this.t("connectedToOpenStreetMap")}</div>
1645
1380
  ${this.osmData?.osm_username
1646
1381
  ? html`
1647
1382
  <div class="osm-username">
@@ -1663,20 +1398,22 @@ export class HankoAuth extends LitElement {
1663
1398
  <div class="osm-connecting">
1664
1399
  <div class="spinner"></div>
1665
1400
  <div class="connecting-text">
1666
- πŸ—ΊοΈ Connecting to OpenStreetMap...
1401
+ πŸ—ΊοΈ ${this.t("connectingToOpenStreetMap")}
1667
1402
  </div>
1668
1403
  </div>
1669
1404
  `
1670
1405
  : html`
1671
- <div class="osm-prompt-title">🌍 OSM Required</div>
1406
+ <div class="osm-prompt-title">
1407
+ 🌍 ${this.t("osmRequired")}
1408
+ </div>
1672
1409
  <div class="osm-prompt-text">
1673
- This endpoint requires OSM connection.
1410
+ ${this.t("osmRequiredText")}
1674
1411
  </div>
1675
1412
  <button
1676
1413
  @click=${this.handleOSMConnect}
1677
1414
  class="btn-primary"
1678
1415
  >
1679
- Connect OSM Account
1416
+ ${this.t("connectOsmAccount")}
1680
1417
  </button>
1681
1418
  `}
1682
1419
  </div>
@@ -1684,7 +1421,7 @@ export class HankoAuth extends LitElement {
1684
1421
  : ""}
1685
1422
 
1686
1423
  <button @click=${this.handleLogout} class="btn-secondary">
1687
- Log out
1424
+ ${this.t("logOut")}
1688
1425
  </button>
1689
1426
  </div>
1690
1427
  </div>
@@ -1695,7 +1432,7 @@ export class HankoAuth extends LitElement {
1695
1432
  <div class="dropdown">
1696
1433
  <button
1697
1434
  @click=${this.toggleDropdown}
1698
- aria-label="Open account menu"
1435
+ aria-label="${this.t("openAccountMenu")}"
1699
1436
  aria-expanded=${this.isOpen}
1700
1437
  aria-haspopup="true"
1701
1438
  class="dropdown-trigger"
@@ -1706,7 +1443,8 @@ export class HankoAuth extends LitElement {
1706
1443
  ? html`
1707
1444
  <span
1708
1445
  class="osm-status-badge connected"
1709
- title="Connected to OSM as @${this.osmData?.osm_username}"
1446
+ title="${this.t("connectedToOsmAs")} @${this.osmData
1447
+ ?.osm_username}"
1710
1448
  >βœ“</span
1711
1449
  >
1712
1450
  `
@@ -1714,7 +1452,7 @@ export class HankoAuth extends LitElement {
1714
1452
  ? html`
1715
1453
  <span
1716
1454
  class="osm-status-badge required"
1717
- title="OSM connection required"
1455
+ title="${this.t("osmConnectionRequired")}"
1718
1456
  >!</span
1719
1457
  >
1720
1458
  `
@@ -1728,14 +1466,15 @@ export class HankoAuth extends LitElement {
1728
1466
  </div>
1729
1467
  <button data-action="profile" @click=${this.handleDropdownSelect}>
1730
1468
  <img src="${accountIcon}" class="icon" alt="Account icon" />
1731
- My HOT Account
1469
+ ${this.t("myHotAccount")}
1732
1470
  </button>
1733
1471
  ${this.osmRequired
1734
1472
  ? this.osmConnected
1735
1473
  ? html`
1736
1474
  <button class="osm-connected" disabled>
1737
1475
  <img src="${checkIcon}" alt="Check icon" class="icon" />
1738
- Connected to OSM (@${this.osmData?.osm_username})
1476
+ ${this.t("connectedToOsm")}
1477
+ (@${this.osmData?.osm_username})
1739
1478
  </button>
1740
1479
  `
1741
1480
  : html`
@@ -1744,13 +1483,13 @@ export class HankoAuth extends LitElement {
1744
1483
  @click=${this.handleDropdownSelect}
1745
1484
  >
1746
1485
  <img src="${mapIcon}" alt="Check icon" class="icon" />
1747
- Connect to OSM
1486
+ ${this.t("connectToOsm")}
1748
1487
  </button>
1749
1488
  `
1750
1489
  : ""}
1751
1490
  <button data-action="logout" @click=${this.handleDropdownSelect}>
1752
1491
  <img src="${logoutIcon}" alt="Log out icon" class="icon" />
1753
- Log Out
1492
+ ${this.t("logOut")}
1754
1493
  </button>
1755
1494
  </div>
1756
1495
  </div>
@@ -1785,7 +1524,7 @@ export class HankoAuth extends LitElement {
1785
1524
  --headline2-font-weight: var(--hot-font-weight-semibold);
1786
1525
  "
1787
1526
  >
1788
- <hanko-auth></hanko-auth>
1527
+ <hanko-auth lang="${this.lang}"></hanko-auth>
1789
1528
  </div>
1790
1529
  `;
1791
1530
  } else {
@@ -1810,9 +1549,13 @@ export class HankoAuth extends LitElement {
1810
1549
  const loginBase = this.loginUrl || `${baseUrl}/app`;
1811
1550
  const loginUrl = `${loginBase}?return_to=${encodeURIComponent(
1812
1551
  returnTo,
1813
- )}${this.osmRequired ? "&osm_required=true" : ""}${autoConnectParam}`;
1552
+ )}${this.osmRequired ? "&osm_required=true" : ""}${autoConnectParam}&lang=${this.lang}`;
1814
1553
 
1815
- return html`<a class="login-link" href="${loginUrl}">Log in</a> `;
1554
+ return html`<a
1555
+ class="login-link ${this.buttonVariant} ${this.buttonColor}"
1556
+ href="${loginUrl}"
1557
+ >${this.t("logIn")}</a
1558
+ > `;
1816
1559
  }
1817
1560
  }
1818
1561
  }