@selfhelp/sh2-shp-survey-js-mobile 0.3.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/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@selfhelp/sh2-shp-survey-js-mobile",
3
+ "version": "0.3.0",
4
+ "description": "SurveyJS v2 mobile renderer for the SelfHelp plugin ecosystem. Hosts the official SurveyJS web runtime (survey-core + survey-react-ui) in an isolated, self-contained WebView (react-native-webview on native, iframe on web export) driven by a typed postMessage bridge; the native host owns all authenticated API calls.",
5
+ "license": "MPL-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/humdek-unibe-ch/sh2-shp-survey-js.git",
9
+ "directory": "mobile"
10
+ },
11
+ "homepage": "https://github.com/humdek-unibe-ch/sh2-shp-survey-js#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/humdek-unibe-ch/sh2-shp-survey-js/issues"
14
+ },
15
+ "main": "dist/index.js",
16
+ "module": "dist/index.mjs",
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.mjs",
22
+ "require": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src",
28
+ "README.md"
29
+ ],
30
+ "scripts": {
31
+ "build:webview": "vite build --config vite.webview.config.ts && node scripts/wrap-webview-html.mjs",
32
+ "build": "npm run build:webview && tsup",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "vitest run"
35
+ },
36
+ "dependencies": {
37
+ "survey-core": "2.5.25",
38
+ "survey-react-ui": "2.5.25"
39
+ },
40
+ "peerDependencies": {
41
+ "@selfhelp/shared": "^1.16.0",
42
+ "react": "^19.2.0",
43
+ "react-dom": "^19.2.0",
44
+ "react-native": "^0.83.0",
45
+ "react-native-webview": ">=13.0.0 <14.0.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "react-dom": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "devDependencies": {
53
+ "@selfhelp/shared": "^1.16.0",
54
+ "@types/react": "^19.2.14",
55
+ "react": "^19.2.0",
56
+ "react-dom": "^19.2.7",
57
+ "react-native": "^0.83.6",
58
+ "react-native-webview": "^13.17.0",
59
+ "survey-core": "2.5.25",
60
+ "survey-react-ui": "2.5.25",
61
+ "tsup": "^8.3.5",
62
+ "typescript": "^5.9.3",
63
+ "vite": "^7.3.5",
64
+ "vitest": "^3.0.0"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "sideEffects": false
70
+ }
@@ -0,0 +1,205 @@
1
+ /*
2
+ SPDX-FileCopyrightText: 2026 Humdek, University of Bern
3
+ SPDX-License-Identifier: MPL-2.0
4
+ */
5
+ /**
6
+ * Public SurveyJS plugin API client for the MOBILE host shell.
7
+ *
8
+ * Unlike the frontend client, the mobile renderer does NOT own authenticated
9
+ * network access: every call goes through the native host-services bridge
10
+ * (`IMobileHostServices.request`), which attaches the bearer token, applies
11
+ * `X-Client-Type: mobile`, and performs the single 401-refresh round-trip
12
+ * host-side. The WebView runtime never sees the token — it emits intents and
13
+ * the shell calls these functions in response.
14
+ *
15
+ * Routes/payloads mirror the frontend client (`frontend/src/api/surveys.ts`)
16
+ * exactly, so a mobile submission stores a real `SurveyRun` identically to web
17
+ * (there is no preview/test branch on the backend).
18
+ *
19
+ * Only the runtime routes the renderer needs are wrapped: hydrate
20
+ * (`published`), per-page progress (`progress`), submit (`submit`). The
21
+ * file/choices/edit pipeline stays web-only.
22
+ */
23
+
24
+ import type { IMobileHostServices } from '@selfhelp/shared/plugin-sdk';
25
+
26
+ export interface IRuntimeConfig {
27
+ restartOnRefresh: boolean;
28
+ autoSaveIntervalSeconds: number;
29
+ timeoutMinutes: number;
30
+ savePdf: boolean;
31
+ closeModalAtEnd: boolean;
32
+ redirectAtEnd: string | null;
33
+ urlParams: boolean;
34
+ startTime: string | null;
35
+ endTime: string | null;
36
+ oncePerUser: boolean;
37
+ oncePerSchedule: boolean;
38
+ ownEntriesOnly: boolean;
39
+ allowAnonymous: boolean;
40
+ labelSurveyDone: string | null;
41
+ labelSurveyNotActive: string | null;
42
+ }
43
+
44
+ export interface IPublishedRuntimeState {
45
+ isAuthenticated: boolean;
46
+ visitorId: string | null;
47
+ lockoutReason: { reason: string; responseId: string; submittedAt: string | null } | null;
48
+ draft: { responseId: string; pageNo: number; lastSavedAt: string } | null;
49
+ completedResponseId: string | null;
50
+ }
51
+
52
+ export interface IPublishedSurvey {
53
+ surveyId: string;
54
+ name: string;
55
+ themeCode: string | null;
56
+ revision: number;
57
+ definition: Record<string, unknown>;
58
+ extraParams: Record<string, string | number | boolean>;
59
+ tokens: Record<string, string>;
60
+ runtimeConfig: IRuntimeConfig;
61
+ state: IPublishedRuntimeState;
62
+ }
63
+
64
+ export interface ISubmitResult {
65
+ runId: number;
66
+ responseId: string;
67
+ submittedAt: string;
68
+ }
69
+
70
+ export interface IDraftPayload {
71
+ responseId: string;
72
+ pageNo: number;
73
+ payload: Record<string, unknown>;
74
+ lastSavedAt: string;
75
+ expiresAt: string;
76
+ }
77
+
78
+ export interface ISubmissionEnforcePayload {
79
+ oncePerUser?: boolean;
80
+ oncePerSchedule?: boolean;
81
+ allowAnonymous?: boolean;
82
+ windowStart?: string | null;
83
+ windowEnd?: string | null;
84
+ responseId?: string;
85
+ editMode?: boolean;
86
+ progress?: Record<string, unknown>;
87
+ }
88
+
89
+ /** Error thrown when a host request fails; carries the lifecycle discriminator. */
90
+ export class SurveyHostError extends Error {
91
+ readonly status: number;
92
+ readonly reason?: string;
93
+ readonly sessionExpired: boolean;
94
+
95
+ constructor(message: string, status: number, reason?: string, sessionExpired = false) {
96
+ super(message);
97
+ this.name = 'SurveyHostError';
98
+ this.status = status;
99
+ this.reason = reason;
100
+ this.sessionExpired = sessionExpired;
101
+ }
102
+ }
103
+
104
+ const PLUGIN_API_PATH = '/cms-api/v1/plugins/sh2-shp-survey-js';
105
+
106
+ /** Unwrap a host response into the inner API-envelope `data`, throwing on failure. */
107
+ function unwrap<T>(res: {
108
+ ok: boolean;
109
+ status: number;
110
+ data: unknown;
111
+ reason?: string;
112
+ error?: string;
113
+ sessionExpired?: boolean;
114
+ }): T {
115
+ if (!res.ok) {
116
+ const reason = res.reason ?? extractReason(res.data);
117
+ throw new SurveyHostError(
118
+ res.error ?? `HTTP ${res.status}`,
119
+ res.status,
120
+ reason,
121
+ res.sessionExpired ?? res.status === 401,
122
+ );
123
+ }
124
+ const body = res.data as { data?: T } | null;
125
+ return (body?.data ?? null) as T;
126
+ }
127
+
128
+ function extractReason(body: unknown): string | undefined {
129
+ if (body && typeof body === 'object' && 'reason' in body) {
130
+ const reason = (body as { reason?: unknown }).reason;
131
+ if (typeof reason === 'string') return reason;
132
+ }
133
+ return undefined;
134
+ }
135
+
136
+ export async function loadPublishedSurvey(
137
+ host: IMobileHostServices,
138
+ key: string,
139
+ serverConfig?: Record<string, unknown>,
140
+ extraParams?: Record<string, string | number | boolean>,
141
+ ): Promise<IPublishedSurvey> {
142
+ const query: Record<string, string | number | boolean> = {};
143
+ if (extraParams) {
144
+ for (const [k, v] of Object.entries(extraParams)) {
145
+ query[`extraParams[${k}]`] = v;
146
+ }
147
+ }
148
+ const headers: Record<string, string> = {};
149
+ if (serverConfig) {
150
+ try {
151
+ headers['X-SurveyJs-Runtime-Config'] = JSON.stringify(serverConfig);
152
+ } catch {
153
+ /* non-serialisable config — send without the echo header */
154
+ }
155
+ }
156
+ const res = await host.request<{ data: IPublishedSurvey }>({
157
+ path: `${PLUGIN_API_PATH}/published/${encodeURIComponent(key)}`,
158
+ method: 'GET',
159
+ headers,
160
+ query,
161
+ });
162
+ return unwrap<IPublishedSurvey>(res);
163
+ }
164
+
165
+ export async function fetchDraft(
166
+ host: IMobileHostServices,
167
+ key: string,
168
+ responseId?: string,
169
+ ): Promise<IDraftPayload | null> {
170
+ const query: Record<string, string | number | boolean> = {};
171
+ if (responseId) query.responseId = responseId;
172
+ const res = await host.request<{ data: IDraftPayload | null }>({
173
+ path: `${PLUGIN_API_PATH}/published/${encodeURIComponent(key)}/progress`,
174
+ method: 'GET',
175
+ query,
176
+ });
177
+ return unwrap<IDraftPayload | null>(res);
178
+ }
179
+
180
+ export async function saveProgress(
181
+ host: IMobileHostServices,
182
+ key: string,
183
+ payload: { responseId?: string; pageNo: number; payload: Record<string, unknown> },
184
+ ): Promise<IDraftPayload> {
185
+ const res = await host.request<{ data: IDraftPayload }>({
186
+ path: `${PLUGIN_API_PATH}/published/${encodeURIComponent(key)}/progress`,
187
+ method: 'PUT',
188
+ body: payload,
189
+ });
190
+ return unwrap<IDraftPayload>(res);
191
+ }
192
+
193
+ export async function submitSurvey(
194
+ host: IMobileHostServices,
195
+ key: string,
196
+ answers: Record<string, unknown>,
197
+ enforce?: ISubmissionEnforcePayload,
198
+ ): Promise<ISubmitResult> {
199
+ const res = await host.request<{ data: ISubmitResult }>({
200
+ path: `${PLUGIN_API_PATH}/published/${encodeURIComponent(key)}/submit`,
201
+ method: 'POST',
202
+ body: { answers, enforce: enforce ?? {} },
203
+ });
204
+ return unwrap<ISubmitResult>(res);
205
+ }
@@ -0,0 +1,264 @@
1
+ /*
2
+ SPDX-FileCopyrightText: 2026 Humdek, University of Bern
3
+ SPDX-License-Identifier: MPL-2.0
4
+ */
5
+ /**
6
+ * Typed postMessage contract between the isolated SurveyJS WebView runtime
7
+ * and the native RN shell (host).
8
+ *
9
+ * Security model (enforced by both sides):
10
+ * - Every message carries `source: 'sh2-surveyjs'` and a known `type`.
11
+ * - The receiver runs the matching type guard and DROPS anything that does
12
+ * not match the expected shape (no `eval`, no trusting arbitrary data).
13
+ * - The WebView NEVER receives the access token and NEVER performs a
14
+ * backend call: it emits *intents* (`LOAD_SURVEY` / `SAVE_PROGRESS` /
15
+ * `SUBMIT_SURVEY`) and consumes *results* the native host returns after
16
+ * performing the authenticated request via the host-services bridge.
17
+ *
18
+ * This module is framework-free so it can be imported by the WebView runtime
19
+ * bundle, the RN shell, and the contract tests alike.
20
+ */
21
+
22
+ import type {
23
+ IPublishedRuntimeState,
24
+ IRuntimeConfig,
25
+ ISubmissionEnforcePayload,
26
+ } from '../api/surveys';
27
+ import type { IRuntimeSectionConfig } from '../styles/section';
28
+
29
+ /** Protocol tag stamped on every message in both directions. */
30
+ export const BRIDGE_SOURCE = 'sh2-surveyjs' as const;
31
+
32
+ /** Bumped when the message contract changes in a breaking way. */
33
+ export const BRIDGE_PROTOCOL_VERSION = 1 as const;
34
+
35
+ /* ------------------------------------------------------------------ *
36
+ * WebView -> Host (intents + UI signals)
37
+ * ------------------------------------------------------------------ */
38
+
39
+ export type TWebviewToHostType =
40
+ | 'READY'
41
+ | 'LOAD_SURVEY'
42
+ | 'SAVE_PROGRESS'
43
+ | 'SUBMIT_SURVEY'
44
+ | 'RESIZE'
45
+ | 'REQUEST_REDIRECT'
46
+ | 'RUNTIME_ERROR'
47
+ | 'UNSUPPORTED';
48
+
49
+ interface IBridgeEnvelope<TType extends string> {
50
+ source: typeof BRIDGE_SOURCE;
51
+ type: TType;
52
+ }
53
+
54
+ /** Runtime has booted and is ready to receive `INIT`. */
55
+ export interface IReadyMessage extends IBridgeEnvelope<'READY'> {
56
+ protocolVersion: number;
57
+ }
58
+
59
+ /** Runtime asks the host to load the published survey definition. */
60
+ export interface ILoadSurveyMessage extends IBridgeEnvelope<'LOAD_SURVEY'> {
61
+ surveyKey: string;
62
+ }
63
+
64
+ /** Runtime asks the host to persist per-page progress (draft). */
65
+ export interface ISaveProgressMessage extends IBridgeEnvelope<'SAVE_PROGRESS'> {
66
+ responseId: string;
67
+ pageNo: number;
68
+ data: Record<string, unknown>;
69
+ locale?: string;
70
+ }
71
+
72
+ /** Runtime asks the host to submit the completed survey. */
73
+ export interface ISubmitSurveyMessage extends IBridgeEnvelope<'SUBMIT_SURVEY'> {
74
+ responseId: string | null;
75
+ data: Record<string, unknown>;
76
+ enforce: ISubmissionEnforcePayload;
77
+ }
78
+
79
+ /** Runtime reports its measured content height (native sizing). */
80
+ export interface IResizeMessage extends IBridgeEnvelope<'RESIZE'> {
81
+ height: number;
82
+ }
83
+
84
+ /** Runtime asks the host to redirect after completion. */
85
+ export interface IRequestRedirectMessage extends IBridgeEnvelope<'REQUEST_REDIRECT'> {
86
+ target: string;
87
+ external: boolean;
88
+ }
89
+
90
+ /** Runtime reports an unrecoverable runtime error. */
91
+ export interface IRuntimeErrorMessage extends IBridgeEnvelope<'RUNTIME_ERROR'> {
92
+ message: string;
93
+ }
94
+
95
+ /** Runtime reports an unsupported feature (degrades in place, never crashes). */
96
+ export interface IUnsupportedMessage extends IBridgeEnvelope<'UNSUPPORTED'> {
97
+ feature: string;
98
+ }
99
+
100
+ export type TWebviewToHostMessage =
101
+ | IReadyMessage
102
+ | ILoadSurveyMessage
103
+ | ISaveProgressMessage
104
+ | ISubmitSurveyMessage
105
+ | IResizeMessage
106
+ | IRequestRedirectMessage
107
+ | IRuntimeErrorMessage
108
+ | IUnsupportedMessage;
109
+
110
+ /* ------------------------------------------------------------------ *
111
+ * Host -> WebView (results + inputs; never a raw token)
112
+ * ------------------------------------------------------------------ */
113
+
114
+ export type THostToWebviewType =
115
+ | 'INIT'
116
+ | 'SURVEY_LOADED'
117
+ | 'PROGRESS_SAVED'
118
+ | 'SUBMIT_RESULT'
119
+ | 'SESSION_EXPIRED'
120
+ | 'SET_LOCALE';
121
+
122
+ /** Host bootstraps the runtime (config + display inputs, no token). */
123
+ export interface IInitMessage extends IBridgeEnvelope<'INIT'> {
124
+ surveyKey: string;
125
+ config: IRuntimeSectionConfig;
126
+ theme: string | null;
127
+ locale: string | null;
128
+ }
129
+
130
+ /** Host returns the loaded survey definition + server state + draft. */
131
+ export interface ISurveyLoadedMessage extends IBridgeEnvelope<'SURVEY_LOADED'> {
132
+ definition: Record<string, unknown>;
133
+ tokens: Record<string, string>;
134
+ extraParams: Record<string, string | number | boolean>;
135
+ runtimeConfig: IRuntimeConfig;
136
+ state: IPublishedRuntimeState;
137
+ draft: { responseId: string; pageNo: number; data: Record<string, unknown> } | null;
138
+ }
139
+
140
+ /** Host confirms a progress save. */
141
+ export interface IProgressSavedMessage extends IBridgeEnvelope<'PROGRESS_SAVED'> {
142
+ ok: boolean;
143
+ responseId?: string;
144
+ }
145
+
146
+ /** Host returns the submission outcome. */
147
+ export type ISubmitResultMessage = IBridgeEnvelope<'SUBMIT_RESULT'> &
148
+ (
149
+ | { ok: true; responseId: string; submittedAt: string }
150
+ | { ok: false; reason?: string; message: string }
151
+ );
152
+
153
+ /** Host signals the session could not be refreshed (auth expired). */
154
+ export type ISessionExpiredMessage = IBridgeEnvelope<'SESSION_EXPIRED'>;
155
+
156
+ /** Host pushes a locale change to the runtime. */
157
+ export interface ISetLocaleMessage extends IBridgeEnvelope<'SET_LOCALE'> {
158
+ locale: string;
159
+ }
160
+
161
+ export type THostToWebviewMessage =
162
+ | IInitMessage
163
+ | ISurveyLoadedMessage
164
+ | IProgressSavedMessage
165
+ | ISubmitResultMessage
166
+ | ISessionExpiredMessage
167
+ | ISetLocaleMessage;
168
+
169
+ /* ------------------------------------------------------------------ *
170
+ * Guards — every receiver validates the shape before acting.
171
+ * ------------------------------------------------------------------ */
172
+
173
+ function isRecord(value: unknown): value is Record<string, unknown> {
174
+ return typeof value === 'object' && value !== null;
175
+ }
176
+
177
+ function hasBridgeEnvelope(value: unknown): value is { source: string; type: string } {
178
+ return (
179
+ isRecord(value) &&
180
+ value.source === BRIDGE_SOURCE &&
181
+ typeof value.type === 'string'
182
+ );
183
+ }
184
+
185
+ const WEBVIEW_TO_HOST_TYPES: ReadonlySet<string> = new Set<TWebviewToHostType>([
186
+ 'READY',
187
+ 'LOAD_SURVEY',
188
+ 'SAVE_PROGRESS',
189
+ 'SUBMIT_SURVEY',
190
+ 'RESIZE',
191
+ 'REQUEST_REDIRECT',
192
+ 'RUNTIME_ERROR',
193
+ 'UNSUPPORTED',
194
+ ]);
195
+
196
+ const HOST_TO_WEBVIEW_TYPES: ReadonlySet<string> = new Set<THostToWebviewType>([
197
+ 'INIT',
198
+ 'SURVEY_LOADED',
199
+ 'PROGRESS_SAVED',
200
+ 'SUBMIT_RESULT',
201
+ 'SESSION_EXPIRED',
202
+ 'SET_LOCALE',
203
+ ]);
204
+
205
+ /**
206
+ * Validate a message coming FROM the WebView (the native host calls this).
207
+ * Performs per-type field checks so a malformed/hostile message is dropped.
208
+ */
209
+ export function isWebviewToHostMessage(value: unknown): value is TWebviewToHostMessage {
210
+ if (!hasBridgeEnvelope(value) || !WEBVIEW_TO_HOST_TYPES.has(value.type)) return false;
211
+ const msg = value as Record<string, unknown>;
212
+ switch (msg.type) {
213
+ case 'READY':
214
+ return true;
215
+ case 'LOAD_SURVEY':
216
+ return typeof msg.surveyKey === 'string' && msg.surveyKey !== '';
217
+ case 'SAVE_PROGRESS':
218
+ return (
219
+ typeof msg.responseId === 'string' &&
220
+ typeof msg.pageNo === 'number' &&
221
+ isRecord(msg.data)
222
+ );
223
+ case 'SUBMIT_SURVEY':
224
+ return (
225
+ (msg.responseId === null || typeof msg.responseId === 'string') &&
226
+ isRecord(msg.data) &&
227
+ isRecord(msg.enforce)
228
+ );
229
+ case 'RESIZE':
230
+ return typeof msg.height === 'number' && Number.isFinite(msg.height);
231
+ case 'REQUEST_REDIRECT':
232
+ return typeof msg.target === 'string' && typeof msg.external === 'boolean';
233
+ case 'RUNTIME_ERROR':
234
+ return typeof msg.message === 'string';
235
+ case 'UNSUPPORTED':
236
+ return typeof msg.feature === 'string';
237
+ default:
238
+ return false;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Validate a message coming FROM the host (the WebView runtime calls this).
244
+ */
245
+ export function isHostToWebviewMessage(value: unknown): value is THostToWebviewMessage {
246
+ if (!hasBridgeEnvelope(value) || !HOST_TO_WEBVIEW_TYPES.has(value.type)) return false;
247
+ const msg = value as Record<string, unknown>;
248
+ switch (msg.type) {
249
+ case 'INIT':
250
+ return typeof msg.surveyKey === 'string' && isRecord(msg.config);
251
+ case 'SURVEY_LOADED':
252
+ return isRecord(msg.definition) && isRecord(msg.state);
253
+ case 'PROGRESS_SAVED':
254
+ return typeof msg.ok === 'boolean';
255
+ case 'SUBMIT_RESULT':
256
+ return typeof msg.ok === 'boolean';
257
+ case 'SESSION_EXPIRED':
258
+ return true;
259
+ case 'SET_LOCALE':
260
+ return typeof msg.locale === 'string';
261
+ default:
262
+ return false;
263
+ }
264
+ }
package/src/css.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /*
2
+ SPDX-FileCopyrightText: 2026 Humdek, University of Bern
3
+ SPDX-License-Identifier: MPL-2.0
4
+ */
5
+ /**
6
+ * Ambient module for side-effect CSS imports.
7
+ *
8
+ * The interactive web runtime lazily `import('survey-core/survey-core.css')`
9
+ * inside a try/catch so the Expo Metro web bundler injects the SurveyJS base
10
+ * styles. The mobile tsconfig has no DOM/bundler CSS typing, so declare the
11
+ * wildcard module to keep `tsc --noEmit` happy without pulling in a CSS loader.
12
+ */
13
+ declare module '*.css';
package/src/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ /*
2
+ SPDX-FileCopyrightText: 2026 Humdek, University of Bern
3
+ SPDX-License-Identifier: MPL-2.0
4
+ */
5
+ /**
6
+ * `@selfhelp/sh2-shp-survey-js-mobile` — mobile entry.
7
+ *
8
+ * Bundled into mobile builds by the host's `plugins:sync` script
9
+ * (per EAS profile). Exports `registerMobile` which returns the
10
+ * plugin's `IMobilePluginRegistration` — the `surveyjs` style.
11
+ *
12
+ * The style (`styles/SurveyJsStyle`) is a thin native shell that hosts the
13
+ * OFFICIAL SurveyJS web runtime (`survey-core` + `survey-react-ui`) inside an
14
+ * isolated, self-contained WebView — `react-native-webview` on native, an
15
+ * `iframe` on the Expo web export — driven by a typed postMessage bridge.
16
+ * This gives mobile full parity with the web frontend (same JSON, question
17
+ * types, validation, conditional logic, completion, redirect). The native
18
+ * host owns ALL authenticated API calls via `@selfhelp/shared`
19
+ * `MobileHostServices`; the WebView never sees the access token.
20
+ */
21
+
22
+ import { defineMobilePlugin } from '@selfhelp/shared/plugin-sdk';
23
+ import type { IMobilePluginRegistration } from '@selfhelp/shared/plugin-sdk';
24
+
25
+ import { SurveyJsStyle } from './styles/SurveyJsStyle';
26
+
27
+ export const PLUGIN_ID = 'sh2-shp-survey-js';
28
+ /**
29
+ * Must match `plugin.json#version` and the mobile `package.json#version`.
30
+ * `PluginRuntime.registerOne()` (web) and the mobile sync script both
31
+ * compare these constants against the manifest version; a mismatch
32
+ * silently breaks the plugin.
33
+ */
34
+ export const PLUGIN_VERSION = '0.3.0';
35
+
36
+ export const registerMobile = (): IMobilePluginRegistration =>
37
+ defineMobilePlugin({
38
+ id: PLUGIN_ID,
39
+ version: PLUGIN_VERSION,
40
+ pluginApiVersion: '0.1.0',
41
+ styles: [
42
+ {
43
+ name: 'surveyjs',
44
+ description: 'Mobile renderer for a published SurveyJS survey (official SurveyJS runtime in a WebView; full submit/validation/redirect parity with web).',
45
+ category: 'forms',
46
+ canHaveChildren: false,
47
+ component: SurveyJsStyle as never,
48
+ },
49
+ ],
50
+ featureFlags: [
51
+ { key: 'gpx', label: 'GPX question type', defaultEnabled: false },
52
+ { key: 'video', label: 'Video question type', defaultEnabled: false },
53
+ { key: 'rich-text', label: 'Rich-text question type', defaultEnabled: true },
54
+ ],
55
+ });
56
+
57
+ export default registerMobile;