@explorer02/cfm-survey-sdk 0.2.0 → 0.2.2
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/cli/index.js +18 -18
- package/dist/cli/index.mjs +14 -14
- package/dist/index.d.mts +465 -580
- package/dist/index.d.ts +465 -580
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +6 -4
- package/postinstall.js +96 -64
- package/templates/AGENT.md +15 -7
- package/templates/docs/00-integration/answer-logic-and-navigation.md +50 -0
- package/templates/docs/00-integration/aws-deploy.md +281 -0
- package/templates/docs/00-integration/client-integration-guide.md +351 -0
- package/templates/docs/00-integration/component-checklist.md +84 -0
- package/templates/docs/00-integration/constraints.md +65 -3
- package/templates/docs/00-integration/custom-field-logic-and-navigation.md +174 -0
- package/templates/docs/00-integration/display-logic-and-navigation.md +49 -0
- package/templates/docs/00-integration/end-page-logic.md +64 -0
- package/templates/docs/00-integration/partial-save-and-recovery.md +74 -0
- package/templates/docs/00-integration/placeholders-and-tokens.md +116 -0
- package/templates/docs/00-integration/progress.md +61 -0
- package/templates/docs/00-integration/question-display-variants.md +65 -0
- package/templates/docs/00-integration/question-numbering.md +51 -0
- package/templates/docs/00-integration/question-type-sdk-matrix.md +242 -0
- package/templates/docs/00-integration/setup.md +84 -0
- package/templates/docs/00-integration/skip-logic-and-navigation.md +80 -0
- package/templates/docs/00-integration/survey-lifecycle-analytics.md +94 -0
- package/templates/docs/00-integration/useSurveySDK.md +151 -23
- package/templates/docs/01-components/01-survey-page.md +76 -54
- package/templates/docs/01-components/02-question.md +77 -89
- package/templates/docs/01-components/03-rating-scale.md +11 -1
- package/templates/docs/01-components/04-star-rating-scale.md +45 -0
- package/templates/docs/01-components/05-csat-matrix-scale.md +20 -11
- package/templates/docs/01-components/06-likert-matrix-scale.md +12 -3
- package/templates/docs/01-components/07-slider-matrix-scale.md +11 -2
- package/templates/docs/01-components/08-file-upload-scale.md +10 -1
- package/templates/docs/01-components/09-custom-slider-track.md +2 -2
- package/templates/docs/01-components/11-progress-bar.md +10 -4
- package/templates/docs/01-components/13-matrix-dropdown.md +3 -1
- package/templates/docs/01-components/14-intro-page.md +34 -0
- package/templates/docs/01-components/15-end-page.md +31 -0
- package/templates/docs/01-components/16-paused-page.md +27 -0
- package/templates/docs/01-components/17-heatmap-scale.md +75 -0
- package/templates/docs/01-components/18-rank-order-scale.md +100 -0
- package/templates/docs/01-components/README.md +44 -36
- package/templates/docs/02-reference/config-field-index.md +141 -139
- package/templates/docs/02-reference/exports.md +149 -0
- package/templates/docs/02-reference/question-types/01-rating.md +31 -32
- package/templates/docs/02-reference/question-types/04-csat.md +51 -42
- package/templates/docs/02-reference/question-types/05-rating-scale.md +16 -11
- package/templates/docs/02-reference/question-types/06-slider.md +2 -2
- package/templates/docs/02-reference/question-types/07-matrix-cfm.md +44 -30
- package/templates/docs/02-reference/question-types/08-matrix-csat.md +4 -26
- package/templates/docs/02-reference/question-types/09-matrix-rating.md +29 -13
- package/templates/docs/02-reference/question-types/10-slider-matrix.md +15 -4
- package/templates/docs/02-reference/question-types/11-file-upload.md +13 -2
- package/templates/docs/02-reference/question-types/12-heatmap.md +59 -0
- package/templates/docs/02-reference/question-types/13-rank-order.md +101 -0
- package/templates/docs/02-reference/question-types/README.md +86 -47
- package/templates/docs/02-reference/routing-table.md +33 -18
- package/templates/docs/02-reference/value-derivation.md +63 -0
- package/templates/docs/03-ui-specs/00-question-shell.md +24 -80
- package/templates/docs/03-ui-specs/01-rating.md +12 -54
- package/templates/docs/03-ui-specs/02-radio.md +21 -33
- package/templates/docs/03-ui-specs/03-text.md +2 -2
- package/templates/docs/03-ui-specs/04-csat.md +38 -33
- package/templates/docs/03-ui-specs/05-rating-scale.md +21 -18
- package/templates/docs/03-ui-specs/06-slider.md +3 -1
- package/templates/docs/03-ui-specs/07-matrix-cfm.md +25 -22
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +18 -46
- package/templates/docs/03-ui-specs/09-slider-matrix.md +2 -2
- package/templates/docs/03-ui-specs/10-file-upload.md +2 -2
- package/templates/docs/03-ui-specs/11-text-and-media.md +29 -29
- package/templates/docs/03-ui-specs/12-survey-chrome.md +20 -14
- package/templates/docs/03-ui-specs/13-heatmap.md +73 -0
- package/templates/docs/03-ui-specs/14-rank-order.md +59 -0
- package/templates/docs/03-ui-specs/README.md +14 -13
- package/templates/docs/03-ui-specs/shared/custom-slider-track.md +3 -3
- package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +6 -1
- package/templates/docs/MANIFEST.json +454 -67
- package/templates/docs/index.md +84 -29
- package/templates/docs/templates/Question.tsx +310 -0
- package/templates/docs/templates/deploy-to-aws.sh +114 -0
- package/templates/docs/templates/heatmapCoords.ts +58 -0
- package/templates/docs/templates/implementation_plan.md +115 -49
- package/templates/docs/templates/survey-inventory.schema.json +72 -0
- package/templates/docs/templates/verify-agent-build.sh +53 -0
- package/templates/docs/01-components/04-csat-scale.md +0 -40
- package/templates/docs/02-question-types/01-rating.md +0 -44
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Answer Logic and Navigation
|
|
2
|
+
|
|
3
|
+
> Integrator-facing behavior contract. Answer logic evaluation is handled inside the SDK — use exported visibility helpers; do not import internal `surveyLogic` paths directly.
|
|
4
|
+
|
|
5
|
+
## Distinction from display logic
|
|
6
|
+
|
|
7
|
+
Skip logic, display logic, and answer logic share the **same condition tree** and **same evaluator** (`evaluateCondition`). They differ by **what** the condition gates:
|
|
8
|
+
|
|
9
|
+
| Kind | API attachment | SDK field | When true |
|
|
10
|
+
|------|----------------|-----------|-----------|
|
|
11
|
+
| `SKIP` | Question `controlConfig.skipLogics` | `question.skipLogics` | Navigate to skip target or end of survey |
|
|
12
|
+
| `DISPLAY` | Question `controlConfig.displayLogic` | `question.displayLogic` | Show the whole question (dynamic or page-level) |
|
|
13
|
+
| `ANSWER` | Sub-entity `controlConfig.displayLogic` | `answerLogic` on options / rows / columns | Show that option, statement row, or scale column |
|
|
14
|
+
|
|
15
|
+
The API uses `displayLogic` on sub-entities (MCQ `answerDetails`, matrix `childSurveyQuestions`, scale `answerDetails` / `matrixColumnQuestions`). The SDK maps these to `answerLogic` to avoid confusion with question-level display logic.
|
|
16
|
+
|
|
17
|
+
## Supported question types
|
|
18
|
+
|
|
19
|
+
| Type | Answer logic on | Not supported |
|
|
20
|
+
|------|-----------------|---------------|
|
|
21
|
+
| MCQ | Each option | — |
|
|
22
|
+
| SLIDER_MATRIX | Each statement row | Scale columns |
|
|
23
|
+
| CSAT_MATRIX | Each statement row + each scale column | — |
|
|
24
|
+
| RATING_MATRIX | Each statement row only | Scale columns |
|
|
25
|
+
| CFM_MATRIX | Each statement row + each scale column | — |
|
|
26
|
+
| RANK_ORDER | Each option | — |
|
|
27
|
+
| NPS_SCALE, TEXTFIELD, FILE_UPLOAD, TEXT_AND_MEDIA | — | No answer logic |
|
|
28
|
+
|
|
29
|
+
## Page breaks
|
|
30
|
+
|
|
31
|
+
Adding answer logic in the survey builder creates a **page break before that question** (backend page structure). The SDK does not split pages at runtime; it filters visible items within the current page.
|
|
32
|
+
|
|
33
|
+
If **every** answer item is hidden (all options, all rows, or all scale columns), the question is treated as not answerable and may disappear from `state.currentQuestions` (same pattern as display logic empty pages).
|
|
34
|
+
|
|
35
|
+
## Client responsibilities
|
|
36
|
+
|
|
37
|
+
1. Use `state.currentQuestions` from `useSurveySDK` — question-level display logic is already applied.
|
|
38
|
+
2. **MCQ:** Filter options with `getVisibleMcqOptionsForAnswers(question, answers, allQuestions, customFieldValues?)`.
|
|
39
|
+
3. **Rank order:** Filter options with `getVisibleRankOrderOptionsForAnswers(question, answers, allQuestions, customFieldValues?)`.
|
|
40
|
+
4. **Matrix (CSAT, CFM, RATING, SLIDER):** Filter rows and (for CSAT/CFM) columns with `getVisibleMatrixItemsForAnswers(question, answers, allQuestions, customFieldValues?)`.
|
|
41
|
+
5. Pass the same `customFieldValues` map used in `useSurveySDK` options when logic rules reference `CUSTOM_FIELD` entities — see [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md).
|
|
42
|
+
6. Wire `onAction({ type: 'CHANGE' })` so answer logic re-evaluates when prior answers change.
|
|
43
|
+
|
|
44
|
+
## Progress and validation
|
|
45
|
+
|
|
46
|
+
- Progress and validation count only **visible** options, statement rows, and scale columns.
|
|
47
|
+
- Hidden selections are stripped automatically on answer change via SDK sanitization.
|
|
48
|
+
- Required questions with zero visible options are not blocked (treated as answered).
|
|
49
|
+
|
|
50
|
+
See also: [`display-logic-and-navigation.md`](display-logic-and-navigation.md), [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md), [`progress.md`](progress.md).
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# AWS Deploy (EC2 Middle API → S3 → CloudFront)
|
|
2
|
+
|
|
3
|
+
> Deploy a **static** survey UI to Sprinklr AWS using the hosted EC2 deploy API.
|
|
4
|
+
> **Do not** embed AWS credentials in client code — the API uses IAM roles on EC2 to presign S3 URLs.
|
|
5
|
+
|
|
6
|
+
Use this doc when the client prompt mentions **AWS**, **CloudFront**, **S3**, **EC2**, or **Sprinklr hosting**.
|
|
7
|
+
For Vercel deploy, use `npx cfm-sdk deploy` instead (see `index.md` Phase 7).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Architecture — Three-Channel Flow
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
sequenceDiagram
|
|
15
|
+
participant Dev as Agent_DevMachine
|
|
16
|
+
participant API as EC2_Deploy_API
|
|
17
|
+
participant S3 as Hosting_Bucket
|
|
18
|
+
participant CF as CloudFront
|
|
19
|
+
|
|
20
|
+
Dev->>Dev: yarn build or npm run build
|
|
21
|
+
Dev->>API: POST /deploy/start + JWT + file manifest
|
|
22
|
+
API-->>Dev: deployId + presigned PUT URLs
|
|
23
|
+
loop each file in out/
|
|
24
|
+
Dev->>S3: PUT file direct
|
|
25
|
+
end
|
|
26
|
+
Dev->>API: POST /deploy/complete
|
|
27
|
+
API->>S3: HeadObject index.html
|
|
28
|
+
API->>CF: CreateInvalidation
|
|
29
|
+
API-->>Dev: public HTTPS URL
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Channel | Direction | Action |
|
|
33
|
+
|---------|-----------|--------|
|
|
34
|
+
| 1 Control | Dev → EC2 API | `POST /deploy/start` with JWT + file manifest |
|
|
35
|
+
| 2 Data | Dev → S3 direct | `PUT` each static file to presigned URLs |
|
|
36
|
+
| 3 Finalize | Dev → EC2 API | `POST /deploy/complete` → CloudFront URL |
|
|
37
|
+
|
|
38
|
+
S3 path shape: `sites/{surveyId}/{deployId}/` — extracted from JWT at deploy time.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Prerequisites (verify before deploy)
|
|
43
|
+
|
|
44
|
+
1. **Phase 6 passed** — `npm run build` / `yarn build` exits 0
|
|
45
|
+
2. **JWT alignment** — deploy JWT **must equal** `SURVEY_INSTANCE_ID` in `SurveyPage.tsx`
|
|
46
|
+
3. **Static output exists** — Next.js: `out/index.html`; Vite: `dist/index.html`
|
|
47
|
+
4. **File count ≤ 500** (API limit)
|
|
48
|
+
|
|
49
|
+
### Environment variables (dev machine)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export CFM_INSTANCE_ID="<same JWT as SurveyPage SURVEY_INSTANCE_ID>"
|
|
53
|
+
export CFM_DEPLOY_API_BASE="http://43.204.26.213:3000"
|
|
54
|
+
export OUT_DIR="<absolute path to static output folder>"
|
|
55
|
+
# Next.js example:
|
|
56
|
+
# export OUT_DIR="/path/to/project/out"
|
|
57
|
+
# Vite example:
|
|
58
|
+
# export OUT_DIR="/path/to/project/dist"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Health check
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl -s "$CFM_DEPLOY_API_BASE/health"
|
|
65
|
+
# Expected: {"ok":true,"service":"sprinklr-deploy-api"}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Build Config (apply during Phase 5 BUILD)
|
|
71
|
+
|
|
72
|
+
### Next.js — static export (required for AWS)
|
|
73
|
+
|
|
74
|
+
See also [`setup.md#nextjs-aws-static-export`](setup.md#nextjs-aws-static-export).
|
|
75
|
+
|
|
76
|
+
**`next.config.js`:**
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
/** @type {import('next').NextConfig} */
|
|
80
|
+
const nextConfig = {
|
|
81
|
+
reactStrictMode: true,
|
|
82
|
+
transpilePackages: ['@explorer02/cfm-survey-sdk'], // or '@repo/sdk'
|
|
83
|
+
output: 'export',
|
|
84
|
+
assetPrefix: './', // required for nested CloudFront path
|
|
85
|
+
images: { unoptimized: true },
|
|
86
|
+
};
|
|
87
|
+
module.exports = nextConfig;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**`package.json` build script** — copy RSC payload for `index.html` URLs:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
"build": "next build && cp out/index.txt out/index.html.txt"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| Setting | Why |
|
|
97
|
+
|---------|-----|
|
|
98
|
+
| `output: 'export'` | Produces uploadable `out/` folder (no Node server) |
|
|
99
|
+
| `assetPrefix: './'` | Assets resolve under `/sites/{surveyId}/{deployId}/`, not CloudFront root |
|
|
100
|
+
| `images.unoptimized` | Image optimization needs a server; static export does not |
|
|
101
|
+
| `index.html.txt` copy | Fixes 403 on `index.html.txt?_rsc` when page URL ends with `index.html` |
|
|
102
|
+
|
|
103
|
+
### Vite
|
|
104
|
+
|
|
105
|
+
Set `OUT_DIR` to `dist/`. Ensure asset paths in built HTML are **relative** (no leading `/`). See Vite `base: './'` if needed.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Static Asset Rules (apply during BUILD)
|
|
110
|
+
|
|
111
|
+
| Rule | Detail |
|
|
112
|
+
|------|--------|
|
|
113
|
+
| Public logos | Use relative paths (`logo.svg`), not `/logo.svg` |
|
|
114
|
+
| Rank-order images | Prefer `option.imageUrl` over `option.previewImageUrl` — preview `*_p.*` URLs often return 403 |
|
|
115
|
+
| Footer hash links | Use plain `<a href="#">`, not `next/link` — avoids RSC prefetch on static export |
|
|
116
|
+
| No AWS keys in code | JWT only in `Authorization: Bearer` header at deploy time |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Deploy Procedure (agent executes in order)
|
|
121
|
+
|
|
122
|
+
### Step 0 — Build
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
cd <project-root>
|
|
126
|
+
yarn build # or npm run build
|
|
127
|
+
test -f "$OUT_DIR/index.html" || { echo "Missing index.html"; exit 1; }
|
|
128
|
+
find "$OUT_DIR" -type f | wc -l # must be ≤ 500
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Step 1 — Channel 1: manifest + start
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
python3 <<'PY' > /tmp/deploy-manifest.json
|
|
135
|
+
import json, os, mimetypes
|
|
136
|
+
from pathlib import Path
|
|
137
|
+
out = Path(os.environ["OUT_DIR"])
|
|
138
|
+
files = []
|
|
139
|
+
for path in sorted(out.rglob("*")):
|
|
140
|
+
if not path.is_file():
|
|
141
|
+
continue
|
|
142
|
+
rel = path.relative_to(out).as_posix()
|
|
143
|
+
mime, _ = mimetypes.guess_type(rel)
|
|
144
|
+
files.append({
|
|
145
|
+
"path": rel,
|
|
146
|
+
"contentType": mime or "application/octet-stream",
|
|
147
|
+
"sizeBytes": path.stat().st_size,
|
|
148
|
+
})
|
|
149
|
+
print(json.dumps({"files": files}))
|
|
150
|
+
print(f"files: {len(files)}", file=__import__('sys').stderr)
|
|
151
|
+
PY
|
|
152
|
+
|
|
153
|
+
curl -s -X POST "$CFM_DEPLOY_API_BASE/deploy/start" \
|
|
154
|
+
-H "Authorization: Bearer $CFM_INSTANCE_ID" \
|
|
155
|
+
-H "Content-Type: application/json" \
|
|
156
|
+
-d @/tmp/deploy-manifest.json \
|
|
157
|
+
| tee /tmp/deploy-start.json
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Success:** JSON with `deployId`, `s3Prefix`, `uploads[]`.
|
|
161
|
+
|
|
162
|
+
| Error | Fix |
|
|
163
|
+
|-------|-----|
|
|
164
|
+
| `401 TOKEN_EXPIRED` | Ask client for fresh instanceId JWT |
|
|
165
|
+
| `503 DEPLOY_UNAVAILABLE` | EC2 hosting env misconfigured — report to operator |
|
|
166
|
+
| `400 files must not exceed 500` | Reduce build output |
|
|
167
|
+
|
|
168
|
+
### Step 2 — Channel 2: upload every file to S3
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
python3 <<'PY'
|
|
172
|
+
import json, os, subprocess
|
|
173
|
+
from pathlib import Path
|
|
174
|
+
|
|
175
|
+
out = Path(os.environ["OUT_DIR"])
|
|
176
|
+
start = json.load(open("/tmp/deploy-start.json"))
|
|
177
|
+
manifest = {f["path"]: f["contentType"] for f in json.load(open("/tmp/deploy-manifest.json"))["files"]}
|
|
178
|
+
failed = []
|
|
179
|
+
|
|
180
|
+
for item in start["uploads"]:
|
|
181
|
+
path = item["path"]
|
|
182
|
+
url = item["uploadUrl"]
|
|
183
|
+
mime = manifest.get(path, "application/octet-stream")
|
|
184
|
+
proc = subprocess.run(
|
|
185
|
+
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
|
|
186
|
+
"-X", "PUT", url, "-H", f"Content-Type: {mime}",
|
|
187
|
+
"--data-binary", f"@{out / path}"],
|
|
188
|
+
capture_output=True, text=True,
|
|
189
|
+
)
|
|
190
|
+
code = proc.stdout.strip()
|
|
191
|
+
mark = "OK" if code == "200" else "FAIL"
|
|
192
|
+
print(f"{mark} {code} {path}")
|
|
193
|
+
if code != "200":
|
|
194
|
+
failed.append(path)
|
|
195
|
+
|
|
196
|
+
if failed:
|
|
197
|
+
print(f"\n{len(failed)} failed — rerun from Step 1 (presigned URLs expire)")
|
|
198
|
+
raise SystemExit(1)
|
|
199
|
+
print("\nAll uploads succeeded")
|
|
200
|
+
PY
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Success:** every line shows `OK 200`.
|
|
204
|
+
|
|
205
|
+
### Step 3 — Channel 3: complete + live URL
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
DEPLOY_ID=$(python3 -c "import json; print(json.load(open('/tmp/deploy-start.json'))['deployId'])")
|
|
209
|
+
|
|
210
|
+
curl -s -X POST "$CFM_DEPLOY_API_BASE/deploy/complete" \
|
|
211
|
+
-H "Authorization: Bearer $CFM_INSTANCE_ID" \
|
|
212
|
+
-H "Content-Type: application/json" \
|
|
213
|
+
-d "{\"deployId\":\"$DEPLOY_ID\"}" \
|
|
214
|
+
| tee /tmp/deploy-complete.json
|
|
215
|
+
|
|
216
|
+
python3 -c "import json; print('\nLive URL:\n' + json.load(open('/tmp/deploy-complete.json'))['url'])"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Success:**
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"url": "https://<cloudfront-domain>/sites/{surveyId}/{deployId}/index.html",
|
|
224
|
+
"deployId": "...",
|
|
225
|
+
"surveyId": "..."
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Step 4 — Verify
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
LIVE_URL=$(python3 -c "import json; print(json.load(open('/tmp/deploy-complete.json'))['url'])")
|
|
233
|
+
curl -s -o /dev/null -w "index.html: %{http_code}\n" "$LIVE_URL"
|
|
234
|
+
# Optional: grep one CSS href from HTML and curl it under the deploy prefix — expect 200
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Tell the client to open the printed URL in a browser.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## One-Command Wrapper (optional)
|
|
242
|
+
|
|
243
|
+
After build, from project root:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
export CFM_INSTANCE_ID="<JWT>"
|
|
247
|
+
export CFM_DEPLOY_API_BASE="http://43.204.26.213:3000"
|
|
248
|
+
export OUT_DIR="<absolute path to out/ or dist/>"
|
|
249
|
+
|
|
250
|
+
bash templates/deploy-to-aws.sh
|
|
251
|
+
# or: bash node_modules/@explorer02/cfm-survey-sdk/templates/docs/templates/deploy-to-aws.sh
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Repeat Deploys
|
|
257
|
+
|
|
258
|
+
Each deploy creates a **new** `deployId` folder under `sites/{surveyId}/`. Old deploys remain in S3.
|
|
259
|
+
|
|
260
|
+
After UI changes: rebuild → re-run Steps 1–3 (same `CFM_INSTANCE_ID` if same survey).
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Troubleshooting
|
|
265
|
+
|
|
266
|
+
| Symptom | Cause | Agent fix |
|
|
267
|
+
|---------|-------|-----------|
|
|
268
|
+
| 403 on `/_next/static/...` | Absolute asset paths | Add `assetPrefix: "./"`, rebuild |
|
|
269
|
+
| 403 on `index.html.txt?_rsc` | Missing RSC payload file | Add `cp out/index.txt out/index.html.txt` to build script |
|
|
270
|
+
| 403 on `*_p.png` / `*_p.jpg` images | Preview URL used | Use `option.imageUrl \|\| option.previewImageUrl` in RankOrderScale |
|
|
271
|
+
| 401 `TOKEN_EXPIRED` | JWT expired | Ask client for new instanceId |
|
|
272
|
+
| 503 `DEPLOY_UNAVAILABLE` | EC2 CloudFront/S3 env wrong | Report to operator |
|
|
273
|
+
| Some PUTs 403 | Presigned URLs expired (default 5 min) | Restart from Step 1; complete within window |
|
|
274
|
+
| Blank page, HTML 200 | JS/CSS not uploaded or wrong paths | Re-check Channel 2 logs |
|
|
275
|
+
| Survey API errors in browser | Deploy JWT ≠ SurveyPage JWT | Align `SURVEY_INSTANCE_ID` |
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Future: CLI automation (Phase 7b)
|
|
280
|
+
|
|
281
|
+
`npx cfm-sdk deploy --aws` is **not implemented yet**. Use the manual flow above or `templates/deploy-to-aws.sh` until the CLI ships.
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# Client Integration Guide
|
|
2
|
+
|
|
3
|
+
> **Start here** after Phase 2 (instanceId). Single map of everything the client must wire to integrate all SDK features correctly.
|
|
4
|
+
|
|
5
|
+
Import only from the package root (`@explorer02/cfm-survey-sdk` or `@repo/sdk`). Never from `/src/` paths.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart TB
|
|
11
|
+
subgraph page [SurveyPage orchestrator]
|
|
12
|
+
Hook["useSurveySDK"]
|
|
13
|
+
PhaseRouter["Phase router\nloading / intro / content / end"]
|
|
14
|
+
Lifecycle["SCROLL + RECORD_QUESTION_VIEW effects"]
|
|
15
|
+
end
|
|
16
|
+
subgraph question [Question.tsx dispatcher]
|
|
17
|
+
Dispatch["QUESTION_TYPE switch"]
|
|
18
|
+
Visibility["getVisible*ForAnswers helpers"]
|
|
19
|
+
Placeholders["resolveSurveyQuestionPlaceholders"]
|
|
20
|
+
end
|
|
21
|
+
subgraph scales [Scale components]
|
|
22
|
+
Matrix["LikertMatrixScale / CsatMatrixScale / …"]
|
|
23
|
+
NPS["RatingScale / FileUploadScale / …"]
|
|
24
|
+
end
|
|
25
|
+
Hook --> PhaseRouter
|
|
26
|
+
PhaseRouter --> question
|
|
27
|
+
Dispatch --> scales
|
|
28
|
+
Visibility --> Dispatch
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Layer | File | Responsibility |
|
|
32
|
+
|-------|------|----------------|
|
|
33
|
+
| Orchestrator | `SurveyPage.tsx` | Hook, phase routing, nav buttons, lifecycle effects, pass props to `Question` |
|
|
34
|
+
| Dispatcher | `Question.tsx` | Route on `question.type`; filter options/rows via visibility helpers; placeholder text |
|
|
35
|
+
| Scales | `*Scale.tsx` | Render one question type; store answers via `onSelect` callback |
|
|
36
|
+
|
|
37
|
+
Reference implementation: `apps/client/src/components/SurveyPage.tsx`.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Feature matrix — who wires what
|
|
42
|
+
|
|
43
|
+
| Feature | Client wires | SDK handles |
|
|
44
|
+
|---------|--------------|-------------|
|
|
45
|
+
| Fetch survey | `instanceId`, loading/error UI | `useFetchSurvey`, survey mapping |
|
|
46
|
+
| Submit | `onAction(NEXT/SUBMIT)`, submit loading UI | Validation, API submit, phase → `end`, end-page selection |
|
|
47
|
+
| End page logic | Render `state.specialPage`; pass `customFieldValues` when end pages reference CRM fields | Display-logic evaluation at submit; see [`end-page-logic.md`](end-page-logic.md) |
|
|
48
|
+
| Answers | `onAction(CHANGE)` with `payload.answerValue` | `state.answers`, sanitization on logic change |
|
|
49
|
+
| Skip logic | Nothing (read doc for UX expectations) | Path simulation, skip jumps, answer clearing |
|
|
50
|
+
| Display logic | Use `state.currentQuestions` (pre-filtered) | Question-level visibility, empty-page skip |
|
|
51
|
+
| Question display variants | Render `question.questionText` from `currentQuestions` (no client variant logic) | Conditional variant text via same logic engine |
|
|
52
|
+
| Answer logic UI | `getVisible*ForAnswers` + pass `customFieldValues` | Condition evaluation engine |
|
|
53
|
+
| Custom fields | Stable `customFieldValues` → hook **and** `Question` | Skip/display/validation/progress evaluation |
|
|
54
|
+
| Progress | Bind `state.progressPercentage` | Active-path denominator + earned credits |
|
|
55
|
+
| Question numbering | Render `question.questionNumber` from `currentQuestions` | Active-path ordinals (same denominator as progress); see [`question-numbering.md`](question-numbering.md) |
|
|
56
|
+
| Placeholders | Stable `placeholders` ref + `language` option | Token replacement at map time |
|
|
57
|
+
| Mark as started | `onAction(SCROLL)` once on first scroll | One-time API call (also fires on CHANGE) |
|
|
58
|
+
| Question activity | `onAction(RECORD_QUESTION_VIEW)` via IntersectionObserver | Deduped VIEWED API calls |
|
|
59
|
+
| Partial save | `savePartialResponse: true` option | Debounced partial POST after user interaction — see [`partial-save-and-recovery.md`](partial-save-and-recovery.md) |
|
|
60
|
+
| Placeholders (detail) | Stable ref + `resolveSurveyQuestionPlaceholders` | CRM tokens at map time; answer tokens at render — see [`placeholders-and-tokens.md`](placeholders-and-tokens.md) |
|
|
61
|
+
| Per-type integration | Read [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) before building scales | Answer shapes, visibility helpers, performance notes per type |
|
|
62
|
+
| Validation scroll | `id={question.id}` on question wrapper | Scroll-to-first-error on failed NEXT/SUBMIT |
|
|
63
|
+
|
|
64
|
+
Deep dives: [`useSurveySDK.md`](useSurveySDK.md) · [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) · [`placeholders-and-tokens.md`](placeholders-and-tokens.md) · [`partial-save-and-recovery.md`](partial-save-and-recovery.md) · [`survey-lifecycle-analytics.md`](survey-lifecycle-analytics.md) · [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md) · [`display-logic-and-navigation.md`](display-logic-and-navigation.md) · [`question-display-variants.md`](question-display-variants.md) · [`question-numbering.md`](question-numbering.md) · [`end-page-logic.md`](end-page-logic.md) · [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md) · [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md) · [`progress.md`](progress.md) · [`constraints.md`](constraints.md)
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Stable references (CRITICAL)
|
|
69
|
+
|
|
70
|
+
Inline option objects cause infinite re-fetch loops and React hook dependency churn. Define **outside** the component and wrap hook options in `useMemo`:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'Customer' };
|
|
74
|
+
|
|
75
|
+
const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {
|
|
76
|
+
'_c_abc123': 'value',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default function SurveyPage() {
|
|
80
|
+
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>();
|
|
81
|
+
|
|
82
|
+
const surveyOptions = useMemo(
|
|
83
|
+
() => ({
|
|
84
|
+
instanceId: 'YOUR_JWT_HERE',
|
|
85
|
+
language: selectedLanguage,
|
|
86
|
+
placeholders: SURVEY_PLACEHOLDERS,
|
|
87
|
+
customFieldValues: CUSTOM_FIELD_VALUES,
|
|
88
|
+
savePartialResponse: false as const,
|
|
89
|
+
debug: false,
|
|
90
|
+
}),
|
|
91
|
+
[selectedLanguage]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
|
|
95
|
+
options: surveyOptions,
|
|
96
|
+
});
|
|
97
|
+
// ...
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Minimal SurveyPage skeleton
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
'use client';
|
|
107
|
+
|
|
108
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
109
|
+
import {
|
|
110
|
+
useSurveySDK,
|
|
111
|
+
type CustomFieldValueMap,
|
|
112
|
+
type Survey,
|
|
113
|
+
} from '@explorer02/cfm-survey-sdk';
|
|
114
|
+
import Question from './Question';
|
|
115
|
+
|
|
116
|
+
const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'Customer' };
|
|
117
|
+
const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {}; // fill from CRM when survey uses custom-field logic
|
|
118
|
+
|
|
119
|
+
function SurveyContent({
|
|
120
|
+
survey,
|
|
121
|
+
allQuestions,
|
|
122
|
+
customFieldValues,
|
|
123
|
+
state,
|
|
124
|
+
submitSurveyResults,
|
|
125
|
+
onAction,
|
|
126
|
+
}: {
|
|
127
|
+
survey: Survey;
|
|
128
|
+
allQuestions: Survey['pages'][number]['questions'];
|
|
129
|
+
customFieldValues: CustomFieldValueMap;
|
|
130
|
+
state: ReturnType<typeof useSurveySDK>['state'];
|
|
131
|
+
submitSurveyResults: ReturnType<typeof useSurveySDK>['submitSurveyResults'];
|
|
132
|
+
onAction: ReturnType<typeof useSurveySDK>['onAction'];
|
|
133
|
+
}) {
|
|
134
|
+
// Mark as started on first scroll
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const onScroll = () => onAction({ type: 'SCROLL' });
|
|
137
|
+
window.addEventListener('scroll', onScroll, { passive: true, once: true });
|
|
138
|
+
return () => window.removeEventListener('scroll', onScroll);
|
|
139
|
+
}, [onAction]);
|
|
140
|
+
|
|
141
|
+
// Question activity (VIEWED)
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const observer = new IntersectionObserver(
|
|
144
|
+
entries => {
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (entry.isIntersecting && entry.target.id) {
|
|
147
|
+
onAction({
|
|
148
|
+
type: 'RECORD_QUESTION_VIEW',
|
|
149
|
+
payload: { questionIds: [entry.target.id] },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{ threshold: 0.5 }
|
|
155
|
+
);
|
|
156
|
+
state.currentQuestions.forEach(q => {
|
|
157
|
+
const el = document.getElementById(q.id);
|
|
158
|
+
if (el) observer.observe(el);
|
|
159
|
+
});
|
|
160
|
+
return () => observer.disconnect();
|
|
161
|
+
}, [state.currentQuestions, onAction]);
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<main>
|
|
165
|
+
<ProgressBar progressPercentage={state.progressPercentage} />
|
|
166
|
+
|
|
167
|
+
{state.currentQuestions.map(question => (
|
|
168
|
+
<Question
|
|
169
|
+
key={question.id}
|
|
170
|
+
question={question}
|
|
171
|
+
selectedValue={state.answers[question.id]}
|
|
172
|
+
validationError={state.validationErrors[question.id]}
|
|
173
|
+
allAnswers={state.answers}
|
|
174
|
+
allQuestions={allQuestions}
|
|
175
|
+
customFieldValues={customFieldValues}
|
|
176
|
+
onSelect={answerValue =>
|
|
177
|
+
onAction({ type: 'CHANGE', payload: { questionId: question.id, answerValue } })
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
))}
|
|
181
|
+
|
|
182
|
+
{state.canGoBack && (
|
|
183
|
+
<button type="button" onClick={() => onAction({ type: 'PREVIOUS' })}>Back</button>
|
|
184
|
+
)}
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
onClick={() => onAction({ type: 'NEXT' })}
|
|
188
|
+
disabled={submitSurveyResults.isLoading}
|
|
189
|
+
>
|
|
190
|
+
{state.primaryButtonType === 'submit' ? 'Submit' : 'Next'}
|
|
191
|
+
</button>
|
|
192
|
+
</main>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export default function SurveyPage() {
|
|
197
|
+
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>();
|
|
198
|
+
|
|
199
|
+
const surveyOptions = useMemo(
|
|
200
|
+
() => ({
|
|
201
|
+
instanceId: 'YOUR_JWT_HERE',
|
|
202
|
+
language: selectedLanguage,
|
|
203
|
+
placeholders: SURVEY_PLACEHOLDERS,
|
|
204
|
+
customFieldValues: CUSTOM_FIELD_VALUES,
|
|
205
|
+
}),
|
|
206
|
+
[selectedLanguage]
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
|
|
210
|
+
options: surveyOptions,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const survey = surveyQueryResults.data;
|
|
214
|
+
const allQuestions = useMemo(
|
|
215
|
+
() => survey?.pages.flatMap(page => page.questions) ?? [],
|
|
216
|
+
[survey]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (surveyQueryResults.isLoading) return <div>Loading…</div>;
|
|
220
|
+
if (surveyQueryResults.error) return <div>Error: {surveyQueryResults.error.message}</div>;
|
|
221
|
+
if (!survey) return <div>No survey data</div>;
|
|
222
|
+
|
|
223
|
+
// Phase router — handle intro / paused / end / terminal before content
|
|
224
|
+
if (state.phase.kind === 'intro') return <IntroPage onStart={() => onAction({ type: 'START' })} />;
|
|
225
|
+
if (state.phase.kind === 'end') return <EndPage page={state.specialPage} />;
|
|
226
|
+
// … paused, submitted, expired, quota_fulfilled
|
|
227
|
+
|
|
228
|
+
if (state.phase.kind === 'content') {
|
|
229
|
+
return (
|
|
230
|
+
<SurveyContent
|
|
231
|
+
survey={survey}
|
|
232
|
+
allQuestions={allQuestions}
|
|
233
|
+
customFieldValues={CUSTOM_FIELD_VALUES}
|
|
234
|
+
state={state}
|
|
235
|
+
submitSurveyResults={submitSurveyResults}
|
|
236
|
+
onAction={onAction}
|
|
237
|
+
/>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
See [`01-components/01-survey-page.md`](../01-components/01-survey-page.md) for full phase routing.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Question dispatcher essentials
|
|
250
|
+
|
|
251
|
+
Copy [`templates/Question.tsx`](../templates/Question.tsx). Dispatch on flat `question.type` — **11** `QUESTION_TYPE` literals, no `subType`.
|
|
252
|
+
|
|
253
|
+
See [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) for per-type answer shapes, visibility helpers, and `onAction` patterns.
|
|
254
|
+
|
|
255
|
+
| Type literal | Component | Visibility helper |
|
|
256
|
+
|--------------|-----------|-------------------|
|
|
257
|
+
| `MCQ` | Inline | `getVisibleMcqOptionsForAnswers` |
|
|
258
|
+
| `TEXTFIELD` | Inline | — |
|
|
259
|
+
| `NPS_SCALE` | `RatingScale` | `getVisibleScaleColumnsForAnswers` |
|
|
260
|
+
| `CFM_MATRIX` | `LikertMatrixScale` | `getVisibleMatrixItemsForAnswers` |
|
|
261
|
+
| `CSAT_MATRIX`, `RATING_MATRIX` | `CsatMatrixScale` | `getVisibleMatrixItemsForAnswers` |
|
|
262
|
+
| `SLIDER_MATRIX` | `SliderMatrixScale` | `getVisibleStatementRowsForAnswers` |
|
|
263
|
+
| `FILE_UPLOAD` | `FileUploadScale` | — |
|
|
264
|
+
| `TEXT_AND_MEDIA` | Inline media | — |
|
|
265
|
+
| `HEATMAP` | `HeatmapScale` | — |
|
|
266
|
+
| `RANK_ORDER` | `RankOrderScale` | `getVisibleRankOrderOptionsForAnswers` |
|
|
267
|
+
|
|
268
|
+
**Answer logic:** filter before render:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
getVisibleMcqOptionsForAnswers(question, allAnswers, allQuestions, customFieldValues);
|
|
272
|
+
getVisibleMatrixItemsForAnswers(question, allAnswers, allQuestions, customFieldValues);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Placeholders:** stable `SURVEY_PLACEHOLDERS` ref + `resolveSurveyQuestionPlaceholders(text, allAnswers, questionsById)`. See [`placeholders-and-tokens.md`](placeholders-and-tokens.md).
|
|
276
|
+
|
|
277
|
+
**Wrapper:** every question `<section id={question.id}>` for scroll-to-error.
|
|
278
|
+
|
|
279
|
+
See [`component-checklist.md`](component-checklist.md) and [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md).
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Custom fields — two wiring points
|
|
284
|
+
|
|
285
|
+
When survey logic references `conditionEntity.entityType: 'CUSTOM_FIELD'`:
|
|
286
|
+
|
|
287
|
+
1. **`useSurveySDK` options** — `customFieldValues` map keyed by `_c_…` ids (skip, display, validation, progress).
|
|
288
|
+
2. **`Question.tsx` visibility helpers** — pass the **same map** as the 4th argument to `getVisible*ForAnswers` (answer-logic UI on options/rows/columns).
|
|
289
|
+
|
|
290
|
+
See [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md).
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Monorepo build prerequisite
|
|
295
|
+
|
|
296
|
+
When using `@repo/sdk` from the monorepo, build the SDK before running the client dev server:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
cd packages/sdk && npm run build
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Without a built `dist/`, the client fails with missing module errors. Rebuild after SDK changes.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Debug logging and analytics
|
|
307
|
+
|
|
308
|
+
Enable debug console logs with `useSurveySDK({ options: { debug: true } })`. Logic match logs appear as `[SDK:INFO]` when conditions **newly become true** on answer change:
|
|
309
|
+
|
|
310
|
+
| Log message | When |
|
|
311
|
+
|-------------|------|
|
|
312
|
+
| `Display logic matched` | A gated question becomes visible |
|
|
313
|
+
| `Answer logic matched` | An option / row / column becomes visible via answer logic |
|
|
314
|
+
| `Question variant matched` | A display variant’s condition matches |
|
|
315
|
+
| `End page logic matched` | The winning end page changes (preview before submit) |
|
|
316
|
+
| `Skip logic matched` | Skip logic newly activates on the current question |
|
|
317
|
+
|
|
318
|
+
Analytics events (`[SDK:ANALYTICS]`) fire **always**, even without `debug: true`:
|
|
319
|
+
|
|
320
|
+
| Event | When |
|
|
321
|
+
|-------|------|
|
|
322
|
+
| `display_logic_matched` | Display logic condition newly true |
|
|
323
|
+
| `answer_logic_matched` | Answer-logic sub-entity newly visible |
|
|
324
|
+
| `question_variant_matched` | Variant text switches to a matching variant |
|
|
325
|
+
| `end_page_logic_matched` | Resolved end page changes |
|
|
326
|
+
| `question_viewed` | Question enters viewport (`RECORD_QUESTION_VIEW`) |
|
|
327
|
+
| `skip_logic_triggered` | Skip logic newly activates on answer change |
|
|
328
|
+
| `logic_session_summary` | On successful submit — rollup table + chronological timeline |
|
|
329
|
+
|
|
330
|
+
The submit-time `logic_session_summary` includes counts per logic kind, viewed vs on-path-not-viewed questions, final end page resolution, and a `logicEventTimeline` audit. It prints via `console.table` alongside `survey_session_summary`.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Pre-flight checklist
|
|
335
|
+
|
|
336
|
+
Before `npm run build`:
|
|
337
|
+
|
|
338
|
+
- [ ] Import from package root only — no `@repo/sdk/src/…` paths
|
|
339
|
+
- [ ] `SURVEY_PLACEHOLDERS` and `CUSTOM_FIELD_VALUES` defined outside component
|
|
340
|
+
- [ ] `surveyOptions` wrapped in `useMemo` (depends on `selectedLanguage` etc.)
|
|
341
|
+
- [ ] `Question.tsx` copied from `docs/templates/Question.tsx` — all **11** type branches, no stub text
|
|
342
|
+
- [ ] `SURVEY_PLACEHOLDERS` defined outside component; `surveyOptions` in `useMemo` (see [`placeholders-and-tokens.md`](placeholders-and-tokens.md))
|
|
343
|
+
- [ ] `id={question.id}` on every question wrapper
|
|
344
|
+
- [ ] `customFieldValues` passed to hook **and** `Question` (when survey uses custom-field logic)
|
|
345
|
+
- [ ] Answer logic: `getVisible*ForAnswers` receives `customFieldValues` 4th arg
|
|
346
|
+
- [ ] `SCROLL` + `RECORD_QUESTION_VIEW` lifecycle effects wired (see [`survey-lifecycle-analytics.md`](survey-lifecycle-analytics.md))
|
|
347
|
+
- [ ] `state.canGoBack` / `state.primaryButtonType` used for nav (not raw page index)
|
|
348
|
+
- [ ] Monorepo: SDK built (`packages/sdk/dist/` exists)
|
|
349
|
+
- [ ] `grep -r "not yet implemented" src/ components/ app/` → zero matches
|
|
350
|
+
|
|
351
|
+
Run `bash templates/verify-agent-build.sh` from project root after Phase 5.
|