@forms.expert/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,331 @@
1
+ # Forms Expert SDK
2
+
3
+ Embeddable forms SDK for submitting forms via the Forms Expert API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @forms-expert/sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### Vanilla JavaScript (CDN)
14
+
15
+ ```html
16
+ <!-- Include the SDK -->
17
+ <script src="https://cdn.mira.io/forms-sdk/latest/vanilla/index.global.js"></script>
18
+
19
+ <!-- Create a container with data attributes -->
20
+ <div
21
+ data-forms-expert="contact"
22
+ data-api-key="pk_live_xxxxxxxxxxxx"
23
+ data-resource-id="your-resource-id"
24
+ ></div>
25
+
26
+ <!-- Forms will auto-initialize on page load -->
27
+ ```
28
+
29
+ ### Vanilla JavaScript (Module)
30
+
31
+ ```javascript
32
+ import { FormWidget } from '@forms-expert/sdk/vanilla';
33
+
34
+ const widget = new FormWidget(
35
+ {
36
+ apiKey: 'pk_live_xxxxxxxxxxxx',
37
+ resourceId: 'your-resource-id',
38
+ },
39
+ {
40
+ target: '#my-form',
41
+ slug: 'contact',
42
+ onSuccess: (response) => {
43
+ console.log('Form submitted:', response.submissionId);
44
+ },
45
+ onError: (error) => {
46
+ console.error('Submission failed:', error);
47
+ },
48
+ }
49
+ );
50
+
51
+ widget.init();
52
+ ```
53
+
54
+ ### React
55
+
56
+ ```tsx
57
+ import { FormsExpertForm } from '@forms-expert/sdk/react';
58
+
59
+ function ContactPage() {
60
+ return (
61
+ <FormsExpertForm
62
+ config={{
63
+ apiKey: 'pk_live_xxxxxxxxxxxx',
64
+ resourceId: 'your-resource-id',
65
+ }}
66
+ slug="contact"
67
+ submitText="Send Message"
68
+ onSuccess={(response) => {
69
+ console.log('Submitted:', response.submissionId);
70
+ }}
71
+ />
72
+ );
73
+ }
74
+ ```
75
+
76
+ ### React Hook
77
+
78
+ ```tsx
79
+ import { useForm, FormsProvider } from '@forms-expert/sdk/react';
80
+
81
+ // With provider
82
+ function App() {
83
+ return (
84
+ <FormsProvider
85
+ config={{
86
+ apiKey: 'pk_live_xxxxxxxxxxxx',
87
+ resourceId: 'your-resource-id',
88
+ }}
89
+ >
90
+ <ContactForm />
91
+ </FormsProvider>
92
+ );
93
+ }
94
+
95
+ function ContactForm() {
96
+ const form = useForm({
97
+ slug: 'contact',
98
+ onSuccess: (response) => alert('Thanks!'),
99
+ });
100
+
101
+ const handleSubmit = async (e) => {
102
+ e.preventDefault();
103
+ await form.submit();
104
+ };
105
+
106
+ if (form.isSubmitted) {
107
+ return <p>Thank you for your submission!</p>;
108
+ }
109
+
110
+ return (
111
+ <form onSubmit={handleSubmit}>
112
+ <input
113
+ type="email"
114
+ value={form.values.email || ''}
115
+ onChange={(e) => form.setValue('email', e.target.value)}
116
+ />
117
+ {form.errors.email && <span>{form.errors.email}</span>}
118
+
119
+ <textarea
120
+ value={form.values.message || ''}
121
+ onChange={(e) => form.setValue('message', e.target.value)}
122
+ />
123
+ {form.errors.message && <span>{form.errors.message}</span>}
124
+
125
+ <button type="submit" disabled={form.isLoading}>
126
+ {form.isLoading ? 'Sending...' : 'Submit'}
127
+ </button>
128
+ </form>
129
+ );
130
+ }
131
+ ```
132
+
133
+ ### Vue 3
134
+
135
+ ```vue
136
+ <script setup>
137
+ import { useForm } from '@forms-expert/sdk/vue';
138
+
139
+ const form = useForm({
140
+ slug: 'contact',
141
+ config: {
142
+ apiKey: 'pk_live_xxxxxxxxxxxx',
143
+ resourceId: 'your-resource-id',
144
+ },
145
+ onSuccess: (response) => {
146
+ alert('Form submitted!');
147
+ },
148
+ });
149
+
150
+ const handleSubmit = async () => {
151
+ await form.submit();
152
+ };
153
+ </script>
154
+
155
+ <template>
156
+ <form @submit.prevent="handleSubmit">
157
+ <div v-if="form.isSubmitted.value">
158
+ Thank you for your submission!
159
+ </div>
160
+
161
+ <template v-else>
162
+ <input
163
+ type="email"
164
+ :value="form.values.value.email"
165
+ @input="form.setValue('email', $event.target.value)"
166
+ />
167
+ <span v-if="form.errors.value.email">{{ form.errors.value.email }}</span>
168
+
169
+ <button type="submit" :disabled="form.isLoading.value">
170
+ {{ form.isLoading.value ? 'Sending...' : 'Submit' }}
171
+ </button>
172
+ </template>
173
+ </form>
174
+ </template>
175
+ ```
176
+
177
+ ## Core SDK
178
+
179
+ For programmatic form submission without UI:
180
+
181
+ ```typescript
182
+ import { FormsSDK } from '@forms-expert/sdk';
183
+
184
+ const sdk = new FormsSDK({
185
+ apiKey: 'pk_live_xxxxxxxxxxxx',
186
+ resourceId: 'your-resource-id',
187
+ });
188
+
189
+ // Check if form is active
190
+ const status = await sdk.isActive('contact');
191
+ if (!status.active) {
192
+ console.error('Form not available');
193
+ }
194
+
195
+ // Validate data
196
+ const validation = await sdk.validate('contact', {
197
+ email: 'user@example.com',
198
+ message: 'Hello!',
199
+ });
200
+
201
+ if (!validation.valid) {
202
+ console.error('Validation errors:', validation.errors);
203
+ }
204
+
205
+ // Submit form
206
+ const response = await sdk.submit('contact', {
207
+ email: 'user@example.com',
208
+ message: 'Hello!',
209
+ });
210
+
211
+ console.log('Submission ID:', response.submissionId);
212
+ ```
213
+
214
+ ### Form Handler
215
+
216
+ For more control, use the FormHandler:
217
+
218
+ ```typescript
219
+ const handler = sdk.form('contact', {
220
+ onSubmitStart: () => console.log('Submitting...'),
221
+ onSubmitSuccess: (response) => console.log('Success!', response),
222
+ onSubmitError: (error) => console.error('Error:', error),
223
+ onValidationError: (errors) => console.error('Validation:', errors),
224
+ });
225
+
226
+ // Initialize to get form config
227
+ await handler.initialize();
228
+
229
+ // Check captcha requirement
230
+ if (handler.requiresCaptcha()) {
231
+ const provider = handler.getCaptchaProvider(); // 'turnstile' | 'recaptcha' | 'hcaptcha'
232
+ // Initialize captcha widget...
233
+ }
234
+
235
+ // Submit with validation
236
+ await handler.submit({ email: 'user@example.com' });
237
+ ```
238
+
239
+ ## Configuration
240
+
241
+ ### SDK Config
242
+
243
+ | Option | Type | Required | Description |
244
+ |--------|------|----------|-------------|
245
+ | `apiKey` | `string` | Yes | Publishable API key (`pk_live_*` or `pk_test_*`) |
246
+ | `resourceId` | `string` | Yes | Resource ID containing the form |
247
+ | `baseUrl` | `string` | No | API base URL (default: `https://api.formsapp.io/api/v1`) |
248
+
249
+ ### Widget Options
250
+
251
+ | Option | Type | Default | Description |
252
+ |--------|------|---------|-------------|
253
+ | `target` | `string \| HTMLElement` | - | Target element or selector |
254
+ | `slug` | `string` | - | Form slug |
255
+ | `submitText` | `string` | `'Submit'` | Submit button text |
256
+ | `showBranding` | `boolean` | `true` | Show "Powered by Mira" branding |
257
+ | `resetOnSuccess` | `boolean` | `false` | Reset form after successful submission |
258
+ | `redirectUrl` | `string` | - | Redirect URL after success |
259
+ | `onSuccess` | `function` | - | Success callback |
260
+ | `onError` | `function` | - | Error callback |
261
+ | `onValidationError` | `function` | - | Validation error callback |
262
+
263
+ ## Data Attributes (Auto-init)
264
+
265
+ | Attribute | Required | Description |
266
+ |-----------|----------|-------------|
267
+ | `data-forms-expert` | Yes | Form slug |
268
+ | `data-api-key` | Yes | Publishable API key |
269
+ | `data-resource-id` | Yes | Resource ID |
270
+ | `data-base-url` | No | Custom API base URL |
271
+ | `data-submit-text` | No | Submit button text |
272
+ | `data-branding` | No | Set to `'false'` to hide branding |
273
+ | `data-reset` | No | Set to `'true'` to reset after submission |
274
+
275
+ ## Error Handling
276
+
277
+ ```typescript
278
+ import { FormsError, FormValidationError } from '@forms-expert/sdk';
279
+
280
+ try {
281
+ await sdk.submit('contact', data);
282
+ } catch (error) {
283
+ if (error instanceof FormValidationError) {
284
+ // Handle validation errors
285
+ error.errors.forEach(({ field, message }) => {
286
+ console.log(`${field}: ${message}`);
287
+ });
288
+ } else if (error instanceof FormsError) {
289
+ // Handle API errors
290
+ console.error(`${error.code}: ${error.message}`);
291
+
292
+ if (error.code === 'FORM_RATE_LIMIT_EXCEEDED') {
293
+ // Retry after delay
294
+ setTimeout(() => retry(), error.retryAfter * 1000);
295
+ }
296
+ }
297
+ }
298
+ ```
299
+
300
+ ### Error Codes
301
+
302
+ | Code | Description |
303
+ |------|-------------|
304
+ | `FORM_NOT_FOUND` | Form does not exist |
305
+ | `FORM_NOT_PUBLISHED` | Form is not published |
306
+ | `VALIDATION_ERROR` | Form data validation failed |
307
+ | `CAPTCHA_REQUIRED` | CAPTCHA token missing |
308
+ | `CAPTCHA_FAILED` | CAPTCHA verification failed |
309
+ | `FORM_RATE_LIMIT_EXCEEDED` | Form-specific rate limit exceeded |
310
+ | `GLOBAL_RATE_LIMIT_EXCEEDED` | IP rate limit exceeded |
311
+ | `ORIGIN_NOT_ALLOWED` | Request origin not in whitelist |
312
+
313
+ ## TypeScript
314
+
315
+ Full TypeScript support with exported types:
316
+
317
+ ```typescript
318
+ import type {
319
+ FormField,
320
+ FormSchema,
321
+ FormStyling,
322
+ FormStatusResponse,
323
+ ValidationResponse,
324
+ SubmissionResponse,
325
+ FormsSDKConfig,
326
+ } from '@forms-expert/sdk';
327
+ ```
328
+
329
+ ## License
330
+
331
+ MIT
@@ -0,0 +1,365 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // core/index.ts
21
+ var core_exports = {};
22
+ __export(core_exports, {
23
+ FormHandler: () => FormHandler,
24
+ FormValidationError: () => FormValidationError,
25
+ FormsApiClient: () => FormsApiClient,
26
+ FormsError: () => FormsError,
27
+ FormsSDK: () => FormsSDK
28
+ });
29
+ module.exports = __toCommonJS(core_exports);
30
+
31
+ // core/types.ts
32
+ var FormsError = class extends Error {
33
+ constructor(message, code, statusCode, retryAfter) {
34
+ super(message);
35
+ this.code = code;
36
+ this.statusCode = statusCode;
37
+ this.retryAfter = retryAfter;
38
+ this.name = "FormsError";
39
+ }
40
+ };
41
+ var FormValidationError = class extends Error {
42
+ constructor(errors) {
43
+ super("Validation failed");
44
+ this.errors = errors;
45
+ this.name = "FormValidationError";
46
+ }
47
+ };
48
+
49
+ // core/api-client.ts
50
+ var FormsApiClient = class {
51
+ constructor(config) {
52
+ this.apiKey = config.apiKey;
53
+ this.resourceId = config.resourceId;
54
+ this.baseUrl = (config.baseUrl || "https://api.formsapp.io/api/v1").replace(/\/$/, "");
55
+ }
56
+ /**
57
+ * Build URL with token query parameter
58
+ */
59
+ buildUrl(path) {
60
+ const separator = path.includes("?") ? "&" : "?";
61
+ return `${this.baseUrl}${path}${separator}token=${encodeURIComponent(this.apiKey)}`;
62
+ }
63
+ /**
64
+ * Make an API request
65
+ */
66
+ async request(method, path, body) {
67
+ const url = this.buildUrl(path);
68
+ const response = await fetch(url, {
69
+ method,
70
+ headers: {
71
+ "Content-Type": "application/json"
72
+ },
73
+ body: body ? JSON.stringify(body) : void 0
74
+ });
75
+ const data = await response.json();
76
+ if (!response.ok) {
77
+ throw new FormsError(
78
+ data.message || "Request failed",
79
+ data.code || "UNKNOWN_ERROR",
80
+ response.status,
81
+ data.retryAfter
82
+ );
83
+ }
84
+ return data;
85
+ }
86
+ /**
87
+ * Check if form is active and get configuration
88
+ */
89
+ async isActive(slug) {
90
+ return this.request("GET", `/f/${this.resourceId}/${slug}/is-active`);
91
+ }
92
+ /**
93
+ * Validate form data without submitting
94
+ */
95
+ async validate(slug, data) {
96
+ return this.request("POST", `/f/${this.resourceId}/${slug}/validate`, {
97
+ data
98
+ });
99
+ }
100
+ /**
101
+ * Submit form data (supports files)
102
+ */
103
+ async submit(slug, data, options) {
104
+ const url = this.buildUrl(`/f/${this.resourceId}/${slug}`);
105
+ const hasFiles = Object.values(data).some(
106
+ (v) => v instanceof File || v instanceof FileList && v.length > 0
107
+ );
108
+ if (hasFiles || options?.onProgress) {
109
+ return this.submitWithFormData(url, data, options);
110
+ }
111
+ return this.request("POST", `/f/${this.resourceId}/${slug}`, {
112
+ data,
113
+ pageUrl: options?.pageUrl || (typeof window !== "undefined" ? window.location.href : void 0),
114
+ captchaToken: options?.captchaToken
115
+ });
116
+ }
117
+ /**
118
+ * Submit with FormData (for file uploads with progress tracking)
119
+ */
120
+ submitWithFormData(url, data, options) {
121
+ return new Promise((resolve, reject) => {
122
+ const formData = new FormData();
123
+ for (const [key, value] of Object.entries(data)) {
124
+ if (value instanceof File) {
125
+ formData.append(key, value);
126
+ } else if (value instanceof FileList) {
127
+ Array.from(value).forEach((file) => formData.append(key, file));
128
+ } else if (value !== void 0 && value !== null) {
129
+ formData.append(`data[${key}]`, String(value));
130
+ }
131
+ }
132
+ const pageUrl = options?.pageUrl || (typeof window !== "undefined" ? window.location.href : "");
133
+ if (pageUrl) {
134
+ formData.append("pageUrl", pageUrl);
135
+ }
136
+ if (options?.captchaToken) {
137
+ formData.append("captchaToken", options.captchaToken);
138
+ }
139
+ const xhr = new XMLHttpRequest();
140
+ if (options?.onProgress) {
141
+ xhr.upload.addEventListener("progress", (event) => {
142
+ if (event.lengthComputable) {
143
+ options.onProgress({
144
+ loaded: event.loaded,
145
+ total: event.total,
146
+ percentage: Math.round(event.loaded / event.total * 100)
147
+ });
148
+ }
149
+ });
150
+ }
151
+ xhr.addEventListener("load", () => {
152
+ try {
153
+ const response = JSON.parse(xhr.responseText);
154
+ if (xhr.status >= 200 && xhr.status < 300) {
155
+ resolve(response);
156
+ } else {
157
+ reject(new FormsError(
158
+ response.message || "Submission failed",
159
+ response.code || "UNKNOWN_ERROR",
160
+ xhr.status,
161
+ response.retryAfter
162
+ ));
163
+ }
164
+ } catch {
165
+ reject(new FormsError("Invalid response", "PARSE_ERROR", xhr.status));
166
+ }
167
+ });
168
+ xhr.addEventListener("error", () => {
169
+ reject(new FormsError("Network error", "NETWORK_ERROR", 0));
170
+ });
171
+ xhr.addEventListener("abort", () => {
172
+ reject(new FormsError("Request aborted", "ABORTED", 0));
173
+ });
174
+ xhr.open("POST", url);
175
+ xhr.send(formData);
176
+ });
177
+ }
178
+ /**
179
+ * Track a form view (for analytics completion rate)
180
+ */
181
+ async trackView(slug) {
182
+ const url = this.buildUrl(`/f/${this.resourceId}/${slug}/view`);
183
+ await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" } }).catch(() => {
184
+ });
185
+ }
186
+ /**
187
+ * Get resource ID
188
+ */
189
+ getResourceId() {
190
+ return this.resourceId;
191
+ }
192
+ /**
193
+ * Get base URL
194
+ */
195
+ getBaseUrl() {
196
+ return this.baseUrl;
197
+ }
198
+ };
199
+
200
+ // core/forms-sdk.ts
201
+ var FormHandler = class {
202
+ constructor(apiClient, slug, options = {}) {
203
+ this.config = null;
204
+ this.apiClient = apiClient;
205
+ this.slug = slug;
206
+ this.options = options;
207
+ }
208
+ /**
209
+ * Initialize form handler and fetch configuration
210
+ */
211
+ async initialize() {
212
+ this.config = await this.apiClient.isActive(this.slug);
213
+ if (this.options.trackViews) {
214
+ this.apiClient.trackView(this.slug);
215
+ }
216
+ return this.config;
217
+ }
218
+ /**
219
+ * Get cached form configuration
220
+ */
221
+ getConfig() {
222
+ return this.config;
223
+ }
224
+ /**
225
+ * Check if form is active
226
+ */
227
+ isActive() {
228
+ return this.config?.active ?? false;
229
+ }
230
+ /**
231
+ * Check if captcha is required
232
+ */
233
+ requiresCaptcha() {
234
+ return this.config?.settings?.captcha?.enabled ?? false;
235
+ }
236
+ /**
237
+ * Get captcha provider
238
+ */
239
+ getCaptchaProvider() {
240
+ return this.config?.settings?.captcha?.provider;
241
+ }
242
+ /**
243
+ * Get form schema
244
+ */
245
+ getSchema() {
246
+ return this.config?.schema;
247
+ }
248
+ /**
249
+ * Validate form data
250
+ */
251
+ async validate(data) {
252
+ return this.apiClient.validate(this.slug, data);
253
+ }
254
+ /**
255
+ * Submit form data
256
+ */
257
+ async submit(data, options) {
258
+ this.options.onSubmitStart?.();
259
+ try {
260
+ if (this.config?.mode === "schema") {
261
+ const validation = await this.validate(data);
262
+ if (!validation.valid) {
263
+ this.options.onValidationError?.(validation.errors);
264
+ throw new FormValidationError(validation.errors);
265
+ }
266
+ }
267
+ const response = await this.apiClient.submit(this.slug, data, options);
268
+ this.options.onSubmitSuccess?.(response);
269
+ return response;
270
+ } catch (error) {
271
+ if (error instanceof FormsError) {
272
+ this.options.onSubmitError?.(error);
273
+ }
274
+ throw error;
275
+ }
276
+ }
277
+ /**
278
+ * Get success message from config
279
+ */
280
+ getSuccessMessage() {
281
+ return this.config?.settings?.successMessage || "Form submitted successfully!";
282
+ }
283
+ /**
284
+ * Get redirect URL from config
285
+ */
286
+ getRedirectUrl() {
287
+ return this.config?.settings?.redirectUrl;
288
+ }
289
+ };
290
+ var FormsSDK = class {
291
+ constructor(config) {
292
+ this.apiClient = new FormsApiClient(config);
293
+ }
294
+ /**
295
+ * Check if form is active and get configuration
296
+ */
297
+ async isActive(slug) {
298
+ return this.apiClient.isActive(slug);
299
+ }
300
+ /**
301
+ * Validate form data without submitting
302
+ */
303
+ async validate(slug, data) {
304
+ return this.apiClient.validate(slug, data);
305
+ }
306
+ /**
307
+ * Submit form data
308
+ */
309
+ async submit(slug, data, options) {
310
+ return this.apiClient.submit(slug, data, options);
311
+ }
312
+ /**
313
+ * Create a form handler for a specific form
314
+ */
315
+ form(slug, options) {
316
+ return new FormHandler(this.apiClient, slug, options);
317
+ }
318
+ /**
319
+ * Track a form view (for analytics completion rate)
320
+ */
321
+ async trackView(slug) {
322
+ return this.apiClient.trackView(slug);
323
+ }
324
+ /**
325
+ * Submit with retry logic for rate limits
326
+ */
327
+ async submitWithRetry(slug, data, options) {
328
+ const maxRetries = options?.maxRetries ?? 3;
329
+ let lastError = null;
330
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
331
+ try {
332
+ return await this.submit(slug, data, options);
333
+ } catch (error) {
334
+ lastError = error;
335
+ if (error instanceof FormsError) {
336
+ if ([
337
+ "VALIDATION_ERROR",
338
+ "CAPTCHA_REQUIRED",
339
+ "ORIGIN_NOT_ALLOWED"
340
+ ].includes(error.code)) {
341
+ throw error;
342
+ }
343
+ if (error.code.includes("RATE_LIMIT")) {
344
+ const retryAfter = error.retryAfter || Math.pow(2, attempt) * 1e3;
345
+ await new Promise((resolve) => setTimeout(resolve, retryAfter));
346
+ continue;
347
+ }
348
+ }
349
+ await new Promise(
350
+ (resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1e3)
351
+ );
352
+ }
353
+ }
354
+ throw lastError;
355
+ }
356
+ };
357
+ // Annotate the CommonJS export names for ESM import in node:
358
+ 0 && (module.exports = {
359
+ FormHandler,
360
+ FormValidationError,
361
+ FormsApiClient,
362
+ FormsError,
363
+ FormsSDK
364
+ });
365
+ //# sourceMappingURL=index.cjs.map