@opendoor/partner-sdk-client-js-core 1.0.6 → 1.0.7-beta.57.1
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/dist/appearance.d.ts +8 -0
- package/dist/appearance.d.ts.map +1 -1
- package/dist/appearance.js +5 -0
- package/dist/internal/questionnaire/engine.d.ts +95 -0
- package/dist/internal/questionnaire/engine.d.ts.map +1 -0
- package/dist/internal/questionnaire/engine.js +260 -0
- package/dist/internal/questionnaire/flows/dtcOnboarding.d.ts +12 -0
- package/dist/internal/questionnaire/flows/dtcOnboarding.d.ts.map +1 -0
- package/dist/internal/questionnaire/flows/dtcOnboarding.js +648 -0
- package/dist/internal/questionnaire/formLogic.d.ts +32 -0
- package/dist/internal/questionnaire/formLogic.d.ts.map +1 -0
- package/dist/internal/questionnaire/formLogic.js +122 -0
- package/dist/internal/questionnaire/index.d.ts +21 -0
- package/dist/internal/questionnaire/index.d.ts.map +1 -0
- package/dist/internal/questionnaire/index.js +24 -0
- package/dist/internal/questionnaire/types.d.ts +106 -0
- package/dist/internal/questionnaire/types.d.ts.map +1 -0
- package/dist/internal/questionnaire/types.js +16 -0
- package/package.json +5 -1
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DTC Onboarding Flow — 17-page questionnaire definition.
|
|
3
|
+
*
|
|
4
|
+
* Page order matches the Figma design with additions from the DTC production flow.
|
|
5
|
+
* Answer keys follow reception-fe conventions (dot-notation matching answerKeyMapping.ts).
|
|
6
|
+
*
|
|
7
|
+
* Images are referenced by imageId strings. The framework layer (React/Vue) provides
|
|
8
|
+
* an image manifest that resolves imageIds to bundler-resolved URLs.
|
|
9
|
+
*/
|
|
10
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
11
|
+
function validateEmail(value) {
|
|
12
|
+
if (typeof value !== 'string')
|
|
13
|
+
return undefined;
|
|
14
|
+
if (!EMAIL_REGEX.test(value))
|
|
15
|
+
return 'Please enter a valid email address';
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
function validatePhone(value) {
|
|
19
|
+
if (typeof value !== 'string')
|
|
20
|
+
return undefined;
|
|
21
|
+
const digits = value.replace(/\D/g, '');
|
|
22
|
+
if (digits.length < 10)
|
|
23
|
+
return 'Please enter a valid 10-digit phone number';
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
// ── Page 1: Confirm Home Details ──────────────────────────────────────────────
|
|
27
|
+
const confirmHomeDetailsPage = {
|
|
28
|
+
id: 'confirm-home-details',
|
|
29
|
+
title: 'Confirm your home details',
|
|
30
|
+
subtitle: "This info is available to the public. You can edit these details if you've made any updates to your home.",
|
|
31
|
+
questions: [
|
|
32
|
+
{
|
|
33
|
+
key: 'home.dwelling_type',
|
|
34
|
+
label: 'Home type',
|
|
35
|
+
type: 'string',
|
|
36
|
+
style: 'dropdown',
|
|
37
|
+
required: true,
|
|
38
|
+
options: [
|
|
39
|
+
{ label: 'Single family home', value: 'single_family' },
|
|
40
|
+
{ label: 'Townhouse', value: 'townhouse' },
|
|
41
|
+
{ label: 'Apartment / Condo', value: 'apartment' },
|
|
42
|
+
{ label: 'Mobile home', value: 'mobile_home' },
|
|
43
|
+
{ label: 'Multi-family', value: 'multi_family' },
|
|
44
|
+
{ label: 'Commercial', value: 'commercial' },
|
|
45
|
+
{ label: 'Vacant land', value: 'vacant' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'home.bedrooms',
|
|
50
|
+
label: 'Bedrooms',
|
|
51
|
+
description: 'Must have a closet and a window/vent',
|
|
52
|
+
type: 'number',
|
|
53
|
+
style: 'dropdown',
|
|
54
|
+
required: true,
|
|
55
|
+
options: Array.from({ length: 12 }, (_, i) => ({
|
|
56
|
+
label: String(i + 1),
|
|
57
|
+
value: i + 1,
|
|
58
|
+
})),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: 'home.bathrooms.full',
|
|
62
|
+
label: 'Full bathrooms',
|
|
63
|
+
description: 'Must have a toilet and a shower/tub',
|
|
64
|
+
type: 'number',
|
|
65
|
+
style: 'dropdown',
|
|
66
|
+
required: true,
|
|
67
|
+
options: Array.from({ length: 10 }, (_, i) => ({
|
|
68
|
+
label: String(i + 1),
|
|
69
|
+
value: i + 1,
|
|
70
|
+
})),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: 'home.bathrooms.half',
|
|
74
|
+
label: 'Partial bathrooms',
|
|
75
|
+
description: 'No shower or tub required',
|
|
76
|
+
type: 'number',
|
|
77
|
+
style: 'dropdown',
|
|
78
|
+
required: false,
|
|
79
|
+
options: Array.from({ length: 11 }, (_, i) => ({
|
|
80
|
+
label: String(i),
|
|
81
|
+
value: i,
|
|
82
|
+
})),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: 'home.exterior_stories',
|
|
86
|
+
label: 'Floors (above ground)',
|
|
87
|
+
description: 'Only count full & liveable floors',
|
|
88
|
+
type: 'number',
|
|
89
|
+
style: 'dropdown',
|
|
90
|
+
required: true,
|
|
91
|
+
options: [
|
|
92
|
+
{ label: '1', value: 1 },
|
|
93
|
+
{ label: '2', value: 2 },
|
|
94
|
+
{ label: '3', value: 3 },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: 'home.above_grade_sq_ft',
|
|
99
|
+
label: 'Square footage (above ground)',
|
|
100
|
+
description: "Don't include the basement, any porches or additions, or non-liveable space/footage.",
|
|
101
|
+
type: 'number',
|
|
102
|
+
style: 'input',
|
|
103
|
+
required: true,
|
|
104
|
+
min: 1,
|
|
105
|
+
suffix: 'sq. ft.',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: 'home.has_basement',
|
|
109
|
+
label: 'Basement',
|
|
110
|
+
type: 'string',
|
|
111
|
+
style: 'dropdown',
|
|
112
|
+
required: true,
|
|
113
|
+
options: [
|
|
114
|
+
{ label: 'Yes', value: 'yes' },
|
|
115
|
+
{ label: 'No', value: 'no' },
|
|
116
|
+
],
|
|
117
|
+
showWhen: {
|
|
118
|
+
and: [
|
|
119
|
+
{ field: 'home.dwelling_type', op: 'exists' },
|
|
120
|
+
{
|
|
121
|
+
field: 'home.dwelling_type',
|
|
122
|
+
op: 'notIn',
|
|
123
|
+
value: ['apartment', 'mobile_home'],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
key: 'home.basement_finished_sq_ft',
|
|
130
|
+
label: 'Finished basement sq. ft.',
|
|
131
|
+
description: 'Finished liveable area that is completely below ground.',
|
|
132
|
+
type: 'number',
|
|
133
|
+
style: 'input',
|
|
134
|
+
required: false,
|
|
135
|
+
min: 0,
|
|
136
|
+
max: 10000,
|
|
137
|
+
suffix: 'sq. ft.',
|
|
138
|
+
showWhen: { field: 'home.has_basement', op: 'eq', value: 'yes' },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
key: 'home.basement_unfinished_sq_ft',
|
|
142
|
+
label: 'Unfinished basement sq. ft.',
|
|
143
|
+
description: 'Unfinished area that is completely below ground.',
|
|
144
|
+
type: 'number',
|
|
145
|
+
style: 'input',
|
|
146
|
+
required: false,
|
|
147
|
+
min: 0,
|
|
148
|
+
max: 10000,
|
|
149
|
+
suffix: 'sq. ft.',
|
|
150
|
+
showWhen: { field: 'home.has_basement', op: 'eq', value: 'yes' },
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
key: 'home.year_built',
|
|
154
|
+
label: 'Year built',
|
|
155
|
+
type: 'number',
|
|
156
|
+
style: 'dropdown',
|
|
157
|
+
required: true,
|
|
158
|
+
options: Array.from({ length: 150 }, (_, i) => {
|
|
159
|
+
const year = new Date().getFullYear() - i;
|
|
160
|
+
return { label: String(year), value: year };
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
key: 'home.pool_type',
|
|
165
|
+
label: 'Pool',
|
|
166
|
+
type: 'string',
|
|
167
|
+
style: 'dropdown',
|
|
168
|
+
required: true,
|
|
169
|
+
options: [
|
|
170
|
+
{ label: 'In-ground pool', value: 'in_ground' },
|
|
171
|
+
{ label: 'Above-ground pool', value: 'above_ground' },
|
|
172
|
+
{ label: 'Community pool', value: 'community' },
|
|
173
|
+
{ label: 'No pool', value: 'none' },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: 'home.covered_parking_type',
|
|
178
|
+
label: 'Covered parking',
|
|
179
|
+
type: 'string',
|
|
180
|
+
style: 'dropdown',
|
|
181
|
+
required: false,
|
|
182
|
+
options: [
|
|
183
|
+
{ label: 'Garage', value: 'garage' },
|
|
184
|
+
{ label: 'Carport', value: 'carport' },
|
|
185
|
+
{ label: 'None', value: 'none' },
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
key: 'home.garage_spaces',
|
|
190
|
+
label: 'Parking spaces',
|
|
191
|
+
type: 'number',
|
|
192
|
+
style: 'dropdown',
|
|
193
|
+
required: false,
|
|
194
|
+
options: Array.from({ length: 6 }, (_, i) => ({
|
|
195
|
+
label: String(i),
|
|
196
|
+
value: i,
|
|
197
|
+
})),
|
|
198
|
+
showWhen: {
|
|
199
|
+
and: [
|
|
200
|
+
{ field: 'home.covered_parking_type', op: 'exists' },
|
|
201
|
+
{ field: 'home.covered_parking_type', op: 'neq', value: 'none' },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
key: 'home.entry_type',
|
|
207
|
+
label: 'Entry type',
|
|
208
|
+
type: 'string',
|
|
209
|
+
style: 'dropdown',
|
|
210
|
+
required: true,
|
|
211
|
+
options: [
|
|
212
|
+
{ label: 'Direct entry', value: 'direct_entry' },
|
|
213
|
+
{ label: 'Shared entrance', value: 'shared_entrance_condo' },
|
|
214
|
+
],
|
|
215
|
+
showWhen: {
|
|
216
|
+
field: 'home.dwelling_type',
|
|
217
|
+
op: 'in',
|
|
218
|
+
value: ['apartment', 'townhouse'],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
// ── Page 2: Ownership ─────────────────────────────────────────────────────────
|
|
224
|
+
const ownershipPage = {
|
|
225
|
+
id: 'ownership',
|
|
226
|
+
title: 'Are you the owner of this home?',
|
|
227
|
+
subtitle: "We have additional questions if you're an agent.",
|
|
228
|
+
questions: [
|
|
229
|
+
{
|
|
230
|
+
key: 'seller.relation_to_owner',
|
|
231
|
+
label: 'Relationship to owner',
|
|
232
|
+
type: 'string',
|
|
233
|
+
style: 'radio-card',
|
|
234
|
+
required: true,
|
|
235
|
+
// No autoAdvance — "Other" option reveals a followup text input on this page
|
|
236
|
+
options: [
|
|
237
|
+
{ label: 'Yes, I own this home', value: 'Self' },
|
|
238
|
+
{ label: "No, I'm an agent", value: 'Agent' },
|
|
239
|
+
{ label: "I'm an agent, and own this home", value: 'AgentAndOwner' },
|
|
240
|
+
{ label: 'Other', value: 'other' },
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
key: 'seller.relation_to_owner.other',
|
|
245
|
+
label: 'Please specify',
|
|
246
|
+
type: 'string',
|
|
247
|
+
style: 'input',
|
|
248
|
+
required: true,
|
|
249
|
+
placeholder: 'Your relationship to the owner',
|
|
250
|
+
showWhen: { field: 'seller.relation_to_owner', op: 'eq', value: 'other' },
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
// ── Page 3: Sale Timeline ─────────────────────────────────────────────────────
|
|
255
|
+
const saleTimelinePage = {
|
|
256
|
+
id: 'sale-timeline',
|
|
257
|
+
title: 'When do you need to sell your home?',
|
|
258
|
+
subtitle: "This won't affect your offer. We're here to help with any timeline.",
|
|
259
|
+
questions: [
|
|
260
|
+
{
|
|
261
|
+
key: 'seller.sale_timeline',
|
|
262
|
+
label: 'Sale timeline',
|
|
263
|
+
type: 'string',
|
|
264
|
+
style: 'radio-card',
|
|
265
|
+
required: true,
|
|
266
|
+
autoAdvance: true,
|
|
267
|
+
options: [
|
|
268
|
+
{ label: 'ASAP', value: 'ASAP' },
|
|
269
|
+
{ label: '2-4 weeks', value: '2 - 4 Weeks' },
|
|
270
|
+
{ label: '4-6 weeks', value: '4 - 6 Weeks' },
|
|
271
|
+
{ label: '6+ weeks', value: '6+ Weeks' },
|
|
272
|
+
{ label: 'Just browsing', value: 'Just Browsing' },
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
// ── Pages 4-7: Room Condition ─────────────────────────────────────────────────
|
|
278
|
+
const CONDITION_LEVELS = [
|
|
279
|
+
'fixer_upper',
|
|
280
|
+
'dated',
|
|
281
|
+
'standard',
|
|
282
|
+
'high_end',
|
|
283
|
+
'luxury',
|
|
284
|
+
];
|
|
285
|
+
const CONDITION_DESCRIPTIONS = {
|
|
286
|
+
kitchen: {
|
|
287
|
+
fixer_upper: 'Kitchen needs significant repairs',
|
|
288
|
+
dated: "Kitchen hasn't been updated recently",
|
|
289
|
+
standard: 'Kitchen is updated with common finishes',
|
|
290
|
+
high_end: 'Kitchen has high-quality upgrades',
|
|
291
|
+
luxury: 'Kitchen has elegant, top-tier finishes',
|
|
292
|
+
},
|
|
293
|
+
bathroom: {
|
|
294
|
+
fixer_upper: 'Bathroom needs significant repairs',
|
|
295
|
+
dated: "Bathroom hasn't been updated recently",
|
|
296
|
+
standard: 'Bathroom is updated with common finishes',
|
|
297
|
+
high_end: 'Bathroom has high-quality upgrades',
|
|
298
|
+
luxury: 'Bathroom has elegant, top-tier finishes',
|
|
299
|
+
},
|
|
300
|
+
'living-room': {
|
|
301
|
+
fixer_upper: 'Living room needs significant repairs',
|
|
302
|
+
dated: "Living room hasn't been updated recently",
|
|
303
|
+
standard: 'Living room is updated with common finishes',
|
|
304
|
+
high_end: 'Living room has high-quality upgrades',
|
|
305
|
+
luxury: 'Living room has elegant, top-tier finishes',
|
|
306
|
+
},
|
|
307
|
+
exterior: {
|
|
308
|
+
fixer_upper: 'Exterior needs significant repairs',
|
|
309
|
+
dated: "Exterior hasn't been updated recently",
|
|
310
|
+
standard: 'Exterior is updated with common finishes',
|
|
311
|
+
high_end: 'Exterior has high-quality upgrades',
|
|
312
|
+
luxury: 'Exterior has elegant, top-tier finishes',
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
const CONDITION_LABELS = {
|
|
316
|
+
fixer_upper: 'Fixer upper',
|
|
317
|
+
dated: 'Dated',
|
|
318
|
+
standard: 'Standard',
|
|
319
|
+
high_end: 'High-end',
|
|
320
|
+
luxury: 'Luxury',
|
|
321
|
+
};
|
|
322
|
+
function createConditionPage(room, answerKey, title) {
|
|
323
|
+
return {
|
|
324
|
+
id: `${room}-condition`,
|
|
325
|
+
title,
|
|
326
|
+
subtitle: 'For these questions, just select the closest match.',
|
|
327
|
+
questions: [
|
|
328
|
+
{
|
|
329
|
+
key: answerKey,
|
|
330
|
+
label: title,
|
|
331
|
+
type: 'string',
|
|
332
|
+
style: 'image-card',
|
|
333
|
+
required: true,
|
|
334
|
+
autoAdvance: true,
|
|
335
|
+
options: CONDITION_LEVELS.map((level) => ({
|
|
336
|
+
label: CONDITION_LABELS[level],
|
|
337
|
+
value: level,
|
|
338
|
+
description: CONDITION_DESCRIPTIONS[room][level],
|
|
339
|
+
imageId: `${room}-${level}`,
|
|
340
|
+
})),
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const kitchenConditionPage = createConditionPage('kitchen', 'home.kitchen_seller_score', 'How would you describe your kitchen?');
|
|
346
|
+
const bathroomConditionPage = createConditionPage('bathroom', 'home.bathroom_seller_score', 'How would you describe your main bathroom?');
|
|
347
|
+
const livingRoomConditionPage = createConditionPage('living-room', 'home.living_room_seller_score', 'How would you describe your living room?');
|
|
348
|
+
const exteriorConditionPage = createConditionPage('exterior', 'home.exterior_seller_score', "How would you describe your home's exterior?");
|
|
349
|
+
// ── Page 8: HOA ───────────────────────────────────────────────────────────────
|
|
350
|
+
const hoaPage = {
|
|
351
|
+
id: 'hoa',
|
|
352
|
+
title: 'Is your home part of a homeowners association?',
|
|
353
|
+
subtitle: "This is often called an HOA. It's a group that helps maintain your community for a fee.",
|
|
354
|
+
questions: [
|
|
355
|
+
{
|
|
356
|
+
key: 'home.hoa',
|
|
357
|
+
label: 'HOA',
|
|
358
|
+
type: 'string',
|
|
359
|
+
style: 'radio-card',
|
|
360
|
+
required: true,
|
|
361
|
+
autoAdvance: true,
|
|
362
|
+
options: [
|
|
363
|
+
{ label: 'Yes', value: 'yes' },
|
|
364
|
+
{ label: 'No', value: 'no' },
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
};
|
|
369
|
+
// ── Page 9: HOA Type ──────────────────────────────────────────────────────────
|
|
370
|
+
const hoaTypePage = {
|
|
371
|
+
id: 'hoa-type',
|
|
372
|
+
title: 'Does your home belong to any of these types of communities?',
|
|
373
|
+
showWhen: { field: 'home.hoa', op: 'eq', value: 'yes' },
|
|
374
|
+
questions: [
|
|
375
|
+
{
|
|
376
|
+
key: 'home.hoa_type',
|
|
377
|
+
label: 'Community type',
|
|
378
|
+
type: 'string[]',
|
|
379
|
+
style: 'checkbox-card',
|
|
380
|
+
required: true,
|
|
381
|
+
options: [
|
|
382
|
+
{
|
|
383
|
+
label: 'Age restricted community',
|
|
384
|
+
value: 'age_restricted_community',
|
|
385
|
+
},
|
|
386
|
+
{ label: 'Gated community', value: 'gated_community' },
|
|
387
|
+
{ label: 'None of the above', value: 'none', exclusive: true },
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
};
|
|
392
|
+
// ── Page 10: HOA Guard ────────────────────────────────────────────────────────
|
|
393
|
+
const hoaGuardPage = {
|
|
394
|
+
id: 'hoa-guard',
|
|
395
|
+
title: 'Is there a guard at the entrance?',
|
|
396
|
+
showWhen: {
|
|
397
|
+
and: [
|
|
398
|
+
{ field: 'home.hoa', op: 'eq', value: 'yes' },
|
|
399
|
+
{ field: 'home.hoa_type', op: 'contains', value: 'gated_community' },
|
|
400
|
+
],
|
|
401
|
+
},
|
|
402
|
+
questions: [
|
|
403
|
+
{
|
|
404
|
+
key: 'home.hoa_type.guarded_gated_community',
|
|
405
|
+
label: 'Guard at entrance',
|
|
406
|
+
type: 'string',
|
|
407
|
+
style: 'radio-card',
|
|
408
|
+
required: true,
|
|
409
|
+
autoAdvance: true,
|
|
410
|
+
options: [
|
|
411
|
+
{ label: 'Yes', value: 'has_guard' },
|
|
412
|
+
{ label: 'No', value: 'no_guard' },
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
// ── Page 11: HOA Fees ─────────────────────────────────────────────────────────
|
|
418
|
+
const hoaFeesPage = {
|
|
419
|
+
id: 'hoa-fees',
|
|
420
|
+
title: 'What are your monthly HOA fees?',
|
|
421
|
+
subtitle: "(Optional) This helps us better understand your property's monthly expenses.",
|
|
422
|
+
showWhen: { field: 'home.hoa', op: 'eq', value: 'yes' },
|
|
423
|
+
questions: [
|
|
424
|
+
{
|
|
425
|
+
key: 'home.hoa_fees',
|
|
426
|
+
label: 'Monthly HOA fees',
|
|
427
|
+
type: 'number',
|
|
428
|
+
style: 'currency',
|
|
429
|
+
required: false,
|
|
430
|
+
min: 0,
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
};
|
|
434
|
+
// ── Page 12: Eligibility Criteria ─────────────────────────────────────────────
|
|
435
|
+
const eligibilityCriteriaPage = {
|
|
436
|
+
id: 'eligibility-criteria',
|
|
437
|
+
title: 'Do any of these apply to your home?',
|
|
438
|
+
subtitle: 'Select all that apply. We keep an eye out for these things when making an offer.',
|
|
439
|
+
questions: [
|
|
440
|
+
{
|
|
441
|
+
key: 'home.eligibility_criteria',
|
|
442
|
+
label: 'Eligibility criteria',
|
|
443
|
+
type: 'string[]',
|
|
444
|
+
style: 'checkbox-card',
|
|
445
|
+
required: false,
|
|
446
|
+
options: [
|
|
447
|
+
{
|
|
448
|
+
label: 'Leased or financed solar panels',
|
|
449
|
+
value: 'leased_solar_panels',
|
|
450
|
+
helperText: 'You may need to buy out the lease or remove the panels to sell to us.',
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
label: 'Known foundation issues',
|
|
454
|
+
value: 'known_foundation_issues',
|
|
455
|
+
helperText: 'Concrete cracking, uneven floors.',
|
|
456
|
+
},
|
|
457
|
+
{ label: 'Fire damage', value: 'fire_damage' },
|
|
458
|
+
{
|
|
459
|
+
label: 'Well water',
|
|
460
|
+
value: 'well_water',
|
|
461
|
+
helperText: 'You maintain a well to supply water.',
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
label: 'Septic system',
|
|
465
|
+
value: 'septic',
|
|
466
|
+
helperText: 'Separate from municipal sewage.',
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
label: 'Unique ownership structure',
|
|
470
|
+
value: 'unique_ownership_structure',
|
|
471
|
+
helperText: 'Tenants in Common, Co-Ops, etc.',
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
label: 'Part of a Below Market Rate (BMR) Ownership Program',
|
|
475
|
+
value: 'below_market_rate_ownership',
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
label: 'Rent controlled and has a tenant today',
|
|
479
|
+
value: 'rent_controlled_tenant_occupied',
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
label: 'Mobile or manufactured home',
|
|
483
|
+
value: 'mobile_manufactured_home',
|
|
484
|
+
helperText: 'Includes homes on a permanent foundation.',
|
|
485
|
+
},
|
|
486
|
+
{ label: 'None of the above', value: 'none', exclusive: true },
|
|
487
|
+
],
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
};
|
|
491
|
+
// ── Page 13: Upgrades ─────────────────────────────────────────────────────────
|
|
492
|
+
const upgradesPage = {
|
|
493
|
+
id: 'upgrades',
|
|
494
|
+
title: 'Has your home had any upgrades?',
|
|
495
|
+
subtitle: 'This includes renovations, remodels, or major improvements.',
|
|
496
|
+
questions: [
|
|
497
|
+
{
|
|
498
|
+
key: 'home.has_upgrades',
|
|
499
|
+
label: 'Home upgrades',
|
|
500
|
+
type: 'string',
|
|
501
|
+
style: 'radio-card',
|
|
502
|
+
required: true,
|
|
503
|
+
autoAdvance: true,
|
|
504
|
+
options: [
|
|
505
|
+
{ label: 'Yes', value: 'yes' },
|
|
506
|
+
{ label: 'No', value: 'no' },
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
};
|
|
511
|
+
// ── Page 14: Homebuilder ──────────────────────────────────────────────────────
|
|
512
|
+
const homebuilderPage = {
|
|
513
|
+
id: 'homebuilder',
|
|
514
|
+
title: 'Are you working with a homebuilder?',
|
|
515
|
+
subtitle: 'We can work with them to line up your close dates.',
|
|
516
|
+
questions: [
|
|
517
|
+
{
|
|
518
|
+
key: 'seller.working_with_home_builder',
|
|
519
|
+
label: 'Working with homebuilder',
|
|
520
|
+
type: 'string',
|
|
521
|
+
style: 'radio-card',
|
|
522
|
+
required: true,
|
|
523
|
+
autoAdvance: true,
|
|
524
|
+
options: [
|
|
525
|
+
{ label: 'Yes', value: 'true' },
|
|
526
|
+
{ label: 'No', value: 'false' },
|
|
527
|
+
],
|
|
528
|
+
},
|
|
529
|
+
],
|
|
530
|
+
};
|
|
531
|
+
// ── Page 15: Homebuilder Name ─────────────────────────────────────────────────
|
|
532
|
+
const homebuilderNamePage = {
|
|
533
|
+
id: 'homebuilder-name',
|
|
534
|
+
title: 'Which homebuilder are you working with?',
|
|
535
|
+
showWhen: {
|
|
536
|
+
field: 'seller.working_with_home_builder',
|
|
537
|
+
op: 'eq',
|
|
538
|
+
value: 'true',
|
|
539
|
+
},
|
|
540
|
+
questions: [
|
|
541
|
+
{
|
|
542
|
+
key: 'seller.home_builder',
|
|
543
|
+
label: 'Homebuilder',
|
|
544
|
+
type: 'string',
|
|
545
|
+
style: 'searchable-dropdown',
|
|
546
|
+
required: false,
|
|
547
|
+
dataSource: 'homebuilders',
|
|
548
|
+
placeholder: 'Select or search...',
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
};
|
|
552
|
+
// ── Page 16: Homebuilder Details ──────────────────────────────────────────────
|
|
553
|
+
const homebuilderDetailsPage = {
|
|
554
|
+
id: 'homebuilder-details',
|
|
555
|
+
title: 'Please tell us more about your homebuilder',
|
|
556
|
+
showWhen: {
|
|
557
|
+
field: 'seller.working_with_home_builder',
|
|
558
|
+
op: 'eq',
|
|
559
|
+
value: 'true',
|
|
560
|
+
},
|
|
561
|
+
questions: [
|
|
562
|
+
{
|
|
563
|
+
key: 'seller.home_builder_email',
|
|
564
|
+
label: 'Sales associate email (Optional)',
|
|
565
|
+
type: 'string',
|
|
566
|
+
style: 'email',
|
|
567
|
+
required: false,
|
|
568
|
+
placeholder: 'you@example.com',
|
|
569
|
+
validate: validateEmail,
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
key: 'seller.home_builder_community',
|
|
573
|
+
label: "Homebuilder's community name (Optional)",
|
|
574
|
+
type: 'string',
|
|
575
|
+
style: 'input',
|
|
576
|
+
required: false,
|
|
577
|
+
placeholder: 'Community name',
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
};
|
|
581
|
+
// ── Page 17: Entry Type (standalone page for non-SFH) ─────────────────────────
|
|
582
|
+
// Note: Entry type is also on confirm-home-details as a conditional field.
|
|
583
|
+
// This standalone page exists for flows where dwelling type is set after
|
|
584
|
+
// confirm-home-details (e.g., from address data). Normally the inline field
|
|
585
|
+
// on confirm-home-details handles this. Kept here for completeness — can be
|
|
586
|
+
// conditionally included based on partner flow configuration.
|
|
587
|
+
// ── Page 18: Contact Info ─────────────────────────────────────────────────────
|
|
588
|
+
const contactInfoPage = {
|
|
589
|
+
id: 'contact-info',
|
|
590
|
+
title: "You're one step from your offer.",
|
|
591
|
+
subtitle: 'Create a free account to see your offer. No commitment to sell — just your number, email, and name.',
|
|
592
|
+
submitLabel: 'See my offer',
|
|
593
|
+
questions: [
|
|
594
|
+
{
|
|
595
|
+
key: 'seller.full_name',
|
|
596
|
+
label: 'Full name',
|
|
597
|
+
type: 'string',
|
|
598
|
+
style: 'input',
|
|
599
|
+
required: true,
|
|
600
|
+
placeholder: 'Full name',
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
key: 'seller.email',
|
|
604
|
+
label: 'Email address',
|
|
605
|
+
type: 'string',
|
|
606
|
+
style: 'email',
|
|
607
|
+
required: true,
|
|
608
|
+
placeholder: 'you@example.com',
|
|
609
|
+
validate: validateEmail,
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
key: 'seller.phone_number',
|
|
613
|
+
label: 'Phone number',
|
|
614
|
+
type: 'string',
|
|
615
|
+
style: 'phone',
|
|
616
|
+
required: true,
|
|
617
|
+
placeholder: '(555) 555-5555',
|
|
618
|
+
validate: validatePhone,
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
key: 'seller.sms_opt_in',
|
|
622
|
+
label: 'I consent to receive marketing calls and texts from Opendoor and its affiliates to the number provided. I understand that I may opt out at any time and consent is not a condition of purchase.',
|
|
623
|
+
type: 'boolean',
|
|
624
|
+
style: 'toggle',
|
|
625
|
+
required: false,
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
};
|
|
629
|
+
// ── Complete DTC Onboarding Flow ──────────────────────────────────────────────
|
|
630
|
+
export const dtcOnboardingPages = [
|
|
631
|
+
confirmHomeDetailsPage,
|
|
632
|
+
ownershipPage,
|
|
633
|
+
saleTimelinePage,
|
|
634
|
+
kitchenConditionPage,
|
|
635
|
+
bathroomConditionPage,
|
|
636
|
+
livingRoomConditionPage,
|
|
637
|
+
exteriorConditionPage,
|
|
638
|
+
hoaPage,
|
|
639
|
+
hoaTypePage,
|
|
640
|
+
hoaGuardPage,
|
|
641
|
+
hoaFeesPage,
|
|
642
|
+
eligibilityCriteriaPage,
|
|
643
|
+
upgradesPage,
|
|
644
|
+
homebuilderPage,
|
|
645
|
+
homebuilderNamePage,
|
|
646
|
+
homebuilderDetailsPage,
|
|
647
|
+
contactInfoPage,
|
|
648
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic form logic for the questionnaire engine.
|
|
3
|
+
*
|
|
4
|
+
* Handles condition evaluation, field validation, and page visibility.
|
|
5
|
+
*/
|
|
6
|
+
import type { AnswerValue, Condition, PageConfig, QuestionConfig, ValidationResult } from './types.js';
|
|
7
|
+
type GetAnswer = (key: string) => AnswerValue;
|
|
8
|
+
/**
|
|
9
|
+
* Recursively evaluates a Condition tree against the current answers.
|
|
10
|
+
*/
|
|
11
|
+
export declare function evaluateCondition(condition: Condition, getAnswer: GetAnswer): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the visible questions on a page, filtering out those whose
|
|
14
|
+
* showWhen condition evaluates to false.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getVisibleQuestions(page: PageConfig, getAnswer: GetAnswer): QuestionConfig[];
|
|
17
|
+
/**
|
|
18
|
+
* Returns the visible pages, filtering out those whose showWhen condition
|
|
19
|
+
* evaluates to false given current answers.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getVisiblePages(pages: PageConfig[], getAnswer: GetAnswer): PageConfig[];
|
|
22
|
+
/**
|
|
23
|
+
* Validates a single field. Returns an error message or undefined.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateField(question: QuestionConfig, value: AnswerValue): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Validates all visible questions on a page.
|
|
28
|
+
* Returns a ValidationResult with field-level errors.
|
|
29
|
+
*/
|
|
30
|
+
export declare function validatePage(page: PageConfig, getAnswer: GetAnswer): ValidationResult;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=formLogic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formLogic.d.ts","sourceRoot":"","sources":["../../../src/internal/questionnaire/formLogic.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,UAAU,EACV,cAAc,EACd,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAGpB,KAAK,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,WAAW,CAAC;AAI9C;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,GACnB,OAAO,CA8CT;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,SAAS,GACnB,cAAc,EAAE,CAIlB;AAID;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EAAE,EACnB,SAAS,EAAE,SAAS,GACnB,UAAU,EAAE,CAId;AAID;;GAEG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,WAAW,GACjB,MAAM,GAAG,SAAS,CAuCpB;AAID;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,SAAS,GACnB,gBAAgB,CAgBlB"}
|