@devwithbobby/loops 0.1.4 → 0.1.6
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 +45 -24
- package/dist/client/index.d.ts +4 -4
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +22 -0
- package/dist/component/lib.d.ts +2 -0
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +65 -0
- package/package.json +1 -1
- package/src/client/index.ts +42 -2
- package/src/component/lib.ts +77 -0
package/README.md
CHANGED
|
@@ -124,8 +124,7 @@ await loops.addContact(ctx, {
|
|
|
124
124
|
#### Update Contact
|
|
125
125
|
|
|
126
126
|
```typescript
|
|
127
|
-
await loops.updateContact(ctx, {
|
|
128
|
-
email: "user@example.com",
|
|
127
|
+
await loops.updateContact(ctx, "user@example.com", {
|
|
129
128
|
firstName: "Jane",
|
|
130
129
|
userGroup: "vip",
|
|
131
130
|
});
|
|
@@ -134,17 +133,13 @@ await loops.updateContact(ctx, {
|
|
|
134
133
|
#### Find Contact
|
|
135
134
|
|
|
136
135
|
```typescript
|
|
137
|
-
const contact = await loops.findContact(ctx,
|
|
138
|
-
email: "user@example.com",
|
|
139
|
-
});
|
|
136
|
+
const contact = await loops.findContact(ctx, "user@example.com");
|
|
140
137
|
```
|
|
141
138
|
|
|
142
139
|
#### Delete Contact
|
|
143
140
|
|
|
144
141
|
```typescript
|
|
145
|
-
await loops.deleteContact(ctx,
|
|
146
|
-
email: "user@example.com",
|
|
147
|
-
});
|
|
142
|
+
await loops.deleteContact(ctx, "user@example.com");
|
|
148
143
|
```
|
|
149
144
|
|
|
150
145
|
#### Batch Create Contacts
|
|
@@ -161,8 +156,8 @@ await loops.batchCreateContacts(ctx, {
|
|
|
161
156
|
#### Unsubscribe/Resubscribe
|
|
162
157
|
|
|
163
158
|
```typescript
|
|
164
|
-
await loops.unsubscribeContact(ctx,
|
|
165
|
-
await loops.resubscribeContact(ctx,
|
|
159
|
+
await loops.unsubscribeContact(ctx, "user@example.com");
|
|
160
|
+
await loops.resubscribeContact(ctx, "user@example.com");
|
|
166
161
|
```
|
|
167
162
|
|
|
168
163
|
#### Count Contacts
|
|
@@ -383,33 +378,59 @@ export const addContact = action({
|
|
|
383
378
|
## Security Best Practices
|
|
384
379
|
|
|
385
380
|
1. **Always add authentication** - Wrap all functions with auth checks
|
|
386
|
-
2. **Use environment variables** - Store API key in Convex environment variables
|
|
387
|
-
3. **Implement rate limiting** - Use the built-in rate limiting queries
|
|
381
|
+
2. **Use environment variables** - Store API key in Convex environment variables (never hardcode)
|
|
382
|
+
3. **Implement rate limiting** - Use the built-in rate limiting queries to prevent abuse
|
|
388
383
|
4. **Monitor for abuse** - Use spam detection queries to identify suspicious patterns
|
|
389
384
|
5. **Sanitize errors** - Don't expose sensitive error details to clients
|
|
390
385
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
## Monitoring & Rate Limiting
|
|
394
|
-
|
|
395
|
-
The component automatically logs all email operations for monitoring. Use the built-in queries to:
|
|
386
|
+
### Authentication Example
|
|
396
387
|
|
|
397
|
-
|
|
398
|
-
- Detect spam patterns
|
|
399
|
-
- Enforce rate limits
|
|
400
|
-
- Monitor for abuse
|
|
388
|
+
All functions should be wrapped with authentication:
|
|
401
389
|
|
|
402
|
-
|
|
390
|
+
```typescript
|
|
391
|
+
export const addContact = action({
|
|
392
|
+
args: { email: v.string(), ... },
|
|
393
|
+
handler: async (ctx, args) => {
|
|
394
|
+
// Add authentication check
|
|
395
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
396
|
+
if (!identity) throw new Error("Unauthorized");
|
|
397
|
+
|
|
398
|
+
// Add authorization checks if needed
|
|
399
|
+
// if (!isAdmin(identity)) throw new Error("Forbidden");
|
|
400
|
+
|
|
401
|
+
return await loops.addContact(ctx, args);
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
```
|
|
403
405
|
|
|
404
|
-
|
|
406
|
+
### Environment Variables
|
|
405
407
|
|
|
406
408
|
Set `LOOPS_API_KEY` in your Convex environment:
|
|
407
409
|
|
|
410
|
+
**Via CLI:**
|
|
408
411
|
```bash
|
|
409
412
|
npx convex env set LOOPS_API_KEY "your-api-key"
|
|
410
413
|
```
|
|
411
414
|
|
|
412
|
-
|
|
415
|
+
**Via Dashboard:**
|
|
416
|
+
1. Go to your Convex Dashboard
|
|
417
|
+
2. Navigate to Settings → Environment Variables
|
|
418
|
+
3. Add `LOOPS_API_KEY` with your Loops.so API key value
|
|
419
|
+
|
|
420
|
+
Get your API key from [Loops.so Dashboard](https://app.loops.so/settings/api).
|
|
421
|
+
|
|
422
|
+
⚠️ **Never** pass the API key directly in code or via function options in production. Always use environment variables.
|
|
423
|
+
|
|
424
|
+
## Monitoring & Rate Limiting
|
|
425
|
+
|
|
426
|
+
The component automatically logs all email operations to the `emailOperations` table for monitoring. Use the built-in queries to:
|
|
427
|
+
|
|
428
|
+
- **Track email statistics** - See total sends, success/failure rates, breakdowns by type
|
|
429
|
+
- **Detect spam patterns** - Identify suspicious activity by recipient or actor
|
|
430
|
+
- **Enforce rate limits** - Prevent abuse with recipient, actor, or global rate limits
|
|
431
|
+
- **Monitor for abuse** - Detect rapid-fire patterns and unusual sending behavior
|
|
432
|
+
|
|
433
|
+
All monitoring queries are available through the `Loops` client - see the [Monitoring & Analytics](#monitoring--analytics) section above for usage examples.
|
|
413
434
|
|
|
414
435
|
## Development
|
|
415
436
|
|
package/dist/client/index.d.ts
CHANGED
|
@@ -21,13 +21,13 @@ export interface EventOptions {
|
|
|
21
21
|
eventProperties?: Record<string, any>;
|
|
22
22
|
}
|
|
23
23
|
export declare class Loops {
|
|
24
|
-
component
|
|
25
|
-
options?: {
|
|
24
|
+
private readonly component;
|
|
25
|
+
readonly options?: {
|
|
26
26
|
apiKey?: string;
|
|
27
|
-
}
|
|
27
|
+
};
|
|
28
28
|
constructor(component: LoopsComponent, options?: {
|
|
29
29
|
apiKey?: string;
|
|
30
|
-
}
|
|
30
|
+
});
|
|
31
31
|
private readonly apiKey;
|
|
32
32
|
/**
|
|
33
33
|
* Add or update a contact in Loops
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gCAAgC,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpE,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;AAE5C,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACzC,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACtC;AAED,qBAAa,KAAK;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
|
package/dist/client/index.js
CHANGED
|
@@ -4,6 +4,17 @@ export class Loops {
|
|
|
4
4
|
component;
|
|
5
5
|
options;
|
|
6
6
|
constructor(component, options) {
|
|
7
|
+
if (!component) {
|
|
8
|
+
throw new Error("Loops component reference is required. " +
|
|
9
|
+
"Make sure the component is mounted in your convex.config.ts and use: " +
|
|
10
|
+
"new Loops(components.loops)");
|
|
11
|
+
}
|
|
12
|
+
if (!component.lib) {
|
|
13
|
+
throw new Error("Invalid component reference. " +
|
|
14
|
+
"The component may not be properly mounted. " +
|
|
15
|
+
"Ensure the component is correctly mounted in convex.config.ts: " +
|
|
16
|
+
"app.use(loops);");
|
|
17
|
+
}
|
|
7
18
|
this.component = component;
|
|
8
19
|
this.options = options;
|
|
9
20
|
const apiKey = options?.apiKey ?? process.env.LOOPS_API_KEY;
|
|
@@ -22,6 +33,17 @@ export class Loops {
|
|
|
22
33
|
* Add or update a contact in Loops
|
|
23
34
|
*/
|
|
24
35
|
async addContact(ctx, contact) {
|
|
36
|
+
if (!this.component) {
|
|
37
|
+
throw new Error("Loops component is not initialized. " +
|
|
38
|
+
"Make sure to pass components.loops to the Loops constructor: " +
|
|
39
|
+
"new Loops(components.loops)");
|
|
40
|
+
}
|
|
41
|
+
if (!this.component.lib) {
|
|
42
|
+
throw new Error("Invalid component reference. " +
|
|
43
|
+
"The component may not be properly mounted. " +
|
|
44
|
+
"Ensure the component is correctly mounted in convex.config.ts: " +
|
|
45
|
+
"app.use(loops);");
|
|
46
|
+
}
|
|
25
47
|
return ctx.runAction(this.component.lib.addContact, {
|
|
26
48
|
apiKey: this.apiKey,
|
|
27
49
|
contact,
|
package/dist/component/lib.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export declare const logEmailOperation: any;
|
|
|
17
17
|
export declare const countContacts: any;
|
|
18
18
|
/**
|
|
19
19
|
* Add or update a contact in Loops
|
|
20
|
+
* This function tries to create a contact, and if the email already exists (409),
|
|
21
|
+
* it falls back to updating the contact instead.
|
|
20
22
|
*/
|
|
21
23
|
export declare const addContact: any;
|
|
22
24
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AA0BA;;GAEG;AACH,eAAO,MAAM,YAAY,KA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,KAgC5B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa,KAwCxB,CAAC;AAEH
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AA0BA;;GAEG;AACH,eAAO,MAAM,YAAY,KA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,KAgC5B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa,KAwCxB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU,KAsHrB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAoDxB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,KAqD5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS,KAgCpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KA8BxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,YAAY,KAkFvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,KA+CtB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,WAAW,KAiDtB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KA4C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KAwC9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe,KAwC1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAgDxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KAsGlC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KA6ClC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KA6C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,KA8B/B,CAAC"}
|
package/dist/component/lib.js
CHANGED
|
@@ -177,6 +177,8 @@ export const countContacts = zq({
|
|
|
177
177
|
});
|
|
178
178
|
/**
|
|
179
179
|
* Add or update a contact in Loops
|
|
180
|
+
* This function tries to create a contact, and if the email already exists (409),
|
|
181
|
+
* it falls back to updating the contact instead.
|
|
180
182
|
*/
|
|
181
183
|
export const addContact = za({
|
|
182
184
|
args: z.object({
|
|
@@ -188,6 +190,7 @@ export const addContact = za({
|
|
|
188
190
|
id: z.string().optional(),
|
|
189
191
|
}),
|
|
190
192
|
handler: async (ctx, args) => {
|
|
193
|
+
// First, try to create the contact
|
|
191
194
|
const response = await fetch(`${LOOPS_API_BASE_URL}/contacts/create`, {
|
|
192
195
|
method: "POST",
|
|
193
196
|
headers: {
|
|
@@ -198,9 +201,71 @@ export const addContact = za({
|
|
|
198
201
|
});
|
|
199
202
|
if (!response.ok) {
|
|
200
203
|
const errorText = await response.text();
|
|
204
|
+
// If contact already exists (409), fall back to update
|
|
205
|
+
if (response.status === 409) {
|
|
206
|
+
console.log(`Contact ${args.contact.email} already exists, updating instead`);
|
|
207
|
+
// Try to find the existing contact to get their ID
|
|
208
|
+
const findResponse = await fetch(`${LOOPS_API_BASE_URL}/contacts/find?email=${encodeURIComponent(args.contact.email)}`, {
|
|
209
|
+
method: "GET",
|
|
210
|
+
headers: {
|
|
211
|
+
Authorization: `Bearer ${args.apiKey}`,
|
|
212
|
+
"Content-Type": "application/json",
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
if (!findResponse.ok) {
|
|
216
|
+
const findErrorText = await findResponse.text();
|
|
217
|
+
console.error(`Failed to find existing contact [${findResponse.status}]:`, findErrorText);
|
|
218
|
+
// Continue with update anyway - it should work
|
|
219
|
+
}
|
|
220
|
+
// Update the contact instead
|
|
221
|
+
const updateResponse = await fetch(`${LOOPS_API_BASE_URL}/contacts/update`, {
|
|
222
|
+
method: "PUT",
|
|
223
|
+
headers: {
|
|
224
|
+
Authorization: `Bearer ${args.apiKey}`,
|
|
225
|
+
"Content-Type": "application/json",
|
|
226
|
+
},
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
email: args.contact.email,
|
|
229
|
+
firstName: args.contact.firstName,
|
|
230
|
+
lastName: args.contact.lastName,
|
|
231
|
+
userId: args.contact.userId,
|
|
232
|
+
source: args.contact.source,
|
|
233
|
+
subscribed: args.contact.subscribed,
|
|
234
|
+
userGroup: args.contact.userGroup,
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
if (!updateResponse.ok) {
|
|
238
|
+
const updateErrorText = await updateResponse.text();
|
|
239
|
+
console.error(`Loops API error [${updateResponse.status}]:`, updateErrorText);
|
|
240
|
+
throw sanitizeError(updateResponse.status, updateErrorText);
|
|
241
|
+
}
|
|
242
|
+
// Get contact ID if available
|
|
243
|
+
let contactId;
|
|
244
|
+
if (findResponse.ok) {
|
|
245
|
+
const findData = (await findResponse.json());
|
|
246
|
+
contactId = findData.id;
|
|
247
|
+
}
|
|
248
|
+
// Store/update in our database
|
|
249
|
+
await ctx.runMutation((internal.lib).storeContact, {
|
|
250
|
+
email: args.contact.email,
|
|
251
|
+
firstName: args.contact.firstName,
|
|
252
|
+
lastName: args.contact.lastName,
|
|
253
|
+
userId: args.contact.userId,
|
|
254
|
+
source: args.contact.source,
|
|
255
|
+
subscribed: args.contact.subscribed,
|
|
256
|
+
userGroup: args.contact.userGroup,
|
|
257
|
+
loopsContactId: contactId,
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
success: true,
|
|
261
|
+
id: contactId,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// For other errors, throw as normal
|
|
201
265
|
console.error(`Loops API error [${response.status}]:`, errorText);
|
|
202
266
|
throw sanitizeError(response.status, errorText);
|
|
203
267
|
}
|
|
268
|
+
// Contact was created successfully
|
|
204
269
|
const data = (await response.json());
|
|
205
270
|
await ctx.runMutation((internal.lib).storeContact, {
|
|
206
271
|
email: args.contact.email,
|
package/package.json
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -28,12 +28,37 @@ export interface EventOptions {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export class Loops {
|
|
31
|
+
private readonly component: LoopsComponent;
|
|
32
|
+
public readonly options?: {
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
31
36
|
constructor(
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
component: LoopsComponent,
|
|
38
|
+
options?: {
|
|
34
39
|
apiKey?: string;
|
|
35
40
|
},
|
|
36
41
|
) {
|
|
42
|
+
if (!component) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Loops component reference is required. " +
|
|
45
|
+
"Make sure the component is mounted in your convex.config.ts and use: " +
|
|
46
|
+
"new Loops(components.loops)"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!component.lib) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"Invalid component reference. " +
|
|
53
|
+
"The component may not be properly mounted. " +
|
|
54
|
+
"Ensure the component is correctly mounted in convex.config.ts: " +
|
|
55
|
+
"app.use(loops);"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.component = component;
|
|
60
|
+
this.options = options;
|
|
61
|
+
|
|
37
62
|
const apiKey = options?.apiKey ?? process.env.LOOPS_API_KEY;
|
|
38
63
|
if (!apiKey) {
|
|
39
64
|
throw new Error(
|
|
@@ -58,6 +83,21 @@ export class Loops {
|
|
|
58
83
|
* Add or update a contact in Loops
|
|
59
84
|
*/
|
|
60
85
|
async addContact(ctx: RunActionCtx, contact: ContactData) {
|
|
86
|
+
if (!this.component) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Loops component is not initialized. " +
|
|
89
|
+
"Make sure to pass components.loops to the Loops constructor: " +
|
|
90
|
+
"new Loops(components.loops)"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (!this.component.lib) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
"Invalid component reference. " +
|
|
96
|
+
"The component may not be properly mounted. " +
|
|
97
|
+
"Ensure the component is correctly mounted in convex.config.ts: " +
|
|
98
|
+
"app.use(loops);"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
61
101
|
return ctx.runAction((this.component.lib as any).addContact, {
|
|
62
102
|
apiKey: this.apiKey,
|
|
63
103
|
contact,
|
package/src/component/lib.ts
CHANGED
|
@@ -179,6 +179,8 @@ export const countContacts = zq({
|
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
181
|
* Add or update a contact in Loops
|
|
182
|
+
* This function tries to create a contact, and if the email already exists (409),
|
|
183
|
+
* it falls back to updating the contact instead.
|
|
182
184
|
*/
|
|
183
185
|
export const addContact = za({
|
|
184
186
|
args: z.object({
|
|
@@ -190,6 +192,7 @@ export const addContact = za({
|
|
|
190
192
|
id: z.string().optional(),
|
|
191
193
|
}),
|
|
192
194
|
handler: async (ctx, args) => {
|
|
195
|
+
// First, try to create the contact
|
|
193
196
|
const response = await fetch(`${LOOPS_API_BASE_URL}/contacts/create`, {
|
|
194
197
|
method: "POST",
|
|
195
198
|
headers: {
|
|
@@ -201,10 +204,84 @@ export const addContact = za({
|
|
|
201
204
|
|
|
202
205
|
if (!response.ok) {
|
|
203
206
|
const errorText = await response.text();
|
|
207
|
+
|
|
208
|
+
// If contact already exists (409), fall back to update
|
|
209
|
+
if (response.status === 409) {
|
|
210
|
+
console.log(`Contact ${args.contact.email} already exists, updating instead`);
|
|
211
|
+
|
|
212
|
+
// Try to find the existing contact to get their ID
|
|
213
|
+
const findResponse = await fetch(
|
|
214
|
+
`${LOOPS_API_BASE_URL}/contacts/find?email=${encodeURIComponent(args.contact.email)}`,
|
|
215
|
+
{
|
|
216
|
+
method: "GET",
|
|
217
|
+
headers: {
|
|
218
|
+
Authorization: `Bearer ${args.apiKey}`,
|
|
219
|
+
"Content-Type": "application/json",
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (!findResponse.ok) {
|
|
225
|
+
const findErrorText = await findResponse.text();
|
|
226
|
+
console.error(`Failed to find existing contact [${findResponse.status}]:`, findErrorText);
|
|
227
|
+
// Continue with update anyway - it should work
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Update the contact instead
|
|
231
|
+
const updateResponse = await fetch(`${LOOPS_API_BASE_URL}/contacts/update`, {
|
|
232
|
+
method: "PUT",
|
|
233
|
+
headers: {
|
|
234
|
+
Authorization: `Bearer ${args.apiKey}`,
|
|
235
|
+
"Content-Type": "application/json",
|
|
236
|
+
},
|
|
237
|
+
body: JSON.stringify({
|
|
238
|
+
email: args.contact.email,
|
|
239
|
+
firstName: args.contact.firstName,
|
|
240
|
+
lastName: args.contact.lastName,
|
|
241
|
+
userId: args.contact.userId,
|
|
242
|
+
source: args.contact.source,
|
|
243
|
+
subscribed: args.contact.subscribed,
|
|
244
|
+
userGroup: args.contact.userGroup,
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (!updateResponse.ok) {
|
|
249
|
+
const updateErrorText = await updateResponse.text();
|
|
250
|
+
console.error(`Loops API error [${updateResponse.status}]:`, updateErrorText);
|
|
251
|
+
throw sanitizeError(updateResponse.status, updateErrorText);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Get contact ID if available
|
|
255
|
+
let contactId: string | undefined;
|
|
256
|
+
if (findResponse.ok) {
|
|
257
|
+
const findData = (await findResponse.json()) as { id?: string };
|
|
258
|
+
contactId = findData.id;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Store/update in our database
|
|
262
|
+
await ctx.runMutation(((internal as any).lib).storeContact as any, {
|
|
263
|
+
email: args.contact.email,
|
|
264
|
+
firstName: args.contact.firstName,
|
|
265
|
+
lastName: args.contact.lastName,
|
|
266
|
+
userId: args.contact.userId,
|
|
267
|
+
source: args.contact.source,
|
|
268
|
+
subscribed: args.contact.subscribed,
|
|
269
|
+
userGroup: args.contact.userGroup,
|
|
270
|
+
loopsContactId: contactId,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
success: true,
|
|
275
|
+
id: contactId,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// For other errors, throw as normal
|
|
204
280
|
console.error(`Loops API error [${response.status}]:`, errorText);
|
|
205
281
|
throw sanitizeError(response.status, errorText);
|
|
206
282
|
}
|
|
207
283
|
|
|
284
|
+
// Contact was created successfully
|
|
208
285
|
const data = (await response.json()) as { id?: string };
|
|
209
286
|
|
|
210
287
|
await ctx.runMutation(((internal as any).lib).storeContact as any, {
|