perspectapi-ts-sdk 6.5.9 → 7.0.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 +46 -1011
- package/dist/chunk-K3T2AFYA.mjs +1393 -0
- package/dist/index-CWvUyMt3.d.mts +2224 -0
- package/dist/index-CWvUyMt3.d.ts +2224 -0
- package/dist/index.d.mts +130 -2221
- package/dist/index.d.ts +130 -2221
- package/dist/index.js +8 -2
- package/dist/index.mjs +13 -1364
- package/dist/v2/index.d.mts +1 -0
- package/dist/v2/index.d.ts +1 -0
- package/dist/v2/index.js +1419 -0
- package/dist/v2/index.mjs +40 -0
- package/docs/README.md +15 -0
- package/docs/v1-deprecated/README.md +9 -0
- package/docs/v1-deprecated/examples/README.md +324 -0
- package/docs/v1-deprecated/examples/basic-usage.ts +258 -0
- package/docs/v1-deprecated/examples/cloudflare-worker.ts +274 -0
- package/docs/v1-deprecated/examples/content-query-with-slug-prefix.ts +237 -0
- package/docs/v1-deprecated/examples/image-transforms.ts +200 -0
- package/docs/v1-deprecated/examples/site-user-checkout.ts +186 -0
- package/docs/v1-deprecated/examples/slug-prefix-examples.ts +491 -0
- package/docs/v1-deprecated/legacy-docs/caching.md +667 -0
- package/docs/v1-deprecated/legacy-docs/contact.md +1396 -0
- package/docs/v1-deprecated/legacy-docs/csrf-protection.md +664 -0
- package/docs/v1-deprecated/legacy-docs/image-transforms.md +523 -0
- package/docs/v1-deprecated/legacy-docs/loaders.md +304 -0
- package/docs/v1-deprecated/legacy-docs/newsletter.md +811 -0
- package/docs/v1-deprecated/legacy-docs/site-users.md +817 -0
- package/docs/v1-deprecated/legacy-notes/CHANGELOG-CHECKOUT.md +143 -0
- package/docs/v1-deprecated/legacy-notes/CSRF-CHECKOUT.md +271 -0
- package/docs/v1-deprecated/legacy-notes/IMAGE_TRANSFORMS_PORT.md +298 -0
- package/docs/v1-deprecated/sdk-readme.md +1076 -0
- package/examples/README.md +19 -0
- package/examples/basic-v2.ts +37 -0
- package/llms.txt +25 -0
- package/package.json +18 -7
- package/src/client/api-keys-client.ts +4 -0
- package/src/client/auth-client.ts +4 -0
- package/src/client/base-client.ts +7 -0
- package/src/client/bundles-client.ts +4 -0
- package/src/client/categories-client.ts +4 -0
- package/src/client/checkout-client.ts +4 -0
- package/src/client/contact-client.ts +4 -0
- package/src/client/content-client.ts +4 -0
- package/src/client/newsletter-client.ts +4 -0
- package/src/client/newsletter-management-client.ts +4 -0
- package/src/client/organizations-client.ts +4 -0
- package/src/client/products-client.ts +4 -0
- package/src/client/site-users-client.ts +10 -1
- package/src/client/sites-client.ts +4 -0
- package/src/client/webhooks-client.ts +4 -0
- package/src/deprecation.ts +2 -1
- package/src/index.ts +2 -1
- package/src/loaders.ts +59 -0
- package/src/perspect-api-client.ts +2 -2
- package/src/v2/client/orders-client.ts +6 -1
- package/src/v2/types.ts +3 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
# Site Users Guide
|
|
2
|
+
|
|
3
|
+
> Deprecated v1 material. Do not copy these examples into new code. v1 sunsets
|
|
4
|
+
> on 2026-06-01; use `createPerspectApiV2Client` from
|
|
5
|
+
> `perspectapi-ts-sdk/v2` and `/api/v2`.
|
|
6
|
+
|
|
7
|
+
Site Users are per-site customer accounts with OTP-based authentication. Each site can have its own set of users, separate from admin users.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Overview](#overview)
|
|
12
|
+
- [Authentication Flow](#authentication-flow)
|
|
13
|
+
- [Creating Waitlist Users](#creating-waitlist-users)
|
|
14
|
+
- [Managing User Metadata](#managing-user-metadata)
|
|
15
|
+
- [Managing User Profiles](#managing-user-profiles)
|
|
16
|
+
- [Cross-Domain Authentication](#cross-domain-authentication)
|
|
17
|
+
- [User Data Management](#user-data-management)
|
|
18
|
+
- [Orders and Subscriptions](#orders-and-subscriptions)
|
|
19
|
+
- [Admin Operations](#admin-operations)
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
22
|
+
|
|
23
|
+
Site Users provide passwordless authentication using One-Time Passwords (OTP) sent via email. Each user belongs to a specific site and can have:
|
|
24
|
+
|
|
25
|
+
- **Basic profile**: first name, last name, avatar, email verification status
|
|
26
|
+
- **Waitlist flag**: Mark users as waitlist signups before service launch
|
|
27
|
+
- **Metadata**: Arbitrary JSON object for custom per-site fields
|
|
28
|
+
- **Profile key-values**: Structured storage for phone, addresses, preferences, etc.
|
|
29
|
+
- **Order history**: Linked checkout sessions and transactions
|
|
30
|
+
- **Subscriptions**: Payment subscriptions across providers (Stripe, etc.)
|
|
31
|
+
|
|
32
|
+
## Authentication Flow
|
|
33
|
+
|
|
34
|
+
### 1. Request OTP
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { PerspectApiClient } from 'perspectapi-ts-sdk';
|
|
38
|
+
|
|
39
|
+
const client = new PerspectApiClient({
|
|
40
|
+
baseUrl: 'https://api.yoursite.com'
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Request OTP for login/signup
|
|
44
|
+
const response = await client.siteUsers.requestOtp(
|
|
45
|
+
'mysite',
|
|
46
|
+
{ email: 'user@example.com' },
|
|
47
|
+
csrfToken // Required for browser-based requests
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// User receives email with 6-digit code
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Verify OTP and Get Token
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// Option 1: Manual token management (recommended for production)
|
|
57
|
+
const response = await client.siteUsers.verifyOtp(
|
|
58
|
+
'mysite',
|
|
59
|
+
{
|
|
60
|
+
email: 'user@example.com',
|
|
61
|
+
code: '123456'
|
|
62
|
+
},
|
|
63
|
+
csrfToken
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const { token, user } = response.data;
|
|
67
|
+
|
|
68
|
+
// Store token securely:
|
|
69
|
+
// - Option A: httpOnly cookie on YOUR domain (recommended)
|
|
70
|
+
await fetch('/your-api/set-auth-cookie', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
body: JSON.stringify({ token })
|
|
73
|
+
});
|
|
74
|
+
client.setAuth(token);
|
|
75
|
+
|
|
76
|
+
// - Option B: Memory (lost on refresh, most secure)
|
|
77
|
+
client.setAuth(token);
|
|
78
|
+
|
|
79
|
+
// - Option C: localStorage (vulnerable to XSS, not recommended)
|
|
80
|
+
localStorage.setItem('site_user_token', token);
|
|
81
|
+
client.setAuth(token);
|
|
82
|
+
|
|
83
|
+
// Option 2: Automatic token management (memory only)
|
|
84
|
+
const response = await client.siteUsers.verifyOtpAndSetAuth(
|
|
85
|
+
'mysite',
|
|
86
|
+
{ email: 'user@example.com', code: '123456' },
|
|
87
|
+
csrfToken
|
|
88
|
+
);
|
|
89
|
+
// Token automatically set in SDK, lost on page refresh
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Logout
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
await client.siteUsers.logout('mysite');
|
|
96
|
+
// Also clear client-side token storage
|
|
97
|
+
client.setAuth(null);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Creating Waitlist Users
|
|
101
|
+
|
|
102
|
+
Mark users as waitlist signups before your service launches. You can optionally include metadata during the initial signup:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// Request OTP with waitlist flag and initial metadata
|
|
106
|
+
const response = await client.siteUsers.requestOtp(
|
|
107
|
+
'mysite',
|
|
108
|
+
{
|
|
109
|
+
email: 'earlyuser@example.com',
|
|
110
|
+
waitlist: true, // Mark as waitlist signup
|
|
111
|
+
metadata: {
|
|
112
|
+
// Capture context at signup time
|
|
113
|
+
signupSource: 'landing-page',
|
|
114
|
+
referralCode: 'FRIEND123',
|
|
115
|
+
interests: ['product-updates', 'beta-features'],
|
|
116
|
+
utm_campaign: 'launch-2024'
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
csrfToken
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// After verification, user will have waitlist: true and metadata
|
|
123
|
+
const { user } = await client.siteUsers.verifyOtp(
|
|
124
|
+
'mysite',
|
|
125
|
+
{ email: 'earlyuser@example.com', code: '123456' },
|
|
126
|
+
csrfToken
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
console.log(user.waitlist); // true
|
|
130
|
+
console.log(user.metadata); // { signupSource: 'landing-page', ... }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Updating Existing Users' Metadata
|
|
134
|
+
|
|
135
|
+
If a user already exists (returning user requesting OTP again), providing metadata will **update** their existing metadata:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// First signup
|
|
139
|
+
await client.siteUsers.requestOtp('mysite', {
|
|
140
|
+
email: 'user@example.com',
|
|
141
|
+
metadata: { signupSource: 'web' }
|
|
142
|
+
}, csrfToken);
|
|
143
|
+
|
|
144
|
+
// Later, same user requests OTP again with new metadata
|
|
145
|
+
await client.siteUsers.requestOtp('mysite', {
|
|
146
|
+
email: 'user@example.com',
|
|
147
|
+
metadata: { lastLoginSource: 'mobile-app' } // This replaces previous metadata
|
|
148
|
+
}, csrfToken);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Query waitlist users (admin only):
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// Get all waitlist users for a site
|
|
155
|
+
const users = await client.siteUsers.listUsers(
|
|
156
|
+
'mysite',
|
|
157
|
+
{ limit: 100, offset: 0 }
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const waitlistUsers = users.data.users.filter(u => u.waitlist);
|
|
161
|
+
|
|
162
|
+
// Analyze waitlist metadata
|
|
163
|
+
waitlistUsers.forEach(user => {
|
|
164
|
+
console.log(`${user.email}: ${user.metadata?.signupSource}`);
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Managing User Metadata
|
|
169
|
+
|
|
170
|
+
Metadata is an arbitrary JSON object for storing custom per-site fields. The client is responsible for merging - the API does **full replacement**.
|
|
171
|
+
|
|
172
|
+
### Setting Metadata
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Set metadata (full replacement)
|
|
176
|
+
await client.siteUsers.updateMe(
|
|
177
|
+
'mysite',
|
|
178
|
+
{
|
|
179
|
+
metadata: {
|
|
180
|
+
preferences: {
|
|
181
|
+
theme: 'dark',
|
|
182
|
+
notifications: true,
|
|
183
|
+
language: 'en'
|
|
184
|
+
},
|
|
185
|
+
tags: ['premium', 'early-adopter'],
|
|
186
|
+
customField: 'value',
|
|
187
|
+
onboardingCompleted: true
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
csrfToken
|
|
191
|
+
);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Merging Metadata (Client-Side Pattern)
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// 1. Get current user data
|
|
198
|
+
const { data } = await client.siteUsers.getMe('mysite');
|
|
199
|
+
const currentMetadata = data.user.metadata || {};
|
|
200
|
+
|
|
201
|
+
// 2. Deep merge with new data (example using lodash or custom logic)
|
|
202
|
+
import { merge } from 'lodash';
|
|
203
|
+
|
|
204
|
+
const updatedMetadata = merge({}, currentMetadata, {
|
|
205
|
+
preferences: {
|
|
206
|
+
theme: 'light' // Update just this field, keep others
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// 3. Send merged result
|
|
211
|
+
await client.siteUsers.updateMe(
|
|
212
|
+
'mysite',
|
|
213
|
+
{ metadata: updatedMetadata },
|
|
214
|
+
csrfToken
|
|
215
|
+
);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Shallow Merge Pattern
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// Simple shallow merge for top-level keys
|
|
222
|
+
const { data } = await client.siteUsers.getMe('mysite');
|
|
223
|
+
|
|
224
|
+
await client.siteUsers.updateMe(
|
|
225
|
+
'mysite',
|
|
226
|
+
{
|
|
227
|
+
metadata: {
|
|
228
|
+
...data.user.metadata,
|
|
229
|
+
newField: 'value',
|
|
230
|
+
tags: [...(data.user.metadata?.tags || []), 'new-tag']
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
csrfToken
|
|
234
|
+
);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Clearing Metadata
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// Clear all metadata
|
|
241
|
+
await client.siteUsers.updateMe(
|
|
242
|
+
'mysite',
|
|
243
|
+
{ metadata: null },
|
|
244
|
+
csrfToken
|
|
245
|
+
);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Managing User Profiles
|
|
249
|
+
|
|
250
|
+
User profiles are a **separate** key-value store for structured data like phone numbers, addresses, and preferences. Unlike metadata, each key is stored as a separate database record.
|
|
251
|
+
|
|
252
|
+
### Setting Profile Values
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// Set a simple string value
|
|
256
|
+
await client.siteUsers.setProfileValue(
|
|
257
|
+
'mysite',
|
|
258
|
+
'phone',
|
|
259
|
+
'+1-555-0123',
|
|
260
|
+
csrfToken
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Set a complex JSON value (addresses, preferences, etc.)
|
|
264
|
+
await client.siteUsers.setProfileValue(
|
|
265
|
+
'mysite',
|
|
266
|
+
'address_shipping',
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
line1: '123 Main St',
|
|
269
|
+
city: 'San Francisco',
|
|
270
|
+
state: 'CA',
|
|
271
|
+
postal_code: '94102',
|
|
272
|
+
country: 'US'
|
|
273
|
+
}),
|
|
274
|
+
csrfToken
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
await client.siteUsers.setProfileValue(
|
|
278
|
+
'mysite',
|
|
279
|
+
'address_billing',
|
|
280
|
+
JSON.stringify({
|
|
281
|
+
line1: '456 Oak Ave',
|
|
282
|
+
city: 'Portland',
|
|
283
|
+
state: 'OR',
|
|
284
|
+
postal_code: '97201',
|
|
285
|
+
country: 'US'
|
|
286
|
+
}),
|
|
287
|
+
csrfToken
|
|
288
|
+
);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Getting Profile Data
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// Get all profile key-values
|
|
295
|
+
const { data } = await client.siteUsers.getProfile('mysite');
|
|
296
|
+
|
|
297
|
+
// Access profile values
|
|
298
|
+
console.log(data.profile.phone?.value); // '+1-555-0123'
|
|
299
|
+
console.log(data.profile.address_shipping?.value); // { line1: '123 Main St', ... }
|
|
300
|
+
console.log(data.profile.address_shipping?.updated_at); // '2024-01-15T12:00:00Z'
|
|
301
|
+
|
|
302
|
+
// Or get user with profile in one request
|
|
303
|
+
const response = await client.siteUsers.getMe('mysite');
|
|
304
|
+
console.log(response.data.user); // Basic user fields
|
|
305
|
+
console.log(response.data.profile); // All profile key-values
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Deleting Profile Values
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// Delete a specific profile key
|
|
312
|
+
await client.siteUsers.deleteProfileValue(
|
|
313
|
+
'mysite',
|
|
314
|
+
'phone',
|
|
315
|
+
csrfToken
|
|
316
|
+
);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Common Profile Keys
|
|
320
|
+
|
|
321
|
+
Suggested key naming conventions:
|
|
322
|
+
|
|
323
|
+
- `phone` - Primary phone number
|
|
324
|
+
- `whatsapp` - WhatsApp number
|
|
325
|
+
- `address_shipping` - Shipping address (JSON)
|
|
326
|
+
- `address_billing` - Billing address (JSON)
|
|
327
|
+
- `preferences` - User preferences (JSON)
|
|
328
|
+
- `notifications` - Notification settings (JSON)
|
|
329
|
+
- `social_twitter` - Twitter handle
|
|
330
|
+
- `social_linkedin` - LinkedIn URL
|
|
331
|
+
|
|
332
|
+
## Cross-Domain Authentication
|
|
333
|
+
|
|
334
|
+
Site users support cross-domain authentication using JWT tokens in the Authorization header.
|
|
335
|
+
|
|
336
|
+
### Same-Domain Setup (Default)
|
|
337
|
+
|
|
338
|
+
If your client app is on the **same domain** as the API:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// The API automatically sets httpOnly cookies
|
|
342
|
+
await client.siteUsers.verifyOtp('mysite', { email, code }, csrfToken);
|
|
343
|
+
|
|
344
|
+
// Cookie is automatically sent with subsequent requests
|
|
345
|
+
await client.siteUsers.getMe('mysite');
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Cross-Domain Setup (Token-Based)
|
|
349
|
+
|
|
350
|
+
If your client app is on a **different domain** than the API:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// 1. Verify OTP and get token
|
|
354
|
+
const response = await client.siteUsers.verifyOtp(
|
|
355
|
+
'mysite',
|
|
356
|
+
{ email, code },
|
|
357
|
+
csrfToken
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const { token } = response.data;
|
|
361
|
+
|
|
362
|
+
// 2. Store token on YOUR domain (httpOnly cookie via your API)
|
|
363
|
+
await fetch('https://yourclient.com/api/set-auth', {
|
|
364
|
+
method: 'POST',
|
|
365
|
+
credentials: 'include',
|
|
366
|
+
body: JSON.stringify({ token })
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// 3. Set token in SDK for Authorization header
|
|
370
|
+
client.setAuth(token);
|
|
371
|
+
|
|
372
|
+
// 4. All subsequent requests include Authorization: Bearer <token>
|
|
373
|
+
await client.siteUsers.getMe('mysite');
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## User Data Management
|
|
377
|
+
|
|
378
|
+
### Updating User Profile
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// Update basic user fields
|
|
382
|
+
await client.siteUsers.updateMe(
|
|
383
|
+
'mysite',
|
|
384
|
+
{
|
|
385
|
+
first_name: 'John',
|
|
386
|
+
last_name: 'Doe',
|
|
387
|
+
avatar_url: 'https://example.com/avatar.jpg',
|
|
388
|
+
metadata: {
|
|
389
|
+
// ... custom metadata
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
csrfToken
|
|
393
|
+
);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Getting Current User
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
const { data } = await client.siteUsers.getMe('mysite');
|
|
400
|
+
|
|
401
|
+
console.log(data.user);
|
|
402
|
+
// {
|
|
403
|
+
// id: 'user_abc123',
|
|
404
|
+
// email: 'user@example.com',
|
|
405
|
+
// first_name: 'John',
|
|
406
|
+
// last_name: 'Doe',
|
|
407
|
+
// avatar_url: 'https://...',
|
|
408
|
+
// status: 'active',
|
|
409
|
+
// email_verified: true,
|
|
410
|
+
// waitlist: false,
|
|
411
|
+
// metadata: { ... },
|
|
412
|
+
// created_at: '2024-01-01T00:00:00Z',
|
|
413
|
+
// last_login_at: '2024-01-15T12:00:00Z'
|
|
414
|
+
// }
|
|
415
|
+
|
|
416
|
+
console.log(data.profile);
|
|
417
|
+
// {
|
|
418
|
+
// phone: { value: '+1-555-0123', updated_at: '...' },
|
|
419
|
+
// address_shipping: { value: { line1: '...', ... }, updated_at: '...' }
|
|
420
|
+
// }
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Orders and Subscriptions
|
|
424
|
+
|
|
425
|
+
### Getting Order History
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
// Get all orders for current user
|
|
429
|
+
const { data } = await client.siteUsers.getOrders('mysite', {
|
|
430
|
+
limit: 50,
|
|
431
|
+
offset: 0
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
console.log(data.orders);
|
|
435
|
+
// [
|
|
436
|
+
// {
|
|
437
|
+
// session_id: 'cs_abc123',
|
|
438
|
+
// order_id: 'ord_xyz789',
|
|
439
|
+
// customer_email: 'user@example.com',
|
|
440
|
+
// amount_total: 4999,
|
|
441
|
+
// currency: 'usd',
|
|
442
|
+
// status: 'complete',
|
|
443
|
+
// payment_status: 'paid',
|
|
444
|
+
// line_items: [...],
|
|
445
|
+
// created_at: '2024-01-15T12:00:00Z'
|
|
446
|
+
// }
|
|
447
|
+
// ]
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Getting Single Order
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// Get order detail by ID or session ID
|
|
454
|
+
const { data } = await client.siteUsers.getOrder('mysite', 'ord_xyz789');
|
|
455
|
+
|
|
456
|
+
console.log(data.order);
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Managing Subscriptions
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
// Get all subscriptions
|
|
463
|
+
const { data } = await client.siteUsers.getSubscriptions('mysite', {
|
|
464
|
+
limit: 50,
|
|
465
|
+
offset: 0
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
console.log(data.subscriptions);
|
|
469
|
+
// [
|
|
470
|
+
// {
|
|
471
|
+
// id: 'sub_abc123',
|
|
472
|
+
// provider: 'stripe',
|
|
473
|
+
// provider_subscription_id: 'sub_stripe123',
|
|
474
|
+
// plan_name: 'Pro Plan',
|
|
475
|
+
// plan_id: 'price_123',
|
|
476
|
+
// status: 'active',
|
|
477
|
+
// amount: 2999,
|
|
478
|
+
// currency: 'usd',
|
|
479
|
+
// billing_interval: 'month',
|
|
480
|
+
// current_period_start: '2024-01-01T00:00:00Z',
|
|
481
|
+
// current_period_end: '2024-02-01T00:00:00Z',
|
|
482
|
+
// cancel_at_period_end: false,
|
|
483
|
+
// created_at: '2024-01-01T00:00:00Z'
|
|
484
|
+
// }
|
|
485
|
+
// ]
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Getting Single Subscription
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
const { data } = await client.siteUsers.getSubscription('mysite', 'sub_abc123');
|
|
492
|
+
|
|
493
|
+
console.log(data.subscription);
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Canceling Subscription
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
// Cancel subscription at period end (default behavior)
|
|
500
|
+
const response = await client.siteUsers.cancelSubscription(
|
|
501
|
+
'mysite',
|
|
502
|
+
'sub_abc123',
|
|
503
|
+
csrfToken
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
console.log(response.data);
|
|
507
|
+
// { success: true, message: 'Subscription will be canceled at period end' }
|
|
508
|
+
|
|
509
|
+
// Cancel immediately
|
|
510
|
+
await client.siteUsers.cancelSubscription(
|
|
511
|
+
'mysite',
|
|
512
|
+
'sub_abc123',
|
|
513
|
+
csrfToken,
|
|
514
|
+
{ mode: 'immediate' }
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
// Cancel at a scheduled timestamp (ISO)
|
|
518
|
+
await client.siteUsers.cancelSubscription(
|
|
519
|
+
'mysite',
|
|
520
|
+
'sub_abc123',
|
|
521
|
+
csrfToken,
|
|
522
|
+
{ mode: 'scheduled', cancel_at: '2026-06-15T14:00:00Z' }
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
// Cancel after a configured delay
|
|
526
|
+
await client.siteUsers.cancelSubscription(
|
|
527
|
+
'mysite',
|
|
528
|
+
'sub_abc123',
|
|
529
|
+
csrfToken,
|
|
530
|
+
{ mode: 'scheduled', delay_days: 3, delay_hours: 6 }
|
|
531
|
+
);
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Getting Newsletter Subscriptions
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
// Get linked newsletter subscriptions
|
|
538
|
+
const { data } = await client.siteUsers.getNewsletterSubscriptions('mysite');
|
|
539
|
+
|
|
540
|
+
console.log(data.newsletters);
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## Admin Operations
|
|
544
|
+
|
|
545
|
+
These endpoints require API key authentication (not site user JWT).
|
|
546
|
+
|
|
547
|
+
### Listing All Users
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
// Initialize client with API key
|
|
551
|
+
const adminClient = new PerspectApiClient({
|
|
552
|
+
baseUrl: 'https://api.yoursite.com',
|
|
553
|
+
apiKey: 'your-api-key'
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// List all users for a site
|
|
557
|
+
const { data } = await adminClient.siteUsers.listUsers(
|
|
558
|
+
'mysite',
|
|
559
|
+
{
|
|
560
|
+
limit: 100,
|
|
561
|
+
offset: 0,
|
|
562
|
+
status: 'active' // Optional: filter by status
|
|
563
|
+
}
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
console.log(data.users);
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Getting User Detail (Admin)
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
// Get full user details including profile
|
|
573
|
+
const { data } = await adminClient.siteUsers.getUser('mysite', 'user_abc123');
|
|
574
|
+
|
|
575
|
+
console.log(data.user);
|
|
576
|
+
console.log(data.profile);
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Updating User Status (Admin)
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
// Suspend a user
|
|
583
|
+
await adminClient.siteUsers.updateUserStatus(
|
|
584
|
+
'mysite',
|
|
585
|
+
'user_abc123',
|
|
586
|
+
'suspended',
|
|
587
|
+
csrfToken
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
// Reactivate a user
|
|
591
|
+
await adminClient.siteUsers.updateUserStatus(
|
|
592
|
+
'mysite',
|
|
593
|
+
'user_abc123',
|
|
594
|
+
'active',
|
|
595
|
+
csrfToken
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
// Available statuses: 'active' | 'suspended' | 'pending_verification'
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
## Complete Example: User Registration Flow
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
import { PerspectApiClient } from 'perspectapi-ts-sdk';
|
|
605
|
+
|
|
606
|
+
const client = new PerspectApiClient({
|
|
607
|
+
baseUrl: 'https://api.yoursite.com'
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// 1. Request OTP with waitlist flag and initial metadata
|
|
611
|
+
async function startSignup(
|
|
612
|
+
email: string,
|
|
613
|
+
isWaitlist: boolean,
|
|
614
|
+
signupContext?: {
|
|
615
|
+
source?: string;
|
|
616
|
+
referralCode?: string;
|
|
617
|
+
utmParams?: Record<string, string>;
|
|
618
|
+
}
|
|
619
|
+
) {
|
|
620
|
+
const csrfToken = await getCsrfToken(); // Get from your CSRF endpoint
|
|
621
|
+
|
|
622
|
+
const response = await client.siteUsers.requestOtp(
|
|
623
|
+
'mysite',
|
|
624
|
+
{
|
|
625
|
+
email,
|
|
626
|
+
waitlist: isWaitlist,
|
|
627
|
+
metadata: {
|
|
628
|
+
signupSource: signupContext?.source || 'direct',
|
|
629
|
+
referralCode: signupContext?.referralCode,
|
|
630
|
+
utmParams: signupContext?.utmParams,
|
|
631
|
+
signupTimestamp: new Date().toISOString()
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
csrfToken
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
return response.data.success;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 2. Verify OTP and complete signup
|
|
641
|
+
async function completeSignup(email: string, code: string) {
|
|
642
|
+
const csrfToken = await getCsrfToken();
|
|
643
|
+
|
|
644
|
+
const response = await client.siteUsers.verifyOtp(
|
|
645
|
+
'mysite',
|
|
646
|
+
{ email, code },
|
|
647
|
+
csrfToken
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
const { token, user } = response.data;
|
|
651
|
+
|
|
652
|
+
// Store token securely (e.g., httpOnly cookie via your API)
|
|
653
|
+
await storeAuthToken(token);
|
|
654
|
+
|
|
655
|
+
// Set auth for SDK
|
|
656
|
+
client.setAuth(token);
|
|
657
|
+
|
|
658
|
+
return user;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// 3. Complete user profile
|
|
662
|
+
async function completeProfile(profileData: {
|
|
663
|
+
first_name: string;
|
|
664
|
+
last_name: string;
|
|
665
|
+
phone: string;
|
|
666
|
+
metadata?: any;
|
|
667
|
+
}) {
|
|
668
|
+
const csrfToken = await getCsrfToken();
|
|
669
|
+
|
|
670
|
+
// Update basic profile
|
|
671
|
+
await client.siteUsers.updateMe(
|
|
672
|
+
'mysite',
|
|
673
|
+
{
|
|
674
|
+
first_name: profileData.first_name,
|
|
675
|
+
last_name: profileData.last_name,
|
|
676
|
+
metadata: {
|
|
677
|
+
onboardingCompleted: true,
|
|
678
|
+
signupSource: 'website',
|
|
679
|
+
...profileData.metadata
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
csrfToken
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
// Set phone in profile key-values
|
|
686
|
+
await client.siteUsers.setProfileValue(
|
|
687
|
+
'mysite',
|
|
688
|
+
'phone',
|
|
689
|
+
profileData.phone,
|
|
690
|
+
csrfToken
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
// Get updated user data
|
|
694
|
+
const { data } = await client.siteUsers.getMe('mysite');
|
|
695
|
+
return data;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Usage
|
|
699
|
+
async function registerUser() {
|
|
700
|
+
// Step 1: Request OTP with signup context
|
|
701
|
+
await startSignup('user@example.com', false, {
|
|
702
|
+
source: 'landing-page',
|
|
703
|
+
referralCode: 'FRIEND123',
|
|
704
|
+
utmParams: {
|
|
705
|
+
utm_source: 'google',
|
|
706
|
+
utm_campaign: 'spring-2024'
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
console.log('OTP sent to email');
|
|
710
|
+
|
|
711
|
+
// Step 2: User enters code from email
|
|
712
|
+
const user = await completeSignup('user@example.com', '123456');
|
|
713
|
+
console.log('Signup successful:', user);
|
|
714
|
+
console.log('Signup metadata:', user.metadata);
|
|
715
|
+
|
|
716
|
+
// Step 3: Complete profile
|
|
717
|
+
const userData = await completeProfile({
|
|
718
|
+
first_name: 'John',
|
|
719
|
+
last_name: 'Doe',
|
|
720
|
+
phone: '+1-555-0123',
|
|
721
|
+
metadata: {
|
|
722
|
+
referralSource: 'friend',
|
|
723
|
+
interests: ['tech', 'design']
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
console.log('Profile complete:', userData);
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
## Best Practices
|
|
732
|
+
|
|
733
|
+
### Security
|
|
734
|
+
|
|
735
|
+
1. **Always use CSRF tokens** for browser-based requests
|
|
736
|
+
2. **Store tokens securely**:
|
|
737
|
+
- Production: httpOnly cookies on your domain
|
|
738
|
+
- Development: Memory (lost on refresh)
|
|
739
|
+
- Avoid: localStorage (vulnerable to XSS)
|
|
740
|
+
3. **Validate email addresses** before requesting OTP
|
|
741
|
+
4. **Rate limit OTP requests** on your frontend
|
|
742
|
+
|
|
743
|
+
### Data Organization
|
|
744
|
+
|
|
745
|
+
1. **Use metadata for**:
|
|
746
|
+
- Unstructured custom fields
|
|
747
|
+
- Site-specific configuration
|
|
748
|
+
- Temporary flags or states
|
|
749
|
+
- Analytics data
|
|
750
|
+
|
|
751
|
+
2. **Use profile key-values for**:
|
|
752
|
+
- Structured data (addresses, phone numbers)
|
|
753
|
+
- Data that needs individual timestamps
|
|
754
|
+
- Data that may be queried separately
|
|
755
|
+
- User preferences with granular updates
|
|
756
|
+
|
|
757
|
+
### Performance
|
|
758
|
+
|
|
759
|
+
1. **Batch profile updates** when possible
|
|
760
|
+
2. **Cache user data** client-side (with TTL)
|
|
761
|
+
3. **Use pagination** for order/subscription lists
|
|
762
|
+
4. **Merge metadata client-side** to avoid fetch-update races
|
|
763
|
+
|
|
764
|
+
## TypeScript Types
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
interface SiteUser {
|
|
768
|
+
id: string;
|
|
769
|
+
email: string;
|
|
770
|
+
first_name?: string;
|
|
771
|
+
last_name?: string;
|
|
772
|
+
avatar_url?: string;
|
|
773
|
+
status: 'active' | 'suspended' | 'pending_verification';
|
|
774
|
+
email_verified: boolean;
|
|
775
|
+
waitlist: boolean;
|
|
776
|
+
metadata?: Record<string, any>;
|
|
777
|
+
created_at: string;
|
|
778
|
+
last_login_at?: string;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
interface SiteUserProfile {
|
|
782
|
+
[key: string]: {
|
|
783
|
+
value: any;
|
|
784
|
+
updated_at: string;
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
interface RequestOtpRequest {
|
|
789
|
+
email: string;
|
|
790
|
+
waitlist?: boolean;
|
|
791
|
+
metadata?: Record<string, any>; // Optional metadata to set on user creation/update
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
interface VerifyOtpRequest {
|
|
795
|
+
email: string;
|
|
796
|
+
code: string;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
interface VerifyOtpResponse {
|
|
800
|
+
success: boolean;
|
|
801
|
+
token: string;
|
|
802
|
+
user: {
|
|
803
|
+
id: string;
|
|
804
|
+
email: string;
|
|
805
|
+
first_name?: string;
|
|
806
|
+
last_name?: string;
|
|
807
|
+
avatar_url?: string;
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
interface UpdateSiteUserRequest {
|
|
812
|
+
first_name?: string;
|
|
813
|
+
last_name?: string;
|
|
814
|
+
avatar_url?: string;
|
|
815
|
+
metadata?: Record<string, any>;
|
|
816
|
+
}
|
|
817
|
+
```
|