ad2app-lib 1.29.0 → 1.30.0

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,8 +10,39 @@ export declare class I_User {
10
10
  has_connected_platforms?: boolean;
11
11
  influencer?: I_Influencer;
12
12
  }
13
+ /**
14
+ * The answer an account has recorded about the onboarding tour.
15
+ *
16
+ * Three values, and the difference between the last two is the whole point:
17
+ * 'skipped' | 'completed' — a recorded answer. Never re-teach this user.
18
+ * null — no record yet. A genuinely new user; show it.
19
+ * ABSENT (the field is missing entirely) — the server could not read the
20
+ * column. Decide NOTHING.
21
+ *
22
+ * Absent is deliberately not `null`. Collapsing them shows the tour to every
23
+ * existing user in the window between a deploy and its manual migration
24
+ * (migrations here are run by hand, never on deploy).
25
+ */
26
+ export type OnboardingState = 'skipped' | 'completed';
13
27
  export declare class I_SignedUser extends I_User {
14
28
  accessToken: string;
29
+ /**
30
+ * AD2-1331 AC3 (Maciej's review, 2026-08-25). Every response that hands back
31
+ * a session carries this, not just `authenticate` — the client replaces its
32
+ * authed slice wholesale with whichever payload arrives, so a path that omits
33
+ * it leaves the answer unknown for the rest of the session (AD2-1329 AC4).
34
+ *
35
+ * It lives HERE because AC3 chose an echoing PATCH over a failing one
36
+ * specifically to remove the forward-only rule from the client, and that only
37
+ * works if both sides agree on the shape. Until this shipped, the field was
38
+ * written by the backend, read by the frontend, and declared by neither: the
39
+ * frontend had to cast through `unknown` to see it, and a rename on either
40
+ * side would have type-checked clean while the onboarding funnel silently
41
+ * went dark.
42
+ *
43
+ * Optional on purpose — see OnboardingState: absent means "could not read".
44
+ */
45
+ onboardingState?: OnboardingState | null;
15
46
  }
16
47
  export declare enum UserRoles {
17
48
  ADMIN = "ADMIN",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ad2app-lib",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "commonjs",
@@ -47,7 +47,7 @@
47
47
  "prepare": "npm run build"
48
48
  },
49
49
  "keywords": [],
50
- "author": "Maciej Górski@ad2.app",
50
+ "author": "Maciej G\u00f3rski@ad2.app",
51
51
  "license": "ISC",
52
52
  "description": "Package to share types and utils across the ad2app projects",
53
53
  "dependencies": {
@@ -13,8 +13,41 @@ export class I_User {
13
13
  influencer?: I_Influencer;
14
14
  }
15
15
 
16
+ /**
17
+ * The answer an account has recorded about the onboarding tour.
18
+ *
19
+ * Three values, and the difference between the last two is the whole point:
20
+ * 'skipped' | 'completed' — a recorded answer. Never re-teach this user.
21
+ * null — no record yet. A genuinely new user; show it.
22
+ * ABSENT (the field is missing entirely) — the server could not read the
23
+ * column. Decide NOTHING.
24
+ *
25
+ * Absent is deliberately not `null`. Collapsing them shows the tour to every
26
+ * existing user in the window between a deploy and its manual migration
27
+ * (migrations here are run by hand, never on deploy).
28
+ */
29
+ export type OnboardingState = 'skipped' | 'completed';
30
+
16
31
  export class I_SignedUser extends I_User {
17
32
  accessToken: string;
33
+
34
+ /**
35
+ * AD2-1331 AC3 (Maciej's review, 2026-08-25). Every response that hands back
36
+ * a session carries this, not just `authenticate` — the client replaces its
37
+ * authed slice wholesale with whichever payload arrives, so a path that omits
38
+ * it leaves the answer unknown for the rest of the session (AD2-1329 AC4).
39
+ *
40
+ * It lives HERE because AC3 chose an echoing PATCH over a failing one
41
+ * specifically to remove the forward-only rule from the client, and that only
42
+ * works if both sides agree on the shape. Until this shipped, the field was
43
+ * written by the backend, read by the frontend, and declared by neither: the
44
+ * frontend had to cast through `unknown` to see it, and a rename on either
45
+ * side would have type-checked clean while the onboarding funnel silently
46
+ * went dark.
47
+ *
48
+ * Optional on purpose — see OnboardingState: absent means "could not read".
49
+ */
50
+ onboardingState?: OnboardingState | null;
18
51
  }
19
52
 
20
53
  export enum UserRoles {
@@ -0,0 +1,66 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { I_SignedUser, OnboardingState, UserRoles } from './I_User';
5
+
6
+ /**
7
+ * AD2-1331 AC3 — the shared shape, pinned.
8
+ *
9
+ * These are mostly COMPILE-time assertions: the value of this file is that
10
+ * `tsc` fails if `onboardingState` is renamed, narrowed, or made required on
11
+ * one side of the wire. Before it existed the field was written by the backend,
12
+ * read by the frontend, and declared by neither, so either side could rename it
13
+ * and every type-check would still pass while the onboarding funnel went dark.
14
+ *
15
+ * The runtime assertions below exist so the file is also a real test rather
16
+ * than a silent type-only artefact that a test runner skips over.
17
+ */
18
+
19
+ test('a signed user may carry a recorded answer', () => {
20
+ const user: I_SignedUser = {
21
+ id: 'u-1',
22
+ role: UserRoles.INFLUENCER,
23
+ accessToken: 'tok',
24
+ onboardingState: 'completed',
25
+ };
26
+
27
+ assert.equal(user.onboardingState, 'completed');
28
+ });
29
+
30
+ test('null is a value, and means "no record yet" — a new user', () => {
31
+ const user: I_SignedUser = {
32
+ id: 'u-1',
33
+ role: UserRoles.INFLUENCER,
34
+ accessToken: 'tok',
35
+ onboardingState: null,
36
+ };
37
+
38
+ // null must survive as null: the client tells it apart from absent, and
39
+ // only null means "show the tour".
40
+ assert.equal(user.onboardingState, null);
41
+ assert.ok('onboardingState' in user);
42
+ });
43
+
44
+ test('ABSENT is distinct from null — the field may be omitted entirely', () => {
45
+ // The migration-pending case. If this ever stops compiling because the
46
+ // field was made required, the deploy window that follows would show the
47
+ // onboarding tour to the entire existing user base.
48
+ const user: I_SignedUser = {
49
+ id: 'u-1',
50
+ role: UserRoles.INFLUENCER,
51
+ accessToken: 'tok',
52
+ };
53
+
54
+ assert.equal('onboardingState' in user, false);
55
+ assert.equal(user.onboardingState, undefined);
56
+ });
57
+
58
+ test('the state union is exactly the two recorded answers', () => {
59
+ const every: OnboardingState[] = ['skipped', 'completed'];
60
+
61
+ assert.deepEqual(every, ['skipped', 'completed']);
62
+ // @ts-expect-error 'pending' is not a recorded answer; null/absent cover
63
+ // "no answer", and a third string would be a state nobody handles.
64
+ const invalid: OnboardingState = 'pending';
65
+ assert.equal(invalid, 'pending');
66
+ });