@fleetbase/ember-core 0.3.15 → 0.3.16

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.
@@ -10,6 +10,25 @@ import { storageFor } from 'ember-local-storage';
10
10
  import { debug } from '@ember/debug';
11
11
  import lookupUserIp from '../utils/lookup-user-ip';
12
12
 
13
+ /**
14
+ * CurrentUserService
15
+ *
16
+ * Manages the authenticated user's identity and preferences. Extends Evented
17
+ * so that any service or component can subscribe to user lifecycle events
18
+ * directly on this service.
19
+ *
20
+ * Session lifecycle events emitted (on both this service and via EventsService
21
+ * which re-broadcasts them on the universe bus for cross-engine listeners):
22
+ *
23
+ * user.loaded — fired after a successful login or session restore.
24
+ * Payload: (user, organization, properties)
25
+ *
26
+ * user.updated — fired when the user record is refreshed in-session
27
+ * (e.g. profile edit). Payload: (user, properties)
28
+ *
29
+ * user.organization_switched — fired when the user switches active org.
30
+ * Payload: (organization, properties)
31
+ */
13
32
  export default class CurrentUserService extends Service.extend(Evented) {
14
33
  @service session;
15
34
  @service store;
@@ -18,6 +37,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
18
37
  @service notifications;
19
38
  @service intl;
20
39
  @service events;
40
+ @service universe;
21
41
 
22
42
  @tracked user = { id: 'anon' };
23
43
  @tracked userSnapshot = { id: 'anon' };
@@ -302,14 +322,47 @@ export default class CurrentUserService extends Service.extend(Evented) {
302
322
  return defaultValue;
303
323
  }
304
324
 
325
+ /**
326
+ * Sets the current user and fires all user.loaded lifecycle events.
327
+ *
328
+ * This is the canonical place where the authenticated user identity is
329
+ * established. It fires:
330
+ *
331
+ * 1. `this.trigger('user.loaded', user)` — on the currentUser service
332
+ * itself (Evented), for direct service-level listeners.
333
+ *
334
+ * 2. `this.events.trackUserLoaded(user, organization)` — on the events
335
+ * service, which re-broadcasts on both the events bus and the universe
336
+ * bus so cross-engine listeners (Intercom, PostHog, Attio, etc.) can
337
+ * subscribe via `universe.on('user.loaded', handler)`.
338
+ *
339
+ * @param {Model} user
340
+ */
305
341
  async setUser(user) {
306
342
  const snapshot = await this.getUserSnapshot(user);
307
343
 
308
344
  // Set current user
309
345
  this.set('user', user);
310
346
  this.set('userSnapshot', snapshot);
347
+
348
+ // Resolve the organization for event payload
349
+ const organization = this.store.peekRecord('company', user.get('company_uuid'));
350
+
351
+ // 1. Trigger on the currentUser Evented bus (backward-compatible)
311
352
  this.trigger('user.loaded', user);
312
353
 
354
+ // 2. Fire through the events service — broadcasts on both events bus
355
+ // and universe bus for cross-engine listeners
356
+ if (this.events) {
357
+ this.events.trackUserLoaded(user, organization);
358
+ }
359
+
360
+ // 3. Trigger directly on universe for framework-level uniformity —
361
+ // guarantees delivery to all engines on the shared bus
362
+ if (this.universe) {
363
+ this.universe.trigger('user.loaded', user, organization);
364
+ }
365
+
313
366
  // Set permissions
314
367
  this.permissions = this.getUserPermissions(user);
315
368
 
@@ -323,4 +376,55 @@ export default class CurrentUserService extends Service.extend(Evented) {
323
376
  await this.loadLocale();
324
377
  }
325
378
  }
379
+
380
+ /**
381
+ * Fires a user.updated event when the user record is refreshed in-session.
382
+ * Call this after any in-session profile update to keep integrations in sync.
383
+ *
384
+ * @param {Model} user
385
+ */
386
+ async refreshUser(user) {
387
+ const snapshot = await this.getUserSnapshot(user);
388
+ this.set('user', user);
389
+ this.set('userSnapshot', snapshot);
390
+
391
+ const organization = this.store.peekRecord('company', user.get('company_uuid'));
392
+
393
+ this.trigger('user.updated', user);
394
+
395
+ if (this.events) {
396
+ this.events.trackEvent('user.updated', {
397
+ user_id: user?.id,
398
+ organization_id: organization?.id,
399
+ organization_name: organization?.name,
400
+ });
401
+ }
402
+
403
+ if (this.universe) {
404
+ this.universe.trigger('user.updated', user, organization);
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Fires a user.organization_switched event when the user changes their
410
+ * active organization. Call this after a successful org switch.
411
+ *
412
+ * @param {Model} organization
413
+ */
414
+ switchOrganization(organization) {
415
+ this.company = organization;
416
+
417
+ this.trigger('user.organization_switched', organization);
418
+
419
+ if (this.events) {
420
+ this.events.trackEvent('user.organization_switched', {
421
+ organization_id: organization?.id,
422
+ organization_name: organization?.name,
423
+ });
424
+ }
425
+
426
+ if (this.universe) {
427
+ this.universe.trigger('user.organization_switched', organization);
428
+ }
429
+ }
326
430
  }
@@ -3,11 +3,66 @@ import Evented from '@ember/object/evented';
3
3
  import config from 'ember-get-config';
4
4
 
5
5
  /**
6
- * Events Service
6
+ * EventsService
7
7
  *
8
- * Provides a centralized event tracking system for Fleetbase.
9
- * This service emits standardized events on both its own event bus and the universe service,
10
- * allowing components, services, and engines to subscribe and react to application events.
8
+ * Provides a centralized, standardized event tracking system for Fleetbase.
9
+ *
10
+ * This service is the single source of truth for all application lifecycle
11
+ * events. It emits every event on two buses simultaneously:
12
+ *
13
+ * 1. Its own Evented bus — for direct service/component listeners:
14
+ * this.events.on('user.loaded', handler)
15
+ *
16
+ * 2. The universe service bus — for cross-engine listeners (recommended
17
+ * for use in Ember Engines / extensions):
18
+ * this.universe.on('user.loaded', handler)
19
+ *
20
+ * ─────────────────────────────────────────────────────────────────────────────
21
+ * Session Lifecycle Events
22
+ * ─────────────────────────────────────────────────────────────────────────────
23
+ *
24
+ * session.authenticated Fired after a successful login or session restore.
25
+ * Payload: (properties)
26
+ *
27
+ * session.invalidated Fired after the session is destroyed (logout).
28
+ * Payload: (duration_seconds, properties)
29
+ *
30
+ * session.terminated Alias for session.invalidated — provided for
31
+ * backward compatibility and semantic clarity.
32
+ * Payload: (duration_seconds, properties)
33
+ *
34
+ * user.loaded Fired after the authenticated user record and
35
+ * organization have been fully loaded into the
36
+ * currentUser service. This is the canonical event
37
+ * for integrations to boot (Intercom, PostHog, etc.)
38
+ * Payload: (user, organization, properties)
39
+ *
40
+ * user.updated Fired when the user record is refreshed in-session
41
+ * (e.g. after a profile edit).
42
+ * Payload: (user, properties)
43
+ *
44
+ * user.deauthenticated Fired when the user identity is cleared on logout.
45
+ * Semantic alias for session.invalidated — use this
46
+ * to shut down integrations cleanly (Intercom, etc.)
47
+ * Payload: (duration_seconds, properties)
48
+ *
49
+ * user.organization_switched Fired when the user switches their active org.
50
+ * Payload: (organization, properties)
51
+ *
52
+ * ─────────────────────────────────────────────────────────────────────────────
53
+ * Resource Events
54
+ * ─────────────────────────────────────────────────────────────────────────────
55
+ *
56
+ * resource.created Generic resource creation.
57
+ * resource.updated Generic resource update.
58
+ * resource.deleted Generic resource deletion.
59
+ * resource.imported Bulk import.
60
+ * resource.exported Export.
61
+ * resource.bulk_action Bulk action (delete, archive, etc.)
62
+ * {modelName}.created Model-specific creation (e.g. order.created)
63
+ * {modelName}.updated Model-specific update.
64
+ * {modelName}.deleted Model-specific deletion.
65
+ * {modelName}.exported Model-specific export.
11
66
  *
12
67
  * @class EventsService
13
68
  * @extends Service
@@ -16,6 +71,108 @@ export default class EventsService extends Service.extend(Evented) {
16
71
  @service universe;
17
72
  @service currentUser;
18
73
 
74
+ // =========================================================================
75
+ // Session Lifecycle Tracking
76
+ // =========================================================================
77
+
78
+ /**
79
+ * Tracks a successful authentication (login or session restore).
80
+ *
81
+ * Called by SessionService.handleAuthentication().
82
+ *
83
+ * @param {Object} [props={}] - Additional properties to include
84
+ */
85
+ trackSessionAuthenticated(props = {}) {
86
+ const properties = this.#enrichProperties(props);
87
+ this.#trigger('session.authenticated', properties);
88
+ }
89
+
90
+ /**
91
+ * Tracks when a user session is terminated (logout).
92
+ *
93
+ * Called by SessionService.handleInvalidation(). Also fires the semantic
94
+ * `user.deauthenticated` event so integrations can react to the user
95
+ * identity being cleared without needing to know about session internals.
96
+ *
97
+ * @param {Number|null} duration - Session duration in seconds (null if unknown)
98
+ * @param {Object} [props={}] - Additional properties to include
99
+ */
100
+ trackSessionTerminated(duration, props = {}) {
101
+ const properties = this.#enrichProperties({
102
+ session_duration: duration,
103
+ ...props,
104
+ });
105
+
106
+ // Fire session.invalidated (technical event)
107
+ this.#trigger('session.invalidated', duration, properties);
108
+
109
+ // Fire session.terminated (backward-compatible alias)
110
+ this.#trigger('session.terminated', duration, properties);
111
+
112
+ // Fire user.deauthenticated (semantic event for integrations)
113
+ this.#trigger('user.deauthenticated', duration, properties);
114
+ }
115
+
116
+ /**
117
+ * Tracks when the current user is loaded (session initialized).
118
+ *
119
+ * Called by CurrentUserService.setUser() after a successful login or
120
+ * session restore. This is the canonical event for integrations to boot.
121
+ *
122
+ * @param {Object} user - The authenticated user model
123
+ * @param {Object} organization - The user's active organization model
124
+ * @param {Object} [props={}] - Additional properties to include
125
+ */
126
+ trackUserLoaded(user, organization, props = {}) {
127
+ const properties = this.#enrichProperties({
128
+ user_id: user?.id,
129
+ organization_id: organization?.id,
130
+ organization_name: organization?.name,
131
+ ...props,
132
+ });
133
+
134
+ this.#trigger('user.loaded', user, organization, properties);
135
+ }
136
+
137
+ /**
138
+ * Tracks when the user record is refreshed in-session (e.g. profile edit).
139
+ *
140
+ * Called by CurrentUserService.refreshUser().
141
+ *
142
+ * @param {Object} user - The updated user model
143
+ * @param {Object} [props={}] - Additional properties to include
144
+ */
145
+ trackUserUpdated(user, props = {}) {
146
+ const properties = this.#enrichProperties({
147
+ user_id: user?.id,
148
+ ...props,
149
+ });
150
+
151
+ this.#trigger('user.updated', user, properties);
152
+ }
153
+
154
+ /**
155
+ * Tracks when the user switches their active organization.
156
+ *
157
+ * Called by CurrentUserService.switchOrganization().
158
+ *
159
+ * @param {Object} organization - The new active organization model
160
+ * @param {Object} [props={}] - Additional properties to include
161
+ */
162
+ trackOrganizationSwitched(organization, props = {}) {
163
+ const properties = this.#enrichProperties({
164
+ organization_id: organization?.id,
165
+ organization_name: organization?.name,
166
+ ...props,
167
+ });
168
+
169
+ this.#trigger('user.organization_switched', organization, properties);
170
+ }
171
+
172
+ // =========================================================================
173
+ // Resource Event Tracking
174
+ // =========================================================================
175
+
19
176
  /**
20
177
  * Tracks the creation of a resource
21
178
  *
@@ -131,38 +288,9 @@ export default class EventsService extends Service.extend(Evented) {
131
288
  this.#trigger('resource.bulk_action', verb, resources, firstResource, properties);
132
289
  }
133
290
 
134
- /**
135
- * Tracks when the current user is loaded (session initialized)
136
- *
137
- * @param {Object} user - The user object
138
- * @param {Object} organization - The organization object
139
- * @param {Object} [props={}] - Additional properties to include
140
- */
141
- trackUserLoaded(user, organization, props = {}) {
142
- const properties = this.#enrichProperties({
143
- user_id: user?.id,
144
- organization_id: organization?.id,
145
- organization_name: organization?.name,
146
- ...props,
147
- });
148
-
149
- this.#trigger('user.loaded', user, organization, properties);
150
- }
151
-
152
- /**
153
- * Tracks when a user session is terminated
154
- *
155
- * @param {Number} duration - Session duration in seconds
156
- * @param {Object} [props={}] - Additional properties to include
157
- */
158
- trackSessionTerminated(duration, props = {}) {
159
- const properties = this.#enrichProperties({
160
- session_duration: duration,
161
- ...props,
162
- });
163
-
164
- this.#trigger('session.terminated', duration, properties);
165
- }
291
+ // =========================================================================
292
+ // Generic Event Tracking
293
+ // =========================================================================
166
294
 
