founder-schema 0.16.0 → 0.18.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 +263 -19
- package/dist/access/access.d.ts +1 -1
- package/dist/access/access.d.ts.map +1 -1
- package/dist/access/access.js +4 -1
- package/dist/access/access.js.map +1 -1
- package/dist/access/permissions.d.ts +27 -29
- package/dist/access/permissions.d.ts.map +1 -1
- package/dist/access/permissions.js +39 -3
- package/dist/access/permissions.js.map +1 -1
- package/dist/core/common.d.ts +117 -23
- package/dist/core/common.d.ts.map +1 -1
- package/dist/core/common.js +104 -21
- package/dist/core/common.js.map +1 -1
- package/dist/core/errors.d.ts +7 -5
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +17 -11
- package/dist/core/errors.js.map +1 -1
- package/dist/core/limits.d.ts +55 -17
- package/dist/core/limits.d.ts.map +1 -1
- package/dist/core/limits.js +59 -22
- package/dist/core/limits.js.map +1 -1
- package/dist/core/money.d.ts +33 -0
- package/dist/core/money.d.ts.map +1 -0
- package/dist/core/money.js +36 -0
- package/dist/core/money.js.map +1 -0
- package/dist/core/responses.d.ts +14 -4
- package/dist/core/responses.d.ts.map +1 -1
- package/dist/core/responses.js.map +1 -1
- package/dist/core/validation.d.ts +32 -2
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +32 -2
- package/dist/core/validation.js.map +1 -1
- package/dist/domains/billing/billing.d.ts +61 -8
- package/dist/domains/billing/billing.d.ts.map +1 -1
- package/dist/domains/billing/billing.js +58 -22
- package/dist/domains/billing/billing.js.map +1 -1
- package/dist/domains/billing/gst.d.ts.map +1 -1
- package/dist/domains/billing/gst.js +4 -3
- package/dist/domains/billing/gst.js.map +1 -1
- package/dist/domains/chat/chat.d.ts +0 -21
- package/dist/domains/chat/chat.d.ts.map +1 -1
- package/dist/domains/chat/chat.js +0 -20
- package/dist/domains/chat/chat.js.map +1 -1
- package/dist/domains/communities/communities.d.ts +30 -12
- package/dist/domains/communities/communities.d.ts.map +1 -1
- package/dist/domains/communities/communities.js +43 -15
- package/dist/domains/communities/communities.js.map +1 -1
- package/dist/domains/contributions/contributions.d.ts +1 -1
- package/dist/domains/contributions/contributions.js +1 -1
- package/dist/domains/contributions/contributions.js.map +1 -1
- package/dist/domains/dashboard/dashboard.d.ts +3 -1
- package/dist/domains/dashboard/dashboard.d.ts.map +1 -1
- package/dist/domains/dashboard/dashboard.js +23 -7
- package/dist/domains/dashboard/dashboard.js.map +1 -1
- package/dist/domains/discover/discover.d.ts +8 -8
- package/dist/domains/feed/feed.d.ts +55 -153
- package/dist/domains/feed/feed.d.ts.map +1 -1
- package/dist/domains/feed/feed.js +53 -58
- package/dist/domains/feed/feed.js.map +1 -1
- package/dist/domains/location/india.d.ts +215 -0
- package/dist/domains/location/india.d.ts.map +1 -0
- package/dist/domains/location/india.js +1203 -0
- package/dist/domains/location/india.js.map +1 -0
- package/dist/domains/members/members.d.ts +2 -2
- package/dist/domains/membership/membership.d.ts +26 -0
- package/dist/domains/membership/membership.d.ts.map +1 -1
- package/dist/domains/membership/membership.js +55 -1
- package/dist/domains/membership/membership.js.map +1 -1
- package/dist/domains/moderation/moderation.d.ts +2 -2
- package/dist/domains/moderation/moderation.js +3 -3
- package/dist/domains/moderation/moderation.js.map +1 -1
- package/dist/domains/notifications/notifications.d.ts +1 -10
- package/dist/domains/notifications/notifications.d.ts.map +1 -1
- package/dist/domains/notifications/notifications.js +2 -3
- package/dist/domains/notifications/notifications.js.map +1 -1
- package/dist/domains/onboarding/onboarding-answers.d.ts +112 -0
- package/dist/domains/onboarding/onboarding-answers.d.ts.map +1 -0
- package/dist/domains/onboarding/onboarding-answers.js +164 -0
- package/dist/domains/onboarding/onboarding-answers.js.map +1 -0
- package/dist/domains/onboarding/onboarding.d.ts +398 -62
- package/dist/domains/onboarding/onboarding.d.ts.map +1 -1
- package/dist/domains/onboarding/onboarding.js +229 -68
- package/dist/domains/onboarding/onboarding.js.map +1 -1
- package/dist/domains/products/products.d.ts +43 -8
- package/dist/domains/products/products.d.ts.map +1 -1
- package/dist/domains/products/products.js +57 -11
- package/dist/domains/products/products.js.map +1 -1
- package/dist/domains/profile/founder-profile.d.ts +120 -68
- package/dist/domains/profile/founder-profile.d.ts.map +1 -1
- package/dist/domains/profile/founder-profile.js +80 -29
- package/dist/domains/profile/founder-profile.js.map +1 -1
- package/dist/domains/profile/my-profile.d.ts +3 -3
- package/dist/domains/reviews/reviews.d.ts +1 -1
- package/dist/domains/reviews/reviews.d.ts.map +1 -1
- package/dist/domains/reviews/reviews.js +2 -2
- package/dist/domains/reviews/reviews.js.map +1 -1
- package/dist/domains/support/support.d.ts +22 -3
- package/dist/domains/support/support.d.ts.map +1 -1
- package/dist/domains/support/support.js +19 -5
- package/dist/domains/support/support.js.map +1 -1
- package/dist/domains/team/team.d.ts.map +1 -1
- package/dist/domains/team/team.js +3 -6
- package/dist/domains/team/team.js.map +1 -1
- package/dist/domains/uploads/uploads.d.ts +7 -22
- package/dist/domains/uploads/uploads.d.ts.map +1 -1
- package/dist/domains/uploads/uploads.js +7 -21
- package/dist/domains/uploads/uploads.js.map +1 -1
- package/dist/env.d.ts +26 -13
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +58 -23
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +18 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
30
|
-
import { ABOUT_MAX, CITY_MAX, COUNTRY_MAX,
|
|
29
|
+
import { displayNameSchema, eachOnce, GENDER_VALUES, GOAL_VALUES, genderSchema, goalSchema, INDUSTRY_VALUES, industrySchema, objectIdSchema, optionalTrimmedString, phoneSchema, requiredGoalListSchema, SKILL_VALUES, STARTUP_STAGE_VALUES, skillListSchema, skillSchema, startupStageSchema, usernameSchema, } from '../../core/common.js';
|
|
30
|
+
import { ABOUT_MAX, CITY_MAX, COUNTRY_MAX, DAY_MS, DISPLAY_NAME_MAX, HEADLINE_MAX, INDIA_UTC_OFFSET_MS, 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
|
/**
|
|
@@ -52,8 +54,6 @@ export const MAX_AGE_YEARS = 120;
|
|
|
52
54
|
* Measured: it disagrees with the calendar on all 400 of the next 400 days.
|
|
53
55
|
* An age gate is exactly the place an approximation is not good enough.
|
|
54
56
|
*/
|
|
55
|
-
/** India Standard Time, UTC+05:30, with no daylight saving. */
|
|
56
|
-
const INDIA_OFFSET_MS = (5 * 60 + 30) * 60 * 1000;
|
|
57
57
|
export function ageInYears(isoDate, now = new Date()) {
|
|
58
58
|
// ⚠️ A STRING, `YYYY-MM-DD`, not a `Date`. This is a wire contract, so it
|
|
59
59
|
// takes the wire type. The backend is the obvious caller and the backend
|
|
@@ -67,7 +67,7 @@ export function ageInYears(isoDate, now = new Date()) {
|
|
|
67
67
|
// The date TODAY IS IN INDIA (0.13.0), where the members are: a birthday
|
|
68
68
|
// begins at midnight there, not five and a half hours later at UTC's
|
|
69
69
|
// midnight. India keeps one offset all year.
|
|
70
|
-
const today = new Date(now.getTime() +
|
|
70
|
+
const today = new Date(now.getTime() + INDIA_UTC_OFFSET_MS);
|
|
71
71
|
let age = today.getUTCFullYear() - y;
|
|
72
72
|
const monthNow = today.getUTCMonth() + 1;
|
|
73
73
|
if (monthNow < m || (monthNow === m && today.getUTCDate() < d))
|
|
@@ -87,6 +87,57 @@ function isRealDate(iso) {
|
|
|
87
87
|
const asDate = new Date(Date.UTC(y, m - 1, d));
|
|
88
88
|
return (asDate.getUTCFullYear() === y && asDate.getUTCMonth() === m - 1 && asDate.getUTCDate() === d);
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* True for a `YYYY-MM-DD` that is a real calendar date (0.17.0, exported so a
|
|
92
|
+
* client's date picker checks a date exactly as `dateOfBirthSchema` does):
|
|
93
|
+
* not 2026-02-30, not 2026-13-01.
|
|
94
|
+
*/
|
|
95
|
+
export function isRealIsoDate(value) {
|
|
96
|
+
return isoDateSchema.safeParse(value).success && isRealDate(value.trim());
|
|
97
|
+
}
|
|
98
|
+
/** `YYYY-MM-DD` for a moment, counted in India's calendar. */
|
|
99
|
+
const isoInIndia = (moment) => new Date(moment + INDIA_UTC_OFFSET_MS).toISOString().slice(0, 10);
|
|
100
|
+
/**
|
|
101
|
+
* Today in India, `YYYY-MM-DD` (0.17.0): the day every age here is counted
|
|
102
|
+
* from (`ageInYears`), so a client's "today" must be this one, never the
|
|
103
|
+
* device's own: east of India the device is already on tomorrow.
|
|
104
|
+
*/
|
|
105
|
+
export function todayInIndia(now = new Date()) {
|
|
106
|
+
return isoInIndia(now.getTime());
|
|
107
|
+
}
|
|
108
|
+
const isoOfUtc = (ms) => new Date(ms).toISOString().slice(0, 10);
|
|
109
|
+
/**
|
|
110
|
+
* The dates of birth `dateOfBirthSchema` accepts TODAY, both ends included
|
|
111
|
+
* (0.17.0): `latest` makes someone exactly `MIN_AGE_YEARS` today, `earliest`
|
|
112
|
+
* exactly `MAX_AGE_YEARS`. Worked out with `ageInYears` itself, so a picker
|
|
113
|
+
* that offers only this window can never offer a date the schema refuses, a
|
|
114
|
+
* 29 February included.
|
|
115
|
+
*/
|
|
116
|
+
export function dateOfBirthBounds(now = new Date()) {
|
|
117
|
+
const [y, m, d] = todayInIndia(now).split('-').map(Number);
|
|
118
|
+
let latest = Date.UTC(y - MIN_AGE_YEARS, m - 1, d);
|
|
119
|
+
while (ageInYears(isoOfUtc(latest), now) < MIN_AGE_YEARS)
|
|
120
|
+
latest -= DAY_MS;
|
|
121
|
+
let earliest = Date.UTC(y - MAX_AGE_YEARS - 1, m - 1, d);
|
|
122
|
+
while (ageInYears(isoOfUtc(earliest), now) > MAX_AGE_YEARS)
|
|
123
|
+
earliest += DAY_MS;
|
|
124
|
+
return { earliest: isoOfUtc(earliest), latest: isoOfUtc(latest) };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* True only for a COMPLETE, real date of birth that is under `MIN_AGE_YEARS`
|
|
128
|
+
* today (0.17.0): the rule the server applies to a draft, where a date still
|
|
129
|
+
* being typed must pass ("19" says nothing about anyone's age yet). Until
|
|
130
|
+
* 0.17.0 the backend wrote this out by hand and skipped the real-date check.
|
|
131
|
+
*/
|
|
132
|
+
export function isUnderAgeDateOfBirth(value, now = new Date()) {
|
|
133
|
+
if (typeof value !== 'string' || !isRealIsoDate(value))
|
|
134
|
+
return false;
|
|
135
|
+
return ageInYears(value.trim(), now) < MIN_AGE_YEARS;
|
|
136
|
+
}
|
|
137
|
+
/** A `YYYY-MM-DD` as the instant it is stored as: midnight UTC that day (0.17.0). */
|
|
138
|
+
export const isoDateToDate = (iso) => new Date(`${iso.trim()}T00:00:00.000Z`);
|
|
139
|
+
/** A stored date (midnight UTC) as `YYYY-MM-DD` (0.17.0): `isoDateToDate`'s inverse. */
|
|
140
|
+
export const dateToIsoDate = (date) => date.toISOString().slice(0, 10);
|
|
90
141
|
/**
|
|
91
142
|
* Date of birth, with the age gate applied per parse.
|
|
92
143
|
*
|
|
@@ -370,6 +421,53 @@ export const TOPIC_CATEGORIES = [
|
|
|
370
421
|
'education_access',
|
|
371
422
|
]),
|
|
372
423
|
];
|
|
424
|
+
// ─── Goals: the grouping the picker renders ──────────────────────────────────
|
|
425
|
+
/**
|
|
426
|
+
* The goals step's three headings (0.17.0, moved here from the web, where the
|
|
427
|
+
* grouping lived in the screen): eighteen goals since 0.16.0, shown a group at
|
|
428
|
+
* a time so the eye takes six tiles, not a wall of eighteen. A group is named
|
|
429
|
+
* by its `key`, which the client words, like a topic category.
|
|
430
|
+
*
|
|
431
|
+
* ⚠ `GOAL_VALUES` stays the single source of truth for what a goal IS; this is
|
|
432
|
+
* typed against it, and a test holds the two together in both directions:
|
|
433
|
+
* every goal in exactly one group, nothing in a group that is not a goal.
|
|
434
|
+
*/
|
|
435
|
+
export const GOAL_CATEGORY_KEYS = ['build', 'grow', 'learn'];
|
|
436
|
+
export const GOAL_CATEGORIES = [
|
|
437
|
+
{
|
|
438
|
+
key: 'build',
|
|
439
|
+
values: [
|
|
440
|
+
'start_company',
|
|
441
|
+
'get_feedback',
|
|
442
|
+
'find_cofounder',
|
|
443
|
+
'find_testers',
|
|
444
|
+
'hiring',
|
|
445
|
+
'find_service_providers',
|
|
446
|
+
],
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
key: 'grow',
|
|
450
|
+
values: [
|
|
451
|
+
'find_customers',
|
|
452
|
+
'find_investors',
|
|
453
|
+
'partnerships',
|
|
454
|
+
'market_insights',
|
|
455
|
+
'business_ideas',
|
|
456
|
+
'grow_network',
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
key: 'learn',
|
|
461
|
+
values: [
|
|
462
|
+
'learn_startup',
|
|
463
|
+
'mentorship',
|
|
464
|
+
'attend_events',
|
|
465
|
+
'join_startup',
|
|
466
|
+
'mentor_others',
|
|
467
|
+
'invest_in_startups',
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
];
|
|
373
471
|
/**
|
|
374
472
|
* How many a member must pick. A minimum exists because these drive feed
|
|
375
473
|
* relevance and zero of them means no relevance at all.
|
|
@@ -393,7 +491,7 @@ export const interestsSchema = z
|
|
|
393
491
|
.array(topicSchema)
|
|
394
492
|
.min(INTERESTS_MIN)
|
|
395
493
|
.max(INTERESTS_MAX)
|
|
396
|
-
.refine(
|
|
494
|
+
.refine(eachOnce, {
|
|
397
495
|
error: VALIDATION_MESSAGE.topicsDuplicate,
|
|
398
496
|
});
|
|
399
497
|
// ─── The step keys ───────────────────────────────────────────────────────────
|
|
@@ -438,8 +536,15 @@ export const ONBOARDING_STEP_KEYS = [
|
|
|
438
536
|
* profile; that record is the point.
|
|
439
537
|
*/
|
|
440
538
|
export const GUIDELINES_VERSION = '2026-10-03';
|
|
539
|
+
/**
|
|
540
|
+
* Not agreed at all (the box left unticked) is `guidelinesRequired`; agreed to
|
|
541
|
+
* another text is `guidelinesOutdated` (0.17.0: both were the second, so a
|
|
542
|
+
* client had to word an unticked box itself).
|
|
543
|
+
*/
|
|
441
544
|
export const guidelinesVersionSchema = z.literal(GUIDELINES_VERSION, {
|
|
442
|
-
error:
|
|
545
|
+
error: (issue) => issue.input === undefined || issue.input === null
|
|
546
|
+
? VALIDATION_MESSAGE.guidelinesRequired
|
|
547
|
+
: VALIDATION_MESSAGE.guidelinesOutdated,
|
|
443
548
|
});
|
|
444
549
|
// ─── Contact ─────────────────────────────────────────────────────────────────
|
|
445
550
|
/**
|
|
@@ -454,6 +559,12 @@ export const contactLinksSchema = founderLinksSchema.pick({
|
|
|
454
559
|
});
|
|
455
560
|
/** A team size has at most this many digits: `TEAM_SIZE_MAX` written out. */
|
|
456
561
|
export const TEAM_SIZE_DIGITS_MAX = String(TEAM_SIZE_MAX).length;
|
|
562
|
+
/**
|
|
563
|
+
* A team size as typed, kept to what the field may hold: digits only, at most
|
|
564
|
+
* `TEAM_SIZE_DIGITS_MAX` (0.17.0: the draft reader and every client's field
|
|
565
|
+
* clean it with this one rule).
|
|
566
|
+
*/
|
|
567
|
+
export const teamSizeDigits = (raw) => raw.replace(/\D/g, '').slice(0, TEAM_SIZE_DIGITS_MAX);
|
|
457
568
|
// ─── The form, mid-flight: the draft each step saves ──────────────────────────
|
|
458
569
|
/**
|
|
459
570
|
* The answers so far, keyed by the finish body's own field names: what each
|
|
@@ -477,24 +588,35 @@ export const TEAM_SIZE_DIGITS_MAX = String(TEAM_SIZE_MAX).length;
|
|
|
477
588
|
* one, wrote it, and the server stores values opaquely. Read it with
|
|
478
589
|
* `readOnboardingDraft`, never by casting.
|
|
479
590
|
*/
|
|
591
|
+
/** Longest date of birth an answer may be: `YYYY-MM-DD` (0.18.0: exported; the form and the draft reader use it). */
|
|
592
|
+
export const DATE_INPUT_MAX = 10;
|
|
593
|
+
/** Longest phone number an answer may be, as typed with spaces and a `+91`. */
|
|
594
|
+
export const PHONE_INPUT_MAX = 20;
|
|
595
|
+
/** Longest guidelines version an answer may carry (`GUIDELINES_VERSION` is a date). */
|
|
596
|
+
export const GUIDELINES_VERSION_MAX = 32;
|
|
480
597
|
export const onboardingFormSchema = z.object({
|
|
481
598
|
displayName: z.string().max(DISPLAY_NAME_MAX),
|
|
482
599
|
gender: genderSchema.nullable(),
|
|
483
600
|
/** `YYYY-MM-DD` once complete; whatever has been typed until then. */
|
|
484
|
-
dateOfBirth: z.string(),
|
|
601
|
+
dateOfBirth: z.string().max(DATE_INPUT_MAX),
|
|
485
602
|
headline: z.string().max(HEADLINE_MAX),
|
|
486
603
|
about: z.string().max(ABOUT_MAX),
|
|
487
604
|
/**
|
|
488
|
-
* The completed upload's id (`media`, purpose `avatar`)
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
605
|
+
* The completed upload's id (`media`, purpose `avatar`), or `null` for no
|
|
606
|
+
* photo.
|
|
607
|
+
*
|
|
608
|
+
* SAVED BY THE PHOTO STEP, KEPT BY THE SERVER, NOT IN THE DRAFT (0.17.0). A
|
|
609
|
+
* step save carrying this key hands the upload to the server, which holds it
|
|
610
|
+
* for the draft (so the unattached sweep, `MEDIA_UNATTACHED_TTL_HOURS`, never
|
|
611
|
+
* deletes it) and lets go of the one it replaces; `null` lets go of it. It
|
|
612
|
+
* comes back as the status's `photo`, read from what the server holds, never
|
|
613
|
+
* from a stored draft: `readOnboardingDraft` still gives `null` here, so an
|
|
614
|
+
* id an older client left in a draft is never trusted.
|
|
493
615
|
*/
|
|
494
616
|
avatarMediaId: objectIdSchema.nullable(),
|
|
495
617
|
skills: z.array(skillSchema).max(SKILLS_MAX),
|
|
496
618
|
/** As typed; the finish body takes `phoneSchema`'s ten digits. */
|
|
497
|
-
phone: z.string(),
|
|
619
|
+
phone: z.string().max(PHONE_INPUT_MAX),
|
|
498
620
|
/** As typed; the finish body takes full `https://` URLs. */
|
|
499
621
|
links: z.object({
|
|
500
622
|
linkedin: z.string().max(LINK_URL_MAX),
|
|
@@ -526,13 +648,9 @@ export const onboardingFormSchema = z.object({
|
|
|
526
648
|
businessType: businessTypeSchema.nullable(),
|
|
527
649
|
legalStructure: legalStructureSchema.nullable(),
|
|
528
650
|
/** The guidelines text the member agreed to, or `null`. */
|
|
529
|
-
guidelinesVersion: z.string().nullable(),
|
|
651
|
+
guidelinesVersion: z.string().max(GUIDELINES_VERSION_MAX).nullable(),
|
|
530
652
|
});
|
|
531
653
|
// ─── Reading a stored draft back ─────────────────────────────────────────────
|
|
532
|
-
/** Longest date of birth worth keeping from a draft: `YYYY-MM-DD`. */
|
|
533
|
-
const DATE_INPUT_MAX = 10;
|
|
534
|
-
/** Longest phone number worth keeping from a draft, as typed with spaces. */
|
|
535
|
-
const PHONE_INPUT_MAX = 20;
|
|
536
654
|
/** Text, cut to its limit; anything that is not text is "not answered". */
|
|
537
655
|
const draftText = (max) => z
|
|
538
656
|
.string()
|
|
@@ -587,10 +705,7 @@ const draftReaderSchema = z.object({
|
|
|
587
705
|
website: draftText(LINK_URL_MAX),
|
|
588
706
|
industry: draftChoice(INDUSTRY_VALUES),
|
|
589
707
|
stage: draftChoice(STARTUP_STAGE_VALUES),
|
|
590
|
-
teamSize: z
|
|
591
|
-
.string()
|
|
592
|
-
.catch('')
|
|
593
|
-
.transform((v) => v.replace(/\D/g, '').slice(0, TEAM_SIZE_DIGITS_MAX)),
|
|
708
|
+
teamSize: z.string().catch('').transform(teamSizeDigits),
|
|
594
709
|
}).transform((venture) => venture ?? { ...EMPTY_ONBOARDING_VENTURE }),
|
|
595
710
|
businessType: draftChoice(BUSINESS_TYPE_VALUES),
|
|
596
711
|
legalStructure: draftChoice(LEGAL_STRUCTURE_VALUES),
|
|
@@ -615,8 +730,8 @@ export const EMPTY_ONBOARDING_VENTURE = {
|
|
|
615
730
|
* one that does not fit becomes "not answered" (`''`, `null`, `[]`) and the
|
|
616
731
|
* rest of the draft survives. Never coerced into a different answer: a value
|
|
617
732
|
* outside a vocabulary is dropped, not mapped to the nearest one. A photo
|
|
618
|
-
* is never restored (
|
|
619
|
-
* CURRENT guidelines.
|
|
733
|
+
* is never restored from here (it comes back as the status's `photo`; see
|
|
734
|
+
* `avatarMediaId`), and agreement only counts for the CURRENT guidelines.
|
|
620
735
|
*
|
|
621
736
|
* It fills in no answer the member did not give: a blank country stays blank.
|
|
622
737
|
* Whatever a screen shows by default is that screen's choice.
|
|
@@ -627,23 +742,12 @@ export function readOnboardingDraft(draft) {
|
|
|
627
742
|
}
|
|
628
743
|
// ─── The finalize body ───────────────────────────────────────────────────────
|
|
629
744
|
/**
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
* every member who skipped the question was recorded as O-positive: a real
|
|
635
|
-
* answer, indistinguishable from one somebody gave, sitting on a screen staff
|
|
636
|
-
* would read in an emergency. Absent has to stay absent.
|
|
637
|
-
*
|
|
638
|
-
* ⚠️ AND THIS IS NOT THE GATE EITHER. The server re-checks the age and refuses
|
|
639
|
-
* at finalize. `isOnboarded` may be set here and nowhere else: four of the
|
|
640
|
-
* profile's partial indexes are gated on it, so a half-built profile carrying
|
|
641
|
-
* it true appears in Discover half-built.
|
|
642
|
-
*
|
|
643
|
-
* Location, venture and links are the profile's own shapes, not copies: the
|
|
644
|
-
* copies are how the profile model came to lack Instagram.
|
|
745
|
+
* The finish body's fields, before the rules that span fields: what each
|
|
746
|
+
* step's slice is PICKED from (Zod 4 refuses `.pick` on a refined object).
|
|
747
|
+
* Everything outside this file reads `completeOnboardingBodySchema`, whose
|
|
748
|
+
* note below says what the body is.
|
|
645
749
|
*/
|
|
646
|
-
|
|
750
|
+
const completeOnboardingBodyFields = z.object({
|
|
647
751
|
// Plain bounds, no custom message: `too_small` with minimum 1 already says
|
|
648
752
|
// "required", and the client words it in the member's language.
|
|
649
753
|
displayName: displayNameSchema,
|
|
@@ -674,11 +778,16 @@ export const completeOnboardingBodySchema = z.object({
|
|
|
674
778
|
avatarMediaId: objectIdSchema.optional(),
|
|
675
779
|
phone: phoneSchema,
|
|
676
780
|
links: contactLinksSchema.optional(),
|
|
677
|
-
/**
|
|
678
|
-
|
|
679
|
-
|
|
781
|
+
/**
|
|
782
|
+
* Absent when the member skipped the step or refused the permission. In
|
|
783
|
+
* India, one of its states (0.17.0: `indiaLocationSchema`, the rule the
|
|
784
|
+
* screen always showed).
|
|
785
|
+
*/
|
|
786
|
+
location: indiaLocationSchema.optional(),
|
|
787
|
+
/** The shared list rule (each goal once, 0.17.0) plus finish's minimum. */
|
|
788
|
+
goals: requiredGoalListSchema,
|
|
680
789
|
interests: interestsSchema,
|
|
681
|
-
skills:
|
|
790
|
+
skills: skillListSchema.optional(),
|
|
682
791
|
/**
|
|
683
792
|
* Absent when the member skipped the business step. The profile's venture
|
|
684
793
|
* without `foundedYear`, which onboarding does not ask; the Me tab does.
|
|
@@ -698,6 +807,37 @@ export const completeOnboardingBodySchema = z.object({
|
|
|
698
807
|
*/
|
|
699
808
|
guidelinesVersion: guidelinesVersionSchema,
|
|
700
809
|
});
|
|
810
|
+
const typeHasVenture = (body) => body.venture !== undefined || body.businessType === undefined;
|
|
811
|
+
const registrationHasVenture = (body) => body.venture !== undefined || body.legalStructure === undefined;
|
|
812
|
+
const TYPE_NEEDS_VENTURE = { error: VALIDATION_MESSAGE.ventureRequired, path: ['businessType'] };
|
|
813
|
+
const REGISTRATION_NEEDS_VENTURE = {
|
|
814
|
+
error: VALIDATION_MESSAGE.ventureRequired,
|
|
815
|
+
path: ['legalStructure'],
|
|
816
|
+
};
|
|
817
|
+
/**
|
|
818
|
+
* What the client sends to finish. Completed answers only.
|
|
819
|
+
*
|
|
820
|
+
* ⚠️ EVERY OPTIONAL FIELD HERE MUST BE ABSENT WHEN UNANSWERED, never a default
|
|
821
|
+
* standing in for one. The sibling gym product sent `bloodGroup ?? 'O+'`, so
|
|
822
|
+
* every member who skipped the question was recorded as O-positive: a real
|
|
823
|
+
* answer, indistinguishable from one somebody gave, sitting on a screen staff
|
|
824
|
+
* would read in an emergency. Absent has to stay absent.
|
|
825
|
+
*
|
|
826
|
+
* ⚠️ AND THIS IS NOT THE GATE EITHER. The server re-checks the age and refuses
|
|
827
|
+
* at finalize. `isOnboarded` may be set here and nowhere else: four of the
|
|
828
|
+
* profile's partial indexes are gated on it, so a half-built profile carrying
|
|
829
|
+
* it true appears in Discover half-built.
|
|
830
|
+
*
|
|
831
|
+
* Location, venture and links are the profile's own shapes, not copies: the
|
|
832
|
+
* copies are how the profile model came to lack Instagram.
|
|
833
|
+
*
|
|
834
|
+
* Refined (the business type and registration need a venture, above), so Zod 4
|
|
835
|
+
* throws on `.pick` / `.omit` / `.partial` / `.merge` of it: read a field with
|
|
836
|
+
* `.shape.<field>`, a step's with `ONBOARDING_STEP_SCHEMAS.<step>`.
|
|
837
|
+
*/
|
|
838
|
+
export const completeOnboardingBodySchema = completeOnboardingBodyFields
|
|
839
|
+
.refine(typeHasVenture, TYPE_NEEDS_VENTURE)
|
|
840
|
+
.refine(registrationHasVenture, REGISTRATION_NEEDS_VENTURE);
|
|
701
841
|
// ─── The steps ───────────────────────────────────────────────────────────────
|
|
702
842
|
/**
|
|
703
843
|
* Each step's slice of the finish body, picked FROM it, so a client validating
|
|
@@ -717,32 +857,35 @@ export const ONBOARDING_STEP_SCHEMAS = {
|
|
|
717
857
|
// 0.16.0: the old `identity` step, split three ways by the user. The basics
|
|
718
858
|
// are the step's only required answers; the photo and the professional
|
|
719
859
|
// details may each be passed over.
|
|
720
|
-
basics:
|
|
721
|
-
photo:
|
|
722
|
-
professional:
|
|
860
|
+
basics: completeOnboardingBodyFields.pick({ displayName: true, gender: true, dateOfBirth: true }),
|
|
861
|
+
photo: completeOnboardingBodyFields.pick({ avatarMediaId: true }),
|
|
862
|
+
professional: completeOnboardingBodyFields.pick({
|
|
723
863
|
headline: true,
|
|
724
864
|
about: true,
|
|
725
865
|
// Ruling D8 folded the design's "title, achievements, skills" screen into
|
|
726
866
|
// the profile's first questions; it now has a step of its own again.
|
|
727
867
|
skills: true,
|
|
728
868
|
}),
|
|
729
|
-
contact:
|
|
730
|
-
location:
|
|
731
|
-
goals:
|
|
732
|
-
interests:
|
|
733
|
-
business:
|
|
869
|
+
contact: completeOnboardingBodyFields.pick({ phone: true, links: true }),
|
|
870
|
+
location: completeOnboardingBodyFields.pick({ location: true }),
|
|
871
|
+
goals: completeOnboardingBodyFields.pick({ goals: true }),
|
|
872
|
+
interests: completeOnboardingBodyFields.pick({ interests: true }),
|
|
873
|
+
business: completeOnboardingBodyFields
|
|
874
|
+
.pick({
|
|
734
875
|
venture: true,
|
|
735
876
|
businessType: true,
|
|
736
877
|
legalStructure: true,
|
|
737
|
-
})
|
|
738
|
-
|
|
878
|
+
})
|
|
879
|
+
.refine(typeHasVenture, TYPE_NEEDS_VENTURE)
|
|
880
|
+
.refine(registrationHasVenture, REGISTRATION_NEEDS_VENTURE),
|
|
881
|
+
guidelines: completeOnboardingBodyFields.pick({ guidelinesVersion: true }),
|
|
739
882
|
};
|
|
740
883
|
export const FOUNDER_ONBOARDING_STEPS = ONBOARDING_STEP_KEYS.map((key) => {
|
|
741
884
|
const slice = ONBOARDING_STEP_SCHEMAS[key];
|
|
742
885
|
return {
|
|
743
886
|
key,
|
|
887
|
+
labelKey: `onboarding.steps.${key}.label`,
|
|
744
888
|
titleKey: `onboarding.steps.${key}.title`,
|
|
745
|
-
helpKey: `onboarding.steps.${key}.help`,
|
|
746
889
|
fields: Object.keys(slice.shape),
|
|
747
890
|
required: !slice.safeParse({}).success,
|
|
748
891
|
};
|
|
@@ -752,8 +895,8 @@ export const ONBOARDING_TOTAL_STEPS = FOUNDER_ONBOARDING_STEPS.length;
|
|
|
752
895
|
export const ONBOARDING_STEP_FIELDS = FOUNDER_ONBOARDING_STEPS.map((s) => s.fields);
|
|
753
896
|
/** What finishing answers: the new profile's id and the handle derived for it. */
|
|
754
897
|
export const completeOnboardingResponseSchema = z.object({
|
|
755
|
-
id:
|
|
756
|
-
username:
|
|
898
|
+
id: objectIdSchema,
|
|
899
|
+
username: usernameSchema,
|
|
757
900
|
});
|
|
758
901
|
// ─── The resume cursor ───────────────────────────────────────────────────────
|
|
759
902
|
/**
|
|
@@ -775,26 +918,36 @@ export const ONBOARDING_DRAFT_KEYS = onboardingDraftKeySchema.options;
|
|
|
775
918
|
* pair: one number is what lets a member start on the web and resume elsewhere,
|
|
776
919
|
* provided both ends agree on the encoding.
|
|
777
920
|
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
921
|
+
* What is READ BACK: the draft's KEYS come from the form, its VALUES are
|
|
922
|
+
* opaque, since a stored draft may predate 0.18.0 (any JSON was stored then);
|
|
923
|
+
* read it through `readOnboardingDraft`, never by casting. What is WRITTEN is
|
|
924
|
+
* the form's own shape (`onboardingProgressUpdateSchema.draft`, 0.18.0), and
|
|
925
|
+
* everything is re-validated strictly at finalize.
|
|
781
926
|
*
|
|
782
927
|
* `partialRecord`, not `record`: in Zod 4 an enum-keyed `z.record` demands
|
|
783
928
|
* every key, and a draft holds whichever steps the member has reached.
|
|
784
929
|
*/
|
|
785
930
|
export const onboardingDraftSchema = z.partialRecord(onboardingDraftKeySchema, z.unknown());
|
|
786
|
-
export const onboardingProgressSchema = z.object({
|
|
787
|
-
step: z.number().int().min(0).max(ONBOARDING_TOTAL_STEPS),
|
|
788
|
-
draft: onboardingDraftSchema.optional(),
|
|
789
|
-
});
|
|
790
931
|
/**
|
|
791
932
|
* `PATCH` body: any subset of the cursor, merged server-side. No defaults.
|
|
792
933
|
* `step` may equal `ONBOARDING_TOTAL_STEPS`: "past the last step", which the
|
|
793
934
|
* client sends on its way to finish.
|
|
935
|
+
*
|
|
936
|
+
* The photo step's `avatarMediaId` is not kept in the draft (0.17.0): it is
|
|
937
|
+
* handed to the server, which holds that upload for the member.
|
|
794
938
|
*/
|
|
795
939
|
export const onboardingProgressUpdateSchema = z.object({
|
|
796
940
|
step: z.number().int().min(0).max(ONBOARDING_TOTAL_STEPS).optional(),
|
|
797
|
-
|
|
941
|
+
/**
|
|
942
|
+
* The answers of the step being saved, IN THE FORM'S OWN SHAPE (0.18.0):
|
|
943
|
+
* half-answered is fine (that is the form: text as typed, `null` for "not
|
|
944
|
+
* yet"), but every value has its type and its bound, and no other key is
|
|
945
|
+
* taken. Before 0.18.0 any JSON up to the body limit was stored as sent,
|
|
946
|
+
* for 90 days, and sent back. The photo's id reads as `objectIdSchema` reads
|
|
947
|
+
* every id (lower case), since the server compares it with the upload it
|
|
948
|
+
* holds.
|
|
949
|
+
*/
|
|
950
|
+
draft: onboardingFormSchema.partial().strict().optional(),
|
|
798
951
|
});
|
|
799
952
|
/**
|
|
800
953
|
* What the client reads on return, from `GET` and from every `PATCH`.
|
|
@@ -808,5 +961,13 @@ export const onboardingStatusSchema = z.object({
|
|
|
808
961
|
onboarded: z.boolean(),
|
|
809
962
|
step: z.number().int().min(0).max(ONBOARDING_TOTAL_STEPS),
|
|
810
963
|
draft: onboardingDraftSchema.optional(),
|
|
964
|
+
/**
|
|
965
|
+
* The photo the member saved on the photo step, as it will appear (0.17.0):
|
|
966
|
+
* the upload the server holds for this member's draft. Absent when they
|
|
967
|
+
* saved none, removed it, or have finished. A client resumes the photo step
|
|
968
|
+
* from this (its `id` is the finish body's `avatarMediaId`), never from the
|
|
969
|
+
* draft.
|
|
970
|
+
*/
|
|
971
|
+
photo: mediaImageSchema.optional(),
|
|
811
972
|
});
|
|
812
973
|
//# sourceMappingURL=onboarding.js.map
|