founder-schema 0.15.0 → 0.17.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.
- package/README.md +197 -5
- package/dist/core/common.d.ts +103 -20
- package/dist/core/common.d.ts.map +1 -1
- package/dist/core/common.js +104 -16
- package/dist/core/common.js.map +1 -1
- package/dist/core/limits.d.ts +7 -0
- package/dist/core/limits.d.ts.map +1 -1
- package/dist/core/limits.js +7 -0
- package/dist/core/limits.js.map +1 -1
- package/dist/core/validation.d.ts +26 -0
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +26 -0
- package/dist/core/validation.js.map +1 -1
- package/dist/domains/communities/communities.d.ts +9 -9
- package/dist/domains/discover/discover.d.ts +7 -0
- package/dist/domains/discover/discover.d.ts.map +1 -1
- package/dist/domains/feed/feed.d.ts +11 -11
- package/dist/domains/location/india.d.ts +205 -0
- package/dist/domains/location/india.d.ts.map +1 -0
- package/dist/domains/location/india.js +1175 -0
- package/dist/domains/location/india.js.map +1 -0
- package/dist/domains/onboarding/onboarding-answers.d.ts +81 -0
- package/dist/domains/onboarding/onboarding-answers.d.ts.map +1 -0
- package/dist/domains/onboarding/onboarding-answers.js +141 -0
- package/dist/domains/onboarding/onboarding-answers.js.map +1 -0
- package/dist/domains/onboarding/onboarding.d.ts +422 -34
- package/dist/domains/onboarding/onboarding.d.ts.map +1 -1
- package/dist/domains/onboarding/onboarding.js +314 -58
- package/dist/domains/onboarding/onboarding.js.map +1 -1
- package/dist/domains/products/products.d.ts +3 -3
- package/dist/domains/profile/founder-profile.d.ts +122 -6
- package/dist/domains/profile/founder-profile.d.ts.map +1 -1
- package/dist/domains/profile/founder-profile.js +29 -9
- package/dist/domains/profile/founder-profile.js.map +1 -1
- package/dist/domains/profile/my-profile.d.ts +386 -0
- package/dist/domains/profile/my-profile.d.ts.map +1 -0
- package/dist/domains/profile/my-profile.js +52 -0
- package/dist/domains/profile/my-profile.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"onboarding.d.ts","sourceRoot":"","sources":["../../../src/domains/onboarding/onboarding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"onboarding.d.ts","sourceRoot":"","sources":["../../../src/domains/onboarding/onboarding.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* for the end. Letting those two drift is how a wizard says a step is optional
|
|
17
17
|
* and then answers 422 at Finish.
|
|
18
18
|
*
|
|
19
|
-
* NO COPY IN HERE.
|
|
20
|
-
* (`
|
|
19
|
+
* NO COPY IN HERE. A step's label and title are translation KEYS
|
|
20
|
+
* (`labelKey`, `titleKey`), Jansathi's rule for every config string: an English
|
|
21
21
|
* sentence in a shared package is a sentence every other language has to match
|
|
22
22
|
* by wording. A topic category is named by its `key`, which the client words
|
|
23
23
|
* (0.6.0 dropped its `labelKey`: the web, English-only since 2026-10-06, read
|
|
@@ -26,9 +26,11 @@
|
|
|
26
26
|
* @module founder-schema/domains/onboarding
|
|
27
27
|
*/
|
|
28
28
|
import { z } from 'zod';
|
|
29
|
-
import { displayNameSchema, GENDER_VALUES, GOAL_VALUES, genderSchema, goalSchema, INDUSTRY_VALUES, industrySchema,
|
|
29
|
+
import { displayNameSchema, GENDER_VALUES, GOAL_VALUES, genderSchema, goalListSchema, goalSchema, INDUSTRY_VALUES, industrySchema, objectIdSchema, optionalTrimmedString, phoneSchema, SKILL_VALUES, STARTUP_STAGE_VALUES, skillListSchema, skillSchema, startupStageSchema, } from '../../core/common.js';
|
|
30
30
|
import { ABOUT_MAX, CITY_MAX, COUNTRY_MAX, DISPLAY_NAME_MAX, GOALS_MIN, HEADLINE_MAX, LINK_URL_MAX, SKILLS_MAX, STATE_MAX, TEAM_SIZE_MAX, VENTURE_NAME_MAX, } from '../../core/limits.js';
|
|
31
31
|
import { VALIDATION_MESSAGE } from '../../core/validation.js';
|
|
32
|
+
import { indiaLocationSchema } from '../location/india.js';
|
|
33
|
+
import { mediaImageSchema } from '../media/media.js';
|
|
32
34
|
import { founderLinksSchema, ventureSchema } from '../profile/founder-profile.js';
|
|
33
35
|
// ─── Age ─────────────────────────────────────────────────────────────────────
|
|
34
36
|
/**
|
|
@@ -87,6 +89,58 @@ function isRealDate(iso) {
|
|
|
87
89
|
const asDate = new Date(Date.UTC(y, m - 1, d));
|
|
88
90
|
return (asDate.getUTCFullYear() === y && asDate.getUTCMonth() === m - 1 && asDate.getUTCDate() === d);
|
|
89
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* True for a `YYYY-MM-DD` that is a real calendar date (0.17.0, exported so a
|
|
94
|
+
* client's date picker checks a date exactly as `dateOfBirthSchema` does):
|
|
95
|
+
* not 2026-02-30, not 2026-13-01.
|
|
96
|
+
*/
|
|
97
|
+
export function isRealIsoDate(value) {
|
|
98
|
+
return isoDateSchema.safeParse(value).success && isRealDate(value.trim());
|
|
99
|
+
}
|
|
100
|
+
/** `YYYY-MM-DD` for a moment, counted in India's calendar. */
|
|
101
|
+
const isoInIndia = (moment) => new Date(moment + INDIA_OFFSET_MS).toISOString().slice(0, 10);
|
|
102
|
+
/**
|
|
103
|
+
* Today in India, `YYYY-MM-DD` (0.17.0): the day every age here is counted
|
|
104
|
+
* from (`ageInYears`), so a client's "today" must be this one, never the
|
|
105
|
+
* device's own: east of India the device is already on tomorrow.
|
|
106
|
+
*/
|
|
107
|
+
export function todayInIndia(now = new Date()) {
|
|
108
|
+
return isoInIndia(now.getTime());
|
|
109
|
+
}
|
|
110
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
111
|
+
const isoOfUtc = (ms) => new Date(ms).toISOString().slice(0, 10);
|
|
112
|
+
/**
|
|
113
|
+
* The dates of birth `dateOfBirthSchema` accepts TODAY, both ends included
|
|
114
|
+
* (0.17.0): `latest` makes someone exactly `MIN_AGE_YEARS` today, `earliest`
|
|
115
|
+
* exactly `MAX_AGE_YEARS`. Worked out with `ageInYears` itself, so a picker
|
|
116
|
+
* that offers only this window can never offer a date the schema refuses, a
|
|
117
|
+
* 29 February included.
|
|
118
|
+
*/
|
|
119
|
+
export function dateOfBirthBounds(now = new Date()) {
|
|
120
|
+
const [y, m, d] = todayInIndia(now).split('-').map(Number);
|
|
121
|
+
let latest = Date.UTC(y - MIN_AGE_YEARS, m - 1, d);
|
|
122
|
+
while (ageInYears(isoOfUtc(latest), now) < MIN_AGE_YEARS)
|
|
123
|
+
latest -= DAY_MS;
|
|
124
|
+
let earliest = Date.UTC(y - MAX_AGE_YEARS - 1, m - 1, d);
|
|
125
|
+
while (ageInYears(isoOfUtc(earliest), now) > MAX_AGE_YEARS)
|
|
126
|
+
earliest += DAY_MS;
|
|
127
|
+
return { earliest: isoOfUtc(earliest), latest: isoOfUtc(latest) };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* True only for a COMPLETE, real date of birth that is under `MIN_AGE_YEARS`
|
|
131
|
+
* today (0.17.0): the rule the server applies to a draft, where a date still
|
|
132
|
+
* being typed must pass ("19" says nothing about anyone's age yet). Until
|
|
133
|
+
* 0.17.0 the backend wrote this out by hand and skipped the real-date check.
|
|
134
|
+
*/
|
|
135
|
+
export function isUnderAgeDateOfBirth(value, now = new Date()) {
|
|
136
|
+
if (typeof value !== 'string' || !isRealIsoDate(value))
|
|
137
|
+
return false;
|
|
138
|
+
return ageInYears(value.trim(), now) < MIN_AGE_YEARS;
|
|
139
|
+
}
|
|
140
|
+
/** A `YYYY-MM-DD` as the instant it is stored as: midnight UTC that day (0.17.0). */
|
|
141
|
+
export const isoDateToDate = (iso) => new Date(`${iso.trim()}T00:00:00.000Z`);
|
|
142
|
+
/** A stored date (midnight UTC) as `YYYY-MM-DD` (0.17.0): `isoDateToDate`'s inverse. */
|
|
143
|
+
export const dateToIsoDate = (date) => date.toISOString().slice(0, 10);
|
|
90
144
|
/**
|
|
91
145
|
* Date of birth, with the age gate applied per parse.
|
|
92
146
|
*
|
|
@@ -101,8 +155,61 @@ export const dateOfBirthSchema = isoDateSchema
|
|
|
101
155
|
.refine((v) => ageInYears(v) >= MIN_AGE_YEARS, { error: VALIDATION_MESSAGE.dobUnderAge })
|
|
102
156
|
.refine((v) => ageInYears(v) <= MAX_AGE_YEARS, { error: VALIDATION_MESSAGE.dobTooOld });
|
|
103
157
|
// ─── Business ────────────────────────────────────────────────────────────────
|
|
104
|
-
|
|
158
|
+
/**
|
|
159
|
+
* What KIND of business it is, which is not how it is registered
|
|
160
|
+
* (`LEGAL_STRUCTURE_VALUES`): a freelancer may be a sole proprietorship, a
|
|
161
|
+
* startup a private limited company, a social enterprise a Section 8 company.
|
|
162
|
+
*
|
|
163
|
+
* 0.16.0 (the user, 2026-10-18: "check what more business types we can use")
|
|
164
|
+
* added the kinds an Indian founder network meets beyond the three it had: the
|
|
165
|
+
* independent professional, the services firm, the social enterprise, the
|
|
166
|
+
* family business and the franchise. `sme` is India's MSME.
|
|
167
|
+
*
|
|
168
|
+
* ⚠ Stored on the profile's venture and a Mongoose enum. Append only.
|
|
169
|
+
*/
|
|
170
|
+
export const BUSINESS_TYPE_VALUES = [
|
|
171
|
+
'startup',
|
|
172
|
+
'sme',
|
|
173
|
+
'enterprise',
|
|
174
|
+
'freelancer',
|
|
175
|
+
'agency',
|
|
176
|
+
'social_enterprise',
|
|
177
|
+
'family_business',
|
|
178
|
+
'franchise',
|
|
179
|
+
];
|
|
105
180
|
export const businessTypeSchema = z.enum(BUSINESS_TYPE_VALUES);
|
|
181
|
+
/**
|
|
182
|
+
* How the business is registered in India, or that it is not yet (0.16.0, the
|
|
183
|
+
* user, 2026-10-18: "the categories that we have in India like private
|
|
184
|
+
* limited, LLP or OPC or proprietorship").
|
|
185
|
+
*
|
|
186
|
+
* The forms Indian law provides: a sole proprietorship and a partnership firm
|
|
187
|
+
* (Indian Partnership Act, 1932), a limited liability partnership (LLP Act,
|
|
188
|
+
* 2008), a one person, private limited, public limited or Section 8 (not for
|
|
189
|
+
* profit) company (Companies Act, 2013), a trust or society, and a
|
|
190
|
+
* co-operative society. Two more are answers a founder often has: not
|
|
191
|
+
* registered yet (an idea, or a side project), and registered outside India
|
|
192
|
+
* (an Indian team whose company is incorporated abroad, often in the US).
|
|
193
|
+
*
|
|
194
|
+
* Optional, like the business type, and private to the member: nothing here
|
|
195
|
+
* verifies it (documents are a settings matter, by the user's ruling).
|
|
196
|
+
*
|
|
197
|
+
* ⚠ Stored on the profile's venture and a Mongoose enum. Append only.
|
|
198
|
+
*/
|
|
199
|
+
export const LEGAL_STRUCTURE_VALUES = [
|
|
200
|
+
'not_registered',
|
|
201
|
+
'sole_proprietorship',
|
|
202
|
+
'partnership',
|
|
203
|
+
'llp',
|
|
204
|
+
'opc',
|
|
205
|
+
'private_limited',
|
|
206
|
+
'public_limited',
|
|
207
|
+
'section_8',
|
|
208
|
+
'trust_or_society',
|
|
209
|
+
'cooperative',
|
|
210
|
+
'registered_abroad',
|
|
211
|
+
];
|
|
212
|
+
export const legalStructureSchema = z.enum(LEGAL_STRUCTURE_VALUES);
|
|
106
213
|
// ─── Topics: the shared vocabulary ───────────────────────────────────────────────────────────────
|
|
107
214
|
/**
|
|
108
215
|
* ONE vocabulary, used TWICE: a member picks these as their `interests`, and a
|
|
@@ -317,6 +424,53 @@ export const TOPIC_CATEGORIES = [
|
|
|
317
424
|
'education_access',
|
|
318
425
|
]),
|
|
319
426
|
];
|
|
427
|
+
// ─── Goals: the grouping the picker renders ──────────────────────────────────
|
|
428
|
+
/**
|
|
429
|
+
* The goals step's three headings (0.17.0, moved here from the web, where the
|
|
430
|
+
* grouping lived in the screen): eighteen goals since 0.16.0, shown a group at
|
|
431
|
+
* a time so the eye takes six tiles, not a wall of eighteen. A group is named
|
|
432
|
+
* by its `key`, which the client words, like a topic category.
|
|
433
|
+
*
|
|
434
|
+
* ⚠ `GOAL_VALUES` stays the single source of truth for what a goal IS; this is
|
|
435
|
+
* typed against it, and a test holds the two together in both directions:
|
|
436
|
+
* every goal in exactly one group, nothing in a group that is not a goal.
|
|
437
|
+
*/
|
|
438
|
+
export const GOAL_CATEGORY_KEYS = ['build', 'grow', 'learn'];
|
|
439
|
+
export const GOAL_CATEGORIES = [
|
|
440
|
+
{
|
|
441
|
+
key: 'build',
|
|
442
|
+
values: [
|
|
443
|
+
'start_company',
|
|
444
|
+
'get_feedback',
|
|
445
|
+
'find_cofounder',
|
|
446
|
+
'find_testers',
|
|
447
|
+
'hiring',
|
|
448
|
+
'find_service_providers',
|
|
449
|
+
],
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
key: 'grow',
|
|
453
|
+
values: [
|
|
454
|
+
'find_customers',
|
|
455
|
+
'find_investors',
|
|
456
|
+
'partnerships',
|
|
457
|
+
'market_insights',
|
|
458
|
+
'business_ideas',
|
|
459
|
+
'grow_network',
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
key: 'learn',
|
|
464
|
+
values: [
|
|
465
|
+
'learn_startup',
|
|
466
|
+
'mentorship',
|
|
467
|
+
'attend_events',
|
|
468
|
+
'join_startup',
|
|
469
|
+
'mentor_others',
|
|
470
|
+
'invest_in_startups',
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
];
|
|
320
474
|
/**
|
|
321
475
|
* How many a member must pick. A minimum exists because these drive feed
|
|
322
476
|
* relevance and zero of them means no relevance at all.
|
|
@@ -328,15 +482,42 @@ export const TOPIC_CATEGORIES = [
|
|
|
328
482
|
*/
|
|
329
483
|
export const INTERESTS_MIN = 3;
|
|
330
484
|
export const INTERESTS_MAX = 10;
|
|
485
|
+
/**
|
|
486
|
+
* A member's interests, by ONE rule (0.16.0): onboarding's finish and the Me
|
|
487
|
+
* tab's editor (`putMyInterestsBodySchema`) both apply this very schema, so a
|
|
488
|
+
* list one accepts the other can never refuse.
|
|
489
|
+
*
|
|
490
|
+
* Each topic only once: without that, three copies of one topic met the
|
|
491
|
+
* minimum of three while meaning one interest.
|
|
492
|
+
*/
|
|
493
|
+
export const interestsSchema = z
|
|
494
|
+
.array(topicSchema)
|
|
495
|
+
.min(INTERESTS_MIN)
|
|
496
|
+
.max(INTERESTS_MAX)
|
|
497
|
+
.refine((topics) => new Set(topics).size === topics.length, {
|
|
498
|
+
error: VALIDATION_MESSAGE.topicsDuplicate,
|
|
499
|
+
});
|
|
331
500
|
// ─── The step keys ───────────────────────────────────────────────────────────
|
|
332
501
|
/**
|
|
333
502
|
* The step keys, in order, as a literal union, so a consumer can key a table by
|
|
334
503
|
* step and have TypeScript insist on every one. Before 0.2.0 `key` was a plain
|
|
335
504
|
* string and the web client kept its own copy of this list to get the same
|
|
336
505
|
* check; a test there held the two together. Now there is one list.
|
|
506
|
+
*
|
|
507
|
+
* 0.16.0 (the user, 2026-10-18) split the old first step, `identity`, into
|
|
508
|
+
* three: `basics` (full name, gender, date of birth, nothing else), `photo`
|
|
509
|
+
* (the photo alone) and `professional` (title, about, skills). Nine steps, up
|
|
510
|
+
* from seven.
|
|
511
|
+
*
|
|
512
|
+
* ⚠ THE SERVER'S RESUME CURSOR IS AN INDEX INTO THIS LIST, so re-ordering it
|
|
513
|
+
* moves every member who stopped part-way. A cursor saved under the old seven
|
|
514
|
+
* is translated by the backend (its progress rows carry the list's version);
|
|
515
|
+
* any further change needs the same.
|
|
337
516
|
*/
|
|
338
517
|
export const ONBOARDING_STEP_KEYS = [
|
|
339
|
-
'
|
|
518
|
+
'basics',
|
|
519
|
+
'photo',
|
|
520
|
+
'professional',
|
|
340
521
|
'contact',
|
|
341
522
|
'location',
|
|
342
523
|
'goals',
|
|
@@ -358,8 +539,15 @@ export const ONBOARDING_STEP_KEYS = [
|
|
|
358
539
|
* profile; that record is the point.
|
|
359
540
|
*/
|
|
360
541
|
export const GUIDELINES_VERSION = '2026-10-03';
|
|
542
|
+
/**
|
|
543
|
+
* Not agreed at all (the box left unticked) is `guidelinesRequired`; agreed to
|
|
544
|
+
* another text is `guidelinesOutdated` (0.17.0: both were the second, so a
|
|
545
|
+
* client had to word an unticked box itself).
|
|
546
|
+
*/
|
|
361
547
|
export const guidelinesVersionSchema = z.literal(GUIDELINES_VERSION, {
|
|
362
|
-
error:
|
|
548
|
+
error: (issue) => issue.input === undefined || issue.input === null
|
|
549
|
+
? VALIDATION_MESSAGE.guidelinesRequired
|
|
550
|
+
: VALIDATION_MESSAGE.guidelinesOutdated,
|
|
363
551
|
});
|
|
364
552
|
// ─── Contact ─────────────────────────────────────────────────────────────────
|
|
365
553
|
/**
|
|
@@ -374,6 +562,12 @@ export const contactLinksSchema = founderLinksSchema.pick({
|
|
|
374
562
|
});
|
|
375
563
|
/** A team size has at most this many digits: `TEAM_SIZE_MAX` written out. */
|
|
376
564
|
export const TEAM_SIZE_DIGITS_MAX = String(TEAM_SIZE_MAX).length;
|
|
565
|
+
/**
|
|
566
|
+
* A team size as typed, kept to what the field may hold: digits only, at most
|
|
567
|
+
* `TEAM_SIZE_DIGITS_MAX` (0.17.0: the draft reader and every client's field
|
|
568
|
+
* clean it with this one rule).
|
|
569
|
+
*/
|
|
570
|
+
export const teamSizeDigits = (raw) => raw.replace(/\D/g, '').slice(0, TEAM_SIZE_DIGITS_MAX);
|
|
377
571
|
// ─── The form, mid-flight: the draft each step saves ──────────────────────────
|
|
378
572
|
/**
|
|
379
573
|
* The answers so far, keyed by the finish body's own field names: what each
|
|
@@ -388,10 +582,10 @@ export const TEAM_SIZE_DIGITS_MAX = String(TEAM_SIZE_MAX).length;
|
|
|
388
582
|
* `completeOnboardingBodySchema`'s decision, and the client builds that body
|
|
389
583
|
* from these answers.
|
|
390
584
|
*
|
|
391
|
-
* Screens may hold a different shape on top of this one
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
585
|
+
* Screens may hold a different shape on top of this one. Derive that type from
|
|
586
|
+
* `OnboardingForm` rather than writing it out, so a field added here reaches
|
|
587
|
+
* every screen. (Since 0.16.0 the name is asked as one full name, which is
|
|
588
|
+
* `displayName` as typed; first and last name are deduced by `splitFullName`.)
|
|
395
589
|
*
|
|
396
590
|
* A stored draft is UNTRUSTED when it comes back: an older client, or another
|
|
397
591
|
* one, wrote it, and the server stores values opaquely. Read it with
|
|
@@ -405,11 +599,16 @@ export const onboardingFormSchema = z.object({
|
|
|
405
599
|
headline: z.string().max(HEADLINE_MAX),
|
|
406
600
|
about: z.string().max(ABOUT_MAX),
|
|
407
601
|
/**
|
|
408
|
-
* The completed upload's id (`media`, purpose `avatar`)
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
602
|
+
* The completed upload's id (`media`, purpose `avatar`), or `null` for no
|
|
603
|
+
* photo.
|
|
604
|
+
*
|
|
605
|
+
* SAVED BY THE PHOTO STEP, KEPT BY THE SERVER, NOT IN THE DRAFT (0.17.0). A
|
|
606
|
+
* step save carrying this key hands the upload to the server, which holds it
|
|
607
|
+
* for the draft (so the unattached sweep, `MEDIA_UNATTACHED_TTL_HOURS`, never
|
|
608
|
+
* deletes it) and lets go of the one it replaces; `null` lets go of it. It
|
|
609
|
+
* comes back as the status's `photo`, read from what the server holds, never
|
|
610
|
+
* from a stored draft: `readOnboardingDraft` still gives `null` here, so an
|
|
611
|
+
* id an older client left in a draft is never trusted.
|
|
413
612
|
*/
|
|
414
613
|
avatarMediaId: objectIdSchema.nullable(),
|
|
415
614
|
skills: z.array(skillSchema).max(SKILLS_MAX),
|
|
@@ -444,6 +643,7 @@ export const onboardingFormSchema = z.object({
|
|
|
444
643
|
teamSize: z.string().max(TEAM_SIZE_DIGITS_MAX),
|
|
445
644
|
}),
|
|
446
645
|
businessType: businessTypeSchema.nullable(),
|
|
646
|
+
legalStructure: legalStructureSchema.nullable(),
|
|
447
647
|
/** The guidelines text the member agreed to, or `null`. */
|
|
448
648
|
guidelinesVersion: z.string().nullable(),
|
|
449
649
|
});
|
|
@@ -506,12 +706,10 @@ const draftReaderSchema = z.object({
|
|
|
506
706
|
website: draftText(LINK_URL_MAX),
|
|
507
707
|
industry: draftChoice(INDUSTRY_VALUES),
|
|
508
708
|
stage: draftChoice(STARTUP_STAGE_VALUES),
|
|
509
|
-
teamSize: z
|
|
510
|
-
.string()
|
|
511
|
-
.catch('')
|
|
512
|
-
.transform((v) => v.replace(/\D/g, '').slice(0, TEAM_SIZE_DIGITS_MAX)),
|
|
709
|
+
teamSize: z.string().catch('').transform(teamSizeDigits),
|
|
513
710
|
}).transform((venture) => venture ?? { ...EMPTY_ONBOARDING_VENTURE }),
|
|
514
711
|
businessType: draftChoice(BUSINESS_TYPE_VALUES),
|
|
712
|
+
legalStructure: draftChoice(LEGAL_STRUCTURE_VALUES),
|
|
515
713
|
/** Agreement to an OLDER text is not agreement to this one. */
|
|
516
714
|
guidelinesVersion: z
|
|
517
715
|
.unknown()
|
|
@@ -533,8 +731,8 @@ export const EMPTY_ONBOARDING_VENTURE = {
|
|
|
533
731
|
* one that does not fit becomes "not answered" (`''`, `null`, `[]`) and the
|
|
534
732
|
* rest of the draft survives. Never coerced into a different answer: a value
|
|
535
733
|
* outside a vocabulary is dropped, not mapped to the nearest one. A photo
|
|
536
|
-
* is never restored (
|
|
537
|
-
* CURRENT guidelines.
|
|
734
|
+
* is never restored from here (it comes back as the status's `photo`; see
|
|
735
|
+
* `avatarMediaId`), and agreement only counts for the CURRENT guidelines.
|
|
538
736
|
*
|
|
539
737
|
* It fills in no answer the member did not give: a blank country stays blank.
|
|
540
738
|
* Whatever a screen shows by default is that screen's choice.
|
|
@@ -545,23 +743,12 @@ export function readOnboardingDraft(draft) {
|
|
|
545
743
|
}
|
|
546
744
|
// ─── The finalize body ───────────────────────────────────────────────────────
|
|
547
745
|
/**
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
* every member who skipped the question was recorded as O-positive: a real
|
|
553
|
-
* answer, indistinguishable from one somebody gave, sitting on a screen staff
|
|
554
|
-
* would read in an emergency. Absent has to stay absent.
|
|
555
|
-
*
|
|
556
|
-
* ⚠️ AND THIS IS NOT THE GATE EITHER. The server re-checks the age and refuses
|
|
557
|
-
* at finalize. `isOnboarded` may be set here and nowhere else: four of the
|
|
558
|
-
* profile's partial indexes are gated on it, so a half-built profile carrying
|
|
559
|
-
* it true appears in Discover half-built.
|
|
560
|
-
*
|
|
561
|
-
* Location, venture and links are the profile's own shapes, not copies: the
|
|
562
|
-
* copies are how the profile model came to lack Instagram.
|
|
746
|
+
* The finish body's fields, before the rules that span fields: what each
|
|
747
|
+
* step's slice is PICKED from (Zod 4 refuses `.pick` on a refined object).
|
|
748
|
+
* Everything outside this file reads `completeOnboardingBodySchema`, whose
|
|
749
|
+
* note below says what the body is.
|
|
563
750
|
*/
|
|
564
|
-
|
|
751
|
+
const completeOnboardingBodyFields = z.object({
|
|
565
752
|
// Plain bounds, no custom message: `too_small` with minimum 1 already says
|
|
566
753
|
// "required", and the client words it in the member's language.
|
|
567
754
|
displayName: displayNameSchema,
|
|
@@ -592,17 +779,24 @@ export const completeOnboardingBodySchema = z.object({
|
|
|
592
779
|
avatarMediaId: objectIdSchema.optional(),
|
|
593
780
|
phone: phoneSchema,
|
|
594
781
|
links: contactLinksSchema.optional(),
|
|
595
|
-
/**
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
782
|
+
/**
|
|
783
|
+
* Absent when the member skipped the step or refused the permission. In
|
|
784
|
+
* India, one of its states (0.17.0: `indiaLocationSchema`, the rule the
|
|
785
|
+
* screen always showed).
|
|
786
|
+
*/
|
|
787
|
+
location: indiaLocationSchema.optional(),
|
|
788
|
+
/** The shared list rule (each goal once, 0.17.0) plus finish's minimum. */
|
|
789
|
+
goals: goalListSchema.min(GOALS_MIN),
|
|
790
|
+
interests: interestsSchema,
|
|
791
|
+
skills: skillListSchema.optional(),
|
|
600
792
|
/**
|
|
601
793
|
* Absent when the member skipped the business step. The profile's venture
|
|
602
794
|
* without `foundedYear`, which onboarding does not ask; the Me tab does.
|
|
603
795
|
*/
|
|
604
796
|
venture: ventureSchema.omit({ foundedYear: true }).optional(),
|
|
605
797
|
businessType: businessTypeSchema.optional(),
|
|
798
|
+
/** How the venture is registered, if the member says. Stored on the venture, like the type. */
|
|
799
|
+
legalStructure: legalStructureSchema.optional(),
|
|
606
800
|
/**
|
|
607
801
|
* The version of the guidelines this member read: `GUIDELINES_VERSION`,
|
|
608
802
|
* exactly.
|
|
@@ -614,6 +808,37 @@ export const completeOnboardingBodySchema = z.object({
|
|
|
614
808
|
*/
|
|
615
809
|
guidelinesVersion: guidelinesVersionSchema,
|
|
616
810
|
});
|
|
811
|
+
const typeHasVenture = (body) => body.venture !== undefined || body.businessType === undefined;
|
|
812
|
+
const registrationHasVenture = (body) => body.venture !== undefined || body.legalStructure === undefined;
|
|
813
|
+
const TYPE_NEEDS_VENTURE = { error: VALIDATION_MESSAGE.ventureRequired, path: ['businessType'] };
|
|
814
|
+
const REGISTRATION_NEEDS_VENTURE = {
|
|
815
|
+
error: VALIDATION_MESSAGE.ventureRequired,
|
|
816
|
+
path: ['legalStructure'],
|
|
817
|
+
};
|
|
818
|
+
/**
|
|
819
|
+
* What the client sends to finish. Completed answers only.
|
|
820
|
+
*
|
|
821
|
+
* ⚠️ EVERY OPTIONAL FIELD HERE MUST BE ABSENT WHEN UNANSWERED, never a default
|
|
822
|
+
* standing in for one. The sibling gym product sent `bloodGroup ?? 'O+'`, so
|
|
823
|
+
* every member who skipped the question was recorded as O-positive: a real
|
|
824
|
+
* answer, indistinguishable from one somebody gave, sitting on a screen staff
|
|
825
|
+
* would read in an emergency. Absent has to stay absent.
|
|
826
|
+
*
|
|
827
|
+
* ⚠️ AND THIS IS NOT THE GATE EITHER. The server re-checks the age and refuses
|
|
828
|
+
* at finalize. `isOnboarded` may be set here and nowhere else: four of the
|
|
829
|
+
* profile's partial indexes are gated on it, so a half-built profile carrying
|
|
830
|
+
* it true appears in Discover half-built.
|
|
831
|
+
*
|
|
832
|
+
* Location, venture and links are the profile's own shapes, not copies: the
|
|
833
|
+
* copies are how the profile model came to lack Instagram.
|
|
834
|
+
*
|
|
835
|
+
* Refined (the business type and registration need a venture, above), so Zod 4
|
|
836
|
+
* throws on `.pick` / `.omit` / `.partial` / `.merge` of it: read a field with
|
|
837
|
+
* `.shape.<field>`, a step's with `ONBOARDING_STEP_SCHEMAS.<step>`.
|
|
838
|
+
*/
|
|
839
|
+
export const completeOnboardingBodySchema = completeOnboardingBodyFields
|
|
840
|
+
.refine(typeHasVenture, TYPE_NEEDS_VENTURE)
|
|
841
|
+
.refine(registrationHasVenture, REGISTRATION_NEEDS_VENTURE);
|
|
617
842
|
// ─── The steps ───────────────────────────────────────────────────────────────
|
|
618
843
|
/**
|
|
619
844
|
* Each step's slice of the finish body, picked FROM it, so a client validating
|
|
@@ -630,31 +855,38 @@ export const completeOnboardingBodySchema = z.object({
|
|
|
630
855
|
* on a draft save, because a half-answered step must be storable.
|
|
631
856
|
*/
|
|
632
857
|
export const ONBOARDING_STEP_SCHEMAS = {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
858
|
+
// 0.16.0: the old `identity` step, split three ways by the user. The basics
|
|
859
|
+
// are the step's only required answers; the photo and the professional
|
|
860
|
+
// details may each be passed over.
|
|
861
|
+
basics: completeOnboardingBodyFields.pick({ displayName: true, gender: true, dateOfBirth: true }),
|
|
862
|
+
photo: completeOnboardingBodyFields.pick({ avatarMediaId: true }),
|
|
863
|
+
professional: completeOnboardingBodyFields.pick({
|
|
637
864
|
headline: true,
|
|
638
865
|
about: true,
|
|
639
|
-
|
|
640
|
-
//
|
|
641
|
-
// skills" screen into step one. It was on no step until 0.2.0, which made
|
|
642
|
-
// it the one finish field no step owned.
|
|
866
|
+
// Ruling D8 folded the design's "title, achievements, skills" screen into
|
|
867
|
+
// the profile's first questions; it now has a step of its own again.
|
|
643
868
|
skills: true,
|
|
644
869
|
}),
|
|
645
|
-
contact:
|
|
646
|
-
location:
|
|
647
|
-
goals:
|
|
648
|
-
interests:
|
|
649
|
-
business:
|
|
650
|
-
|
|
870
|
+
contact: completeOnboardingBodyFields.pick({ phone: true, links: true }),
|
|
871
|
+
location: completeOnboardingBodyFields.pick({ location: true }),
|
|
872
|
+
goals: completeOnboardingBodyFields.pick({ goals: true }),
|
|
873
|
+
interests: completeOnboardingBodyFields.pick({ interests: true }),
|
|
874
|
+
business: completeOnboardingBodyFields
|
|
875
|
+
.pick({
|
|
876
|
+
venture: true,
|
|
877
|
+
businessType: true,
|
|
878
|
+
legalStructure: true,
|
|
879
|
+
})
|
|
880
|
+
.refine(typeHasVenture, TYPE_NEEDS_VENTURE)
|
|
881
|
+
.refine(registrationHasVenture, REGISTRATION_NEEDS_VENTURE),
|
|
882
|
+
guidelines: completeOnboardingBodyFields.pick({ guidelinesVersion: true }),
|
|
651
883
|
};
|
|
652
884
|
export const FOUNDER_ONBOARDING_STEPS = ONBOARDING_STEP_KEYS.map((key) => {
|
|
653
885
|
const slice = ONBOARDING_STEP_SCHEMAS[key];
|
|
654
886
|
return {
|
|
655
887
|
key,
|
|
888
|
+
labelKey: `onboarding.steps.${key}.label`,
|
|
656
889
|
titleKey: `onboarding.steps.${key}.title`,
|
|
657
|
-
helpKey: `onboarding.steps.${key}.help`,
|
|
658
890
|
fields: Object.keys(slice.shape),
|
|
659
891
|
required: !slice.safeParse({}).success,
|
|
660
892
|
};
|
|
@@ -703,10 +935,26 @@ export const onboardingProgressSchema = z.object({
|
|
|
703
935
|
* `PATCH` body: any subset of the cursor, merged server-side. No defaults.
|
|
704
936
|
* `step` may equal `ONBOARDING_TOTAL_STEPS`: "past the last step", which the
|
|
705
937
|
* client sends on its way to finish.
|
|
938
|
+
*
|
|
939
|
+
* The draft's values are opaque, but ONE is not kept as typed (0.17.0): the
|
|
940
|
+
* photo step's `avatarMediaId` is handed to the server, which holds that
|
|
941
|
+
* upload, so it must be an upload id or `null` like the form's own field.
|
|
706
942
|
*/
|
|
707
943
|
export const onboardingProgressUpdateSchema = z.object({
|
|
708
944
|
step: z.number().int().min(0).max(ONBOARDING_TOTAL_STEPS).optional(),
|
|
709
|
-
draft: onboardingDraftSchema
|
|
945
|
+
draft: onboardingDraftSchema
|
|
946
|
+
.refine((draft) => !('avatarMediaId' in draft) ||
|
|
947
|
+
onboardingFormSchema.shape.avatarMediaId.safeParse(draft.avatarMediaId).success, { error: VALIDATION_MESSAGE.invalidId, path: ['avatarMediaId'] })
|
|
948
|
+
// ...and read as an id is read everywhere (`objectIdSchema`: lower case),
|
|
949
|
+
// since the server compares it with the upload it already holds: the
|
|
950
|
+
// same photo sent in capitals was not "the one held", and was refused.
|
|
951
|
+
.transform((draft) => 'avatarMediaId' in draft
|
|
952
|
+
? {
|
|
953
|
+
...draft,
|
|
954
|
+
avatarMediaId: onboardingFormSchema.shape.avatarMediaId.parse(draft.avatarMediaId),
|
|
955
|
+
}
|
|
956
|
+
: draft)
|
|
957
|
+
.optional(),
|
|
710
958
|
});
|
|
711
959
|
/**
|
|
712
960
|
* What the client reads on return, from `GET` and from every `PATCH`.
|
|
@@ -720,5 +968,13 @@ export const onboardingStatusSchema = z.object({
|
|
|
720
968
|
onboarded: z.boolean(),
|
|
721
969
|
step: z.number().int().min(0).max(ONBOARDING_TOTAL_STEPS),
|
|
722
970
|
draft: onboardingDraftSchema.optional(),
|
|
971
|
+
/**
|
|
972
|
+
* The photo the member saved on the photo step, as it will appear (0.17.0):
|
|
973
|
+
* the upload the server holds for this member's draft. Absent when they
|
|
974
|
+
* saved none, removed it, or have finished. A client resumes the photo step
|
|
975
|
+
* from this (its `id` is the finish body's `avatarMediaId`), never from the
|
|
976
|
+
* draft.
|
|
977
|
+
*/
|
|
978
|
+
photo: mediaImageSchema.optional(),
|
|
723
979
|
});
|
|
724
980
|
//# sourceMappingURL=onboarding.js.map
|