167
295
  /**
168
296
  * Tracks a generic custom event
@@ -190,11 +318,11 @@ export default class EventsService extends Service.extend(Evented) {
190
318
  // =========================================================================
191
319
 
192
320
  /**
193
- * Triggers an event on both the events service and universe service
321
+ * Triggers an event on both the events service and universe service.
194
322
  *
195
323
  * This dual event system allows listeners to subscribe to events on either:
196
- * - this.events.on('event.name', handler) - Local listeners
197
- * - this.universe.on('event.name', handler) - Cross-engine listeners
324
+ * - this.events.on('event.name', handler) local listeners
325
+ * - this.universe.on('event.name', handler) cross-engine listeners
198
326
  *
199
327
  * @private
200
328
  * @param {String} eventName - The event name
@@ -5,11 +5,42 @@ import { later } from '@ember/runloop';
5
5
  import { debug } from '@ember/debug';
6
6
  import getWithDefault from '../utils/get-with-default';
7
7
 
8
+ /**
9
+ * SessionService
10
+ *
11
+ * Extends ember-simple-auth's session service to add:
12
+ *
13
+ * - Proper lifecycle event firing through the EventsService (which
14
+ * re-broadcasts on both the events bus and the universe bus).
15
+ * - Session start timestamp tracking for duration calculation.
16
+ *
17
+ * Session lifecycle events fired (via EventsService → universe bus):
18
+ *
19
+ * session.authenticated — fired after a successful login/session restore.
20
+ * Payload: (properties)
21
+ *
22
+ * session.invalidated — fired after the session is destroyed (logout).
23
+ * Payload: (duration_seconds, properties)
24
+ *
25
+ * user.deauthenticated — alias for session.invalidated, provided as a
26
+ * semantic convenience for integrations that want
27
+ * to react to the user identity being cleared
28
+ * (e.g. Intercom shutdown, PostHog reset).
29
+ * Payload: (duration_seconds, properties)
30
+ *
31
+ * All events are fired on both the EventsService Evented bus and the universe
32
+ * service, so listeners can use either:
33
+ *
34
+ * this.events.on('session.authenticated', handler)
35
+ * this.universe.on('session.authenticated', handler) ← recommended for engines
36
+ */
8
37
  export default class SessionService extends SimpleAuthSessionService {
9
38
  @service router;
10
39
  @service currentUser;
11
40
  @service fetch;
12
41
  @service notifications;
42
+ @service events;
43
+ @service universe;
13
44
 
14
45
  /**
15
46
  * Set where to transition to
@@ -25,6 +56,14 @@ export default class SessionService extends SimpleAuthSessionService {
25
56
  */
26
57
  @tracked _isOnboarding = false;
27
58
 
59
+ /**
60
+ * Timestamp (ms) when the session was authenticated.
61
+ * Used to calculate session duration on invalidation.
62
+ *
63
+ * @var {Number|null}
64
+ */
65
+ @tracked _sessionStartedAt = null;
66
+
28
67
  /**
29
68
  * Set this as onboarding.
30
69
  *
@@ -44,11 +83,20 @@ export default class SessionService extends SimpleAuthSessionService {
44
83
  }
45
84
 
46
85
  /**
47
- * Overwrite the handle authentication method
86
+ * Overwrite the handle authentication method.
87
+ *
88
+ * Fires `session.authenticated` through the events service so that
89
+ * integrations (Intercom, PostHog, etc.) can react to a successful login.
48
90
  *
49
91
  * @void
50
92
  */
51
93
  async handleAuthentication() {
94
+ // Record session start time for duration tracking on logout
95
+ this._sessionStartedAt = Date.now();
96
+
97
+ // Fire session.authenticated event
98
+ this._fireSessionEvent('session.authenticated');
99
+
52
100
  if (this._isOnboarding) {
53
101
  return;
54
102
  }
@@ -76,6 +124,37 @@ export default class SessionService extends SimpleAuthSessionService {
76
124
  removeLoaderNode();
77
125
  }
78
126
 
127
+ /**
128
+ * Extends the parent handleInvalidation method.
129
+ *
130
+ * IMPORTANT: super.handleInvalidation(routeAfterInvalidation) is called
131
+ * first to preserve the ember-simple-auth behaviour of redirecting the
132
+ * user to the login page (via handleSessionInvalidated). Our event
133
+ * firing happens after so it cannot interfere with the redirect.
134
+ *
135
+ * @param {String} routeAfterInvalidation - Passed through from ember-simple-auth
136
+ * @void
137
+ */
138
+ handleInvalidation(routeAfterInvalidation) {
139
+ // 1. Always call super first — this performs the actual post-logout
140
+ // redirect/reload that ember-simple-auth is responsible for.
141
+ super.handleInvalidation(routeAfterInvalidation);
142
+
143
+ const durationSeconds = this._sessionStartedAt ? Math.round((Date.now() - this._sessionStartedAt) / 1000) : null;
144
+
145
+ // 2. Fire session lifecycle events for integrations (Intercom, PostHog, etc.)
146
+ if (this.events) {
147
+ this.events.trackSessionTerminated(durationSeconds);
148
+ }
149
+
150
+ // 3. Fire user.deauthenticated directly on universe for framework-level
151
+ // uniformity — engines can listen without needing the events service.
152
+ this._fireSessionEvent('user.deauthenticated', { session_duration: durationSeconds });
153
+
154
+ // Reset session start time
155
+ this._sessionStartedAt = null;
156
+ }
157
+
79
158
  /**
80
159
  * Loads the current authenticated user
81
160
  *
@@ -217,4 +296,39 @@ export default class SessionService extends SimpleAuthSessionService {
217
296
  throw new Error(error.message);
218
297
  });
219
298
  }
299
+
300
+ // =========================================================================
301
+ // Private helpers
302
+ // =========================================================================
303
+
304
+ /**
305
+ * Fires a named session lifecycle event on both the events service and
306
+ * directly on the universe service.
307
+ *
308
+ * Firing on universe directly (in addition to via events service) ensures
309
+ * that all engines and extensions receive the event on the shared framework-
310
+ * level bus regardless of whether the events service has fully initialised.
311
+ *
312
+ * Listeners can subscribe via:
313
+ * this.universe.on('session.authenticated', handler)
314
+ * this.universe.on('user.deauthenticated', handler)
315
+ * this.events.on('session.authenticated', handler)
316
+ *
317
+ * @private
318
+ * @param {String} eventName
319
+ * @param {Object} [extraProps={}]
320
+ */
321
+ _fireSessionEvent(eventName, extraProps = {}) {
322
+ // 1. Fire through the events service (dual-broadcasts on events + universe bus)
323
+ if (this.events) {
324
+ this.events.trackEvent(eventName, extraProps);
325
+ }
326
+
327
+ // 2. Also trigger directly on universe for framework-level uniformity —
328
+ // ensures the event reaches all engines even if events service is
329
+ // not yet available or not injected in a given engine context.
330
+ if (this.universe) {
331
+ this.universe.trigger(eventName, extraProps);
332
+ }
333
+ }
220
334
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/ember-core",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "description": "Provides all the core services, decorators and utilities for building a Fleetbase extension for the Console.",
5
5
  "keywords": [
6
6
  "fleetbase-core",