@opendoor/partner-sdk-client-js-core 1.0.6-beta.55.1 → 1.0.7-beta.56.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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * DtcOnboardingEngine — framework-agnostic state machine for the
3
+ * DTC onboarding questionnaire flow.
4
+ *
5
+ * The engine manages answers, navigation (with page skipping for conditional
6
+ * pages), validation, and event emission. Framework wrappers (React, Vue)
7
+ * subscribe to events and mirror state into their reactive systems.
8
+ *
9
+ * Navigation is tracked by **page ID**, not index. This ensures correct
10
+ * behavior when answers change page visibility mid-flow.
11
+ */
12
+ import type { AnswerValue, EngineEvent, PageConfig, QuestionConfig, ValidationResult } from './types.js';
13
+ export interface DtcOnboardingOptions {
14
+ /** Market identifier for market-dependent questions (e.g., eligibility criteria). */
15
+ market?: string;
16
+ /** Pre-filled answers (e.g., from address lookup or prior session). */
17
+ initialAnswers?: Record<string, AnswerValue>;
18
+ }
19
+ export declare class DtcOnboardingEngine {
20
+ private _allPages;
21
+ private _answers;
22
+ private _currentPageId;
23
+ private _validationErrors;
24
+ private _submitted;
25
+ private _listeners;
26
+ private _market;
27
+ constructor(pages: PageConfig[], options?: DtcOnboardingOptions);
28
+ /** Pages visible given current answers (filtered by showWhen conditions). */
29
+ get visiblePages(): PageConfig[];
30
+ /** The current page, or undefined if no pages are visible. */
31
+ get currentPage(): PageConfig | undefined;
32
+ /** Index of the current page within visible pages. */
33
+ get currentPageIndex(): number;
34
+ /** Progress as a fraction (0 to 1). 0 on first page, 1 on last page. */
35
+ get progress(): number;
36
+ get isFirstPage(): boolean;
37
+ get isLastPage(): boolean;
38
+ /** Whether the engine has already emitted a submit event. */
39
+ get submitted(): boolean;
40
+ get answers(): Readonly<Record<string, AnswerValue>>;
41
+ get validationErrors(): Readonly<Record<string, string>>;
42
+ get market(): string | undefined;
43
+ /**
44
+ * Get a single answer value. Arrow function so it can be passed directly
45
+ * to evaluateCondition without binding.
46
+ */
47
+ getAnswer: (key: string) => AnswerValue;
48
+ /**
49
+ * Set an answer value. Clears any validation error for that field.
50
+ */
51
+ setAnswer(key: string, value: AnswerValue): void;
52
+ /**
53
+ * Set multiple answers at once (e.g., for initial data prefill).
54
+ * Emits a single answerChange event for the last key.
55
+ */
56
+ setAnswers(answers: Record<string, AnswerValue>): void;
57
+ /**
58
+ * Validate current page and advance to the next visible page.
59
+ * If on the last page, emits 'submit' instead of advancing.
60
+ * No-ops if the engine has already submitted.
61
+ * Returns validation result (callers can check errors).
62
+ */
63
+ goToNext(): ValidationResult;
64
+ /**
65
+ * Navigate to the previous visible page. No validation on backward navigation.
66
+ */
67
+ goToPrev(): void;
68
+ /**
69
+ * Jump to a specific page index (within visible pages).
70
+ */
71
+ goToPage(index: number): void;
72
+ /**
73
+ * Validate the current page without advancing.
74
+ */
75
+ validate(): ValidationResult;
76
+ /**
77
+ * Get visible questions on the current page (filtered by showWhen).
78
+ */
79
+ getVisibleQuestionsForCurrentPage(): QuestionConfig[];
80
+ /**
81
+ * Subscribe to an engine event. Returns an unsubscribe function.
82
+ */
83
+ on(eventType: EngineEvent['type'], handler: (event: EngineEvent) => void): () => void;
84
+ /**
85
+ * Remove all listeners and clean up. Call on component unmount.
86
+ */
87
+ destroy(): void;
88
+ private emit;
89
+ /**
90
+ * If the current page became hidden (answer change removed it from visible pages),
91
+ * auto-correct to the nearest visible page. Emits pageChange if corrected.
92
+ */
93
+ private recoverIfCurrentPageHidden;
94
+ }
95
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/internal/questionnaire/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,UAAU,EACV,cAAc,EACd,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAOpB,MAAM,WAAW,oBAAoB;IACnC,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CAC9C;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,iBAAiB,CAA8B;IACvD,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,UAAU,CAGd;IACJ,OAAO,CAAC,OAAO,CAAqB;gBAExB,KAAK,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,EAAE,oBAAoB;IAa/D,6EAA6E;IAC7E,IAAI,YAAY,IAAI,UAAU,EAAE,CAE/B;IAED,8DAA8D;IAC9D,IAAI,WAAW,IAAI,UAAU,GAAG,SAAS,CAExC;IAED,sDAAsD;IACtD,IAAI,gBAAgB,IAAI,MAAM,CAK7B;IAED,wEAAwE;IACxE,IAAI,QAAQ,IAAI,MAAM,CAKrB;IAED,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,IAAI,UAAU,IAAI,OAAO,CAGxB;IAED,6DAA6D;IAC7D,IAAI,SAAS,IAAI,OAAO,CAEvB;IAID,IAAI,OAAO,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAEnD;IAED,IAAI,gBAAgB,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAEvD;IAED,IAAI,MAAM,IAAI,MAAM,GAAG,SAAS,CAE/B;IAED;;;OAGG;IACH,SAAS,GAAI,KAAK,MAAM,KAAG,WAAW,CAEpC;IAIF;;OAEG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI;IAahD;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAI;IAsBtD;;;;;OAKG;IACH,QAAQ,IAAI,gBAAgB;IA0C5B;;OAEG;IACH,QAAQ,IAAI,IAAI;IAehB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAc7B;;OAEG;IACH,QAAQ,IAAI,gBAAgB;IAQ5B;;OAEG;IACH,iCAAiC,IAAI,cAAc,EAAE;IAQrD;;OAEG;IACH,EAAE,CACA,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,EAC9B,OAAO,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GACpC,MAAM,IAAI;IAWb;;OAEG;IACH,OAAO,IAAI,IAAI;IAMf,OAAO,CAAC,IAAI;IASZ;;;OAGG;IACH,OAAO,CAAC,0BAA0B;CAwBnC"}
@@ -0,0 +1,260 @@
1
+ /**
2
+ * DtcOnboardingEngine — framework-agnostic state machine for the
3
+ * DTC onboarding questionnaire flow.
4
+ *
5
+ * The engine manages answers, navigation (with page skipping for conditional
6
+ * pages), validation, and event emission. Framework wrappers (React, Vue)
7
+ * subscribe to events and mirror state into their reactive systems.
8
+ *
9
+ * Navigation is tracked by **page ID**, not index. This ensures correct
10
+ * behavior when answers change page visibility mid-flow.
11
+ */
12
+ import { getVisiblePages, getVisibleQuestions, validatePage, } from './formLogic.js';
13
+ export class DtcOnboardingEngine {
14
+ constructor(pages, options) {
15
+ this._validationErrors = {};
16
+ this._submitted = false;
17
+ this._listeners = new Map();
18
+ /**
19
+ * Get a single answer value. Arrow function so it can be passed directly
20
+ * to evaluateCondition without binding.
21
+ */
22
+ this.getAnswer = (key) => {
23
+ return this._answers[key];
24
+ };
25
+ this._allPages = pages;
26
+ this._answers = options?.initialAnswers
27
+ ? { ...options.initialAnswers }
28
+ : {};
29
+ this._market = options?.market;
30
+ // Initialize to first visible page
31
+ const visible = this.visiblePages;
32
+ this._currentPageId = visible.length > 0 ? visible[0].id : '';
33
+ }
34
+ // ── Derived state (recomputed on access) ──────────────────────────────────
35
+ /** Pages visible given current answers (filtered by showWhen conditions). */
36
+ get visiblePages() {
37
+ return getVisiblePages(this._allPages, this.getAnswer);
38
+ }
39
+ /** The current page, or undefined if no pages are visible. */
40
+ get currentPage() {
41
+ return this.visiblePages.find((p) => p.id === this._currentPageId);
42
+ }
43
+ /** Index of the current page within visible pages. */
44
+ get currentPageIndex() {
45
+ const idx = this.visiblePages.findIndex((p) => p.id === this._currentPageId);
46
+ return idx >= 0 ? idx : 0;
47
+ }
48
+ /** Progress as a fraction (0 to 1). 0 on first page, 1 on last page. */
49
+ get progress() {
50
+ const pages = this.visiblePages;
51
+ if (pages.length <= 1)
52
+ return 0;
53
+ const idx = this.currentPageIndex;
54
+ return Math.min(1, Math.max(0, idx / (pages.length - 1)));
55
+ }
56
+ get isFirstPage() {
57
+ return this.currentPageIndex === 0;
58
+ }
59
+ get isLastPage() {
60
+ const pages = this.visiblePages;
61
+ return pages.length > 0 && this.currentPageIndex === pages.length - 1;
62
+ }
63
+ /** Whether the engine has already emitted a submit event. */
64
+ get submitted() {
65
+ return this._submitted;
66
+ }
67
+ // ── State accessors ───────────────────────────────────────────────────────
68
+ get answers() {
69
+ return { ...this._answers };
70
+ }
71
+ get validationErrors() {
72
+ return { ...this._validationErrors };
73
+ }
74
+ get market() {
75
+ return this._market;
76
+ }
77
+ // ── Actions ───────────────────────────────────────────────────────────────
78
+ /**
79
+ * Set an answer value. Clears any validation error for that field.
80
+ */
81
+ setAnswer(key, value) {
82
+ this._answers = { ...this._answers, [key]: value };
83
+ // Clear validation error for this field
84
+ if (this._validationErrors[key]) {
85
+ const { [key]: _, ...rest } = this._validationErrors;
86
+ this._validationErrors = rest;
87
+ }
88
+ this.emit({ type: 'answerChange', key, value });
89
+ this.recoverIfCurrentPageHidden();
90
+ }
91
+ /**
92
+ * Set multiple answers at once (e.g., for initial data prefill).
93
+ * Emits a single answerChange event for the last key.
94
+ */
95
+ setAnswers(answers) {
96
+ this._answers = { ...this._answers, ...answers };
97
+ // Clear validation errors for all set keys
98
+ for (const key of Object.keys(answers)) {
99
+ if (this._validationErrors[key]) {
100
+ const { [key]: _, ...rest } = this._validationErrors;
101
+ this._validationErrors = rest;
102
+ }
103
+ }
104
+ // Emit for the batch — listeners should read engine.answers for full state
105
+ const keys = Object.keys(answers);
106
+ if (keys.length > 0) {
107
+ const lastKey = keys[keys.length - 1];
108
+ this.emit({
109
+ type: 'answerChange',
110
+ key: lastKey,
111
+ value: answers[lastKey],
112
+ });
113
+ }
114
+ this.recoverIfCurrentPageHidden();
115
+ }
116
+ /**
117
+ * Validate current page and advance to the next visible page.
118
+ * If on the last page, emits 'submit' instead of advancing.
119
+ * No-ops if the engine has already submitted.
120
+ * Returns validation result (callers can check errors).
121
+ */
122
+ goToNext() {
123
+ if (this._submitted) {
124
+ return { valid: true, errors: {} };
125
+ }
126
+ const page = this.currentPage;
127
+ if (!page) {
128
+ return { valid: false, errors: { _page: 'No current page' } };
129
+ }
130
+ const result = validatePage(page, this.getAnswer);
131
+ if (!result.valid) {
132
+ this._validationErrors = result.errors;
133
+ this.emit({ type: 'validationError', errors: result.errors });
134
+ return result;
135
+ }
136
+ // Clear errors on successful validation
137
+ this._validationErrors = {};
138
+ if (this.isLastPage) {
139
+ this._submitted = true;
140
+ this.emit({ type: 'submit', answers: { ...this._answers } });
141
+ return result;
142
+ }
143
+ // Advance to next visible page by finding current position and moving forward
144
+ const visible = this.visiblePages;
145
+ const currentIdx = visible.findIndex((p) => p.id === this._currentPageId);
146
+ if (currentIdx >= 0 && currentIdx < visible.length - 1) {
147
+ this._currentPageId = visible[currentIdx + 1].id;
148
+ }
149
+ this.emit({
150
+ type: 'pageChange',
151
+ pageIndex: this.currentPageIndex,
152
+ pageId: this._currentPageId,
153
+ });
154
+ return result;
155
+ }
156
+ /**
157
+ * Navigate to the previous visible page. No validation on backward navigation.
158
+ */
159
+ goToPrev() {
160
+ const visible = this.visiblePages;
161
+ const currentIdx = visible.findIndex((p) => p.id === this._currentPageId);
162
+ if (currentIdx > 0) {
163
+ this._currentPageId = visible[currentIdx - 1].id;
164
+ this._validationErrors = {};
165
+ this.emit({
166
+ type: 'pageChange',
167
+ pageIndex: this.currentPageIndex,
168
+ pageId: this._currentPageId,
169
+ });
170
+ }
171
+ }
172
+ /**
173
+ * Jump to a specific page index (within visible pages).
174
+ */
175
+ goToPage(index) {
176
+ const visible = this.visiblePages;
177
+ if (index >= 0 && index < visible.length) {
178
+ this._currentPageId = visible[index].id;
179
+ this._validationErrors = {};
180
+ this.emit({
181
+ type: 'pageChange',
182
+ pageIndex: index,
183
+ pageId: this._currentPageId,
184
+ });
185
+ }
186
+ }
187
+ /**
188
+ * Validate the current page without advancing.
189
+ */
190
+ validate() {
191
+ const page = this.currentPage;
192
+ if (!page) {
193
+ return { valid: false, errors: { _page: 'No current page' } };
194
+ }
195
+ return validatePage(page, this.getAnswer);
196
+ }
197
+ /**
198
+ * Get visible questions on the current page (filtered by showWhen).
199
+ */
200
+ getVisibleQuestionsForCurrentPage() {
201
+ const page = this.currentPage;
202
+ if (!page)
203
+ return [];
204
+ return getVisibleQuestions(page, this.getAnswer);
205
+ }
206
+ // ── Events ────────────────────────────────────────────────────────────────
207
+ /**
208
+ * Subscribe to an engine event. Returns an unsubscribe function.
209
+ */
210
+ on(eventType, handler) {
211
+ if (!this._listeners.has(eventType)) {
212
+ this._listeners.set(eventType, new Set());
213
+ }
214
+ this._listeners.get(eventType).add(handler);
215
+ return () => {
216
+ this._listeners.get(eventType)?.delete(handler);
217
+ };
218
+ }
219
+ /**
220
+ * Remove all listeners and clean up. Call on component unmount.
221
+ */
222
+ destroy() {
223
+ this._listeners.clear();
224
+ }
225
+ // ── Private ───────────────────────────────────────────────────────────────
226
+ emit(event) {
227
+ const handlers = this._listeners.get(event.type);
228
+ if (handlers) {
229
+ for (const handler of handlers) {
230
+ handler(event);
231
+ }
232
+ }
233
+ }
234
+ /**
235
+ * If the current page became hidden (answer change removed it from visible pages),
236
+ * auto-correct to the nearest visible page. Emits pageChange if corrected.
237
+ */
238
+ recoverIfCurrentPageHidden() {
239
+ const visible = this.visiblePages;
240
+ if (visible.length === 0)
241
+ return;
242
+ // If current page is still visible, nothing to do
243
+ if (visible.some((p) => p.id === this._currentPageId))
244
+ return;
245
+ // Find the closest visible page (prefer staying near the same position)
246
+ const oldIdx = this._allPages.findIndex((p) => p.id === this._currentPageId);
247
+ let bestPage = visible[0];
248
+ for (const page of visible) {
249
+ const pageIdx = this._allPages.findIndex((p) => p.id === page.id);
250
+ if (pageIdx <= oldIdx)
251
+ bestPage = page;
252
+ }
253
+ this._currentPageId = bestPage.id;
254
+ this.emit({
255
+ type: 'pageChange',
256
+ pageIndex: this.currentPageIndex,
257
+ pageId: this._currentPageId,
258
+ });
259
+ }
260
+ }
@@ -0,0 +1,12 @@
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
+ import type { PageConfig } from '../types.js';
11
+ export declare const dtcOnboardingPages: PageConfig[];
12
+ //# sourceMappingURL=dtcOnboarding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dtcOnboarding.d.ts","sourceRoot":"","sources":["../../../../src/internal/questionnaire/flows/dtcOnboarding.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA+qB9C,eAAO,MAAM,kBAAkB,EAAE,UAAU,EAkB1C,CAAC"}