@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 +331 -0
- package/dist/core/index.cjs +365 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +322 -0
- package/dist/core/index.d.ts +322 -0
- package/dist/core/index.js +334 -0
- package/dist/core/index.js.map +1 -0
- package/dist/react/index.cjs +1014 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +430 -0
- package/dist/react/index.d.ts +430 -0
- package/dist/react/index.js +991 -0
- package/dist/react/index.js.map +1 -0
- package/dist/vanilla/index.cjs +209 -0
- package/dist/vanilla/index.cjs.map +1 -0
- package/dist/vanilla/index.d.cts +188 -0
- package/dist/vanilla/index.d.ts +188 -0
- package/dist/vanilla/index.global.js +209 -0
- package/dist/vanilla/index.global.js.map +1 -0
- package/dist/vanilla/index.js +209 -0
- package/dist/vanilla/index.js.map +1 -0
- package/dist/vue/index.cjs +482 -0
- package/dist/vue/index.cjs.map +1 -0
- package/dist/vue/index.d.cts +197 -0
- package/dist/vue/index.d.ts +197 -0
- package/dist/vue/index.js +453 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +70 -0
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
|