retainful-shopify-sdk 1.0.2

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,443 @@
1
+ # @yuko-app/retainful-shopify-sdk
2
+
3
+ A typed TypeScript SDK for sending Shopify app lifecycle events to [Retainful](https://retainful.com).
4
+ Built to be shared across all Yuko Apps — configure once per app, use everywhere.
5
+
6
+ ---
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @yuko-app/retainful-shopify-sdk
12
+ ```
13
+
14
+ Or, if using a monorepo with a local path:
15
+
16
+ ```json
17
+ // package.json of your Shopify app
18
+ {
19
+ "dependencies": {
20
+ "@yuko-app/retainful-shopify-sdk": "file:../../packages/retainful-sdk"
21
+ }
22
+ }
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Setup
28
+
29
+ ### 1. Get your credentials from Retainful
30
+
31
+ | Credential | Where to find it |
32
+ |---|---|
33
+ | `apiKey` | Retainful Dashboard → Settings → API Keys |
34
+
35
+ ### 2. Instantiate the client
36
+
37
+ Create **one client per Shopify app** and export it for reuse.
38
+
39
+ ```typescript
40
+ // lib/retainful.ts
41
+ import { RetainfulClient } from "@yuko-app/retainful-shopify-sdk";
42
+
43
+ export const retainful = new RetainfulClient(
44
+ {
45
+ apiKey: process.env.RETAINFUL_API_KEY!,
46
+ },
47
+ {
48
+ // Set these once — they're stamped on every event automatically
49
+ appName: "Order Tracking Pro",
50
+ appSlug: "order-tracking-pro",
51
+ }
52
+ );
53
+ ```
54
+
55
+ > **One client per app.** The `appName` and `appSlug` are baked into the client,
56
+ > so you never repeat them at the call site.
57
+
58
+ ---
59
+
60
+ ## Usage
61
+
62
+ ### Track an Install
63
+
64
+ Call this inside your Shopify auth callback or `app/installed` webhook handler.
65
+
66
+ ```typescript
67
+ import { retainful } from "~/lib/retainful";
68
+
69
+ // Inside your install handler
70
+ await retainful.events.installed({
71
+ email: shop.email,
72
+ shopDomain: shop.myshopify_domain, // e.g. "my-store.myshopify.com"
73
+ shopDetails: {
74
+ name: shop.name,
75
+ ownerName: shop.shop_owner,
76
+ plan: shop.plan_name,
77
+ country: shop.country_name,
78
+ currency: shop.currency,
79
+ timezone: shop.timezone,
80
+ phone: shop.phone,
81
+ },
82
+ });
83
+ ```
84
+
85
+ ### Track an Uninstall
86
+
87
+ Call this inside your `app/uninstalled` webhook handler.
88
+
89
+ ```typescript
90
+ import { retainful } from "~/lib/retainful";
91
+
92
+ // Inside your uninstall webhook handler
93
+ await retainful.events.uninstalled({
94
+ email: shop.email,
95
+ shopDomain: shop.myshopify_domain,
96
+ shopDetails: {
97
+ name: shop.name,
98
+ ownerName: shop.shop_owner,
99
+ plan: shop.plan_name,
100
+ country: shop.country_name,
101
+ currency: shop.currency,
102
+ timezone: shop.timezone,
103
+ },
104
+ });
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Framework Setup
110
+
111
+ The SDK is framework-agnostic — it only needs a place to instantiate the client once
112
+ and a place to call `events.installed()` / `events.uninstalled()` (usually a webhook
113
+ or auth callback). Below are idiomatic wirings for the common stacks.
114
+
115
+ > In every example, set `RETAINFUL_API_KEY` in your environment and **never throw**
116
+ > on a Retainful failure — a tracking error should not break your webhook response.
117
+
118
+ ### Remix
119
+
120
+ ```typescript
121
+ // app/lib/retainful.ts
122
+ import { RetainfulClient } from "@yuko-app/retainful-shopify-sdk";
123
+
124
+ export const retainful = new RetainfulClient(
125
+ { apiKey: process.env.RETAINFUL_API_KEY! },
126
+ { appName: "Order Tracking Pro", appSlug: "order-tracking-pro" }
127
+ );
128
+ ```
129
+
130
+ ```typescript
131
+ // app/routes/webhooks.tsx
132
+ import type { ActionFunctionArgs } from "@remix-run/node";
133
+ import { retainful } from "~/lib/retainful";
134
+ import { RetainfulApiError } from "@yuko-app/retainful-shopify-sdk";
135
+
136
+ export const action = async ({ request }: ActionFunctionArgs) => {
137
+ const topic = request.headers.get("x-shopify-topic");
138
+ const shop = await request.json();
139
+
140
+ try {
141
+ if (topic === "app/uninstalled") {
142
+ await retainful.events.uninstalled({
143
+ email: shop.email,
144
+ shopDomain: shop.myshopify_domain,
145
+ shopDetails: {
146
+ name: shop.name,
147
+ ownerName: shop.shop_owner,
148
+ plan: shop.plan_name,
149
+ country: shop.country_name,
150
+ currency: shop.currency,
151
+ },
152
+ });
153
+ }
154
+ } catch (error) {
155
+ if (error instanceof RetainfulApiError) {
156
+ console.error(`Retainful error ${error.statusCode}:`, error.statusText);
157
+ // Don't throw — don't let Retainful failures break your webhook response
158
+ }
159
+ }
160
+
161
+ return new Response("OK", { status: 200 });
162
+ };
163
+ ```
164
+
165
+ ### Next.js (App Router)
166
+
167
+ ```typescript
168
+ // lib/retainful.ts
169
+ import { RetainfulClient } from "@yuko-app/retainful-shopify-sdk";
170
+
171
+ export const retainful = new RetainfulClient(
172
+ { apiKey: process.env.RETAINFUL_API_KEY! },
173
+ { appName: "Order Tracking Pro", appSlug: "order-tracking-pro" }
174
+ );
175
+ ```
176
+
177
+ ```typescript
178
+ // app/api/webhooks/route.ts
179
+ import { NextResponse } from "next/server";
180
+ import { retainful } from "@/lib/retainful";
181
+ import { RetainfulApiError } from "@yuko-app/retainful-shopify-sdk";
182
+
183
+ // Webhooks must run on the Node.js runtime, not the Edge runtime.
184
+ export const runtime = "nodejs";
185
+
186
+ export async function POST(request: Request) {
187
+ const topic = request.headers.get("x-shopify-topic");
188
+ const shop = await request.json();
189
+
190
+ try {
191
+ if (topic === "app/uninstalled") {
192
+ await retainful.events.uninstalled({
193
+ email: shop.email,
194
+ shopDomain: shop.myshopify_domain,
195
+ shopDetails: {
196
+ name: shop.name,
197
+ ownerName: shop.shop_owner,
198
+ plan: shop.plan_name,
199
+ country: shop.country_name,
200
+ currency: shop.currency,
201
+ },
202
+ });
203
+ }
204
+ } catch (error) {
205
+ if (error instanceof RetainfulApiError) {
206
+ console.error(`Retainful error ${error.statusCode}:`, error.statusText);
207
+ }
208
+ }
209
+
210
+ return NextResponse.json({ ok: true });
211
+ }
212
+ ```
213
+
214
+ > **Pages Router?** The same `retainful` client works inside
215
+ > `pages/api/webhooks.ts` — read `req.headers["x-shopify-topic"]` and `req.body`,
216
+ > then `res.status(200).json({ ok: true })`.
217
+
218
+ ### NestJS
219
+
220
+ Wrap the client in an injectable provider so it can be shared across the app via DI.
221
+
222
+ ```typescript
223
+ // retainful/retainful.module.ts
224
+ import { Module } from "@nestjs/common";
225
+ import { RetainfulClient } from "@yuko-app/retainful-shopify-sdk";
226
+
227
+ export const RETAINFUL = "RETAINFUL_CLIENT";
228
+
229
+ @Module({
230
+ providers: [
231
+ {
232
+ provide: RETAINFUL,
233
+ useFactory: () =>
234
+ new RetainfulClient(
235
+ { apiKey: process.env.RETAINFUL_API_KEY! },
236
+ { appName: "Order Tracking Pro", appSlug: "order-tracking-pro" }
237
+ ),
238
+ },
239
+ ],
240
+ exports: [RETAINFUL],
241
+ })
242
+ export class RetainfulModule {}
243
+ ```
244
+
245
+ ```typescript
246
+ // webhooks/webhooks.controller.ts
247
+ import { Controller, Post, Body, Headers, Inject } from "@nestjs/common";
248
+ import { RetainfulClient, RetainfulApiError } from "@yuko-app/retainful-shopify-sdk";
249
+ import { RETAINFUL } from "../retainful/retainful.module";
250
+
251
+ @Controller("webhooks")
252
+ export class WebhooksController {
253
+ constructor(@Inject(RETAINFUL) private readonly retainful: RetainfulClient) {}
254
+
255
+ @Post()
256
+ async handle(
257
+ @Headers("x-shopify-topic") topic: string,
258
+ @Body() shop: any
259
+ ) {
260
+ try {
261
+ if (topic === "app/uninstalled") {
262
+ await this.retainful.events.uninstalled({
263
+ email: shop.email,
264
+ shopDomain: shop.myshopify_domain,
265
+ shopDetails: {
266
+ name: shop.name,
267
+ ownerName: shop.shop_owner,
268
+ plan: shop.plan_name,
269
+ country: shop.country_name,
270
+ currency: shop.currency,
271
+ },
272
+ });
273
+ }
274
+ } catch (error) {
275
+ if (error instanceof RetainfulApiError) {
276
+ console.error(`Retainful error ${error.statusCode}:`, error.statusText);
277
+ }
278
+ }
279
+
280
+ return { ok: true };
281
+ }
282
+ }
283
+ ```
284
+
285
+ ---
286
+
287
+ ## Retry Configuration
288
+
289
+ By default the SDK retries failed requests (status codes `429`, `503`, `504`) up to **3 times** with exponential backoff.
290
+
291
+ You can override this:
292
+
293
+ ```typescript
294
+ const retainful = new RetainfulClient(
295
+ { apiKey: "..." },
296
+ { appName: "My App", appSlug: "my-app" },
297
+ {
298
+ retryCodes: [429, 503],
299
+ numRetries: 5,
300
+ maxInterval: 30, // seconds
301
+ }
302
+ );
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Payload Reference
308
+
309
+ ### `installed(payload)` / `uninstalled(payload)`
310
+
311
+ | Field | Type | Required | Description |
312
+ |---|---|---|---|
313
+ | `email` | `string` | ✅ | Store owner email — contact identifier in Retainful |
314
+ | `shopDomain` | `string` | ✅ | Myshopify domain, e.g. `my-store.myshopify.com` |
315
+ | `shopDetails` | `ShopDetails` | ✅ | Full shop object from Shopify |
316
+
317
+ ### `ShopDetails`
318
+
319
+ | Field | Type | Description |
320
+ |---|---|---|
321
+ | `name` | `string` | Store display name |
322
+ | `ownerName` | `string` | Store owner's full name |
323
+ | `plan` | `string` | Shopify plan, e.g. `"basic"`, `"shopify"`, `"advanced"` |
324
+ | `country` | `string` | Store country |
325
+ | `currency` | `string` | Store currency, e.g. `"USD"` |
326
+ | `timezone` | `string` | Store timezone |
327
+ | `phone` | `string \| null` | Owner phone number |
328
+
329
+ All `ShopDetails` fields are optional. You can also pass any extra Shopify fields — the type allows `[key: string]: unknown`.
330
+
331
+ ---
332
+
333
+ ## What gets sent over the wire
334
+
335
+ Every event POST to `https://api.retainful.net/api/v1/events` includes:
336
+
337
+ **Headers:**
338
+ ```
339
+ Retainful-Api-Key: YOUR_API_KEY
340
+ Content-Type: application/json
341
+ ```
342
+
343
+ **Body:** the SDK maps the ergonomic `installed()` / `uninstalled()` payload onto
344
+ Retainful's `eventName` / `contact` / `eventData` / `uniqueIdentifier` format:
345
+
346
+ ```json
347
+ {
348
+ "eventName": "Order Tracking Pro installed",
349
+ "description": "App installed",
350
+ "contact": {
351
+ "email": "owner@my-store.com",
352
+ "first_name": "Jane",
353
+ "last_name": "Doe",
354
+ "phone": "+1234567890",
355
+ "email_opt_in": "SUBSCRIBED"
356
+ },
357
+ "eventData": {
358
+ "eventType": "installed",
359
+ "appName": "Order Tracking Pro",
360
+ "appSlug": "order-tracking-pro",
361
+ "storeDomain": "my-store.myshopify.com",
362
+ "countryCode": "United States",
363
+ "name": "My Store",
364
+ "plan": "basic",
365
+ "currency": "USD",
366
+ "timezone": "America/New_York"
367
+ },
368
+ "uniqueIdentifier": "my-store.myshopify.com-installed"
369
+ }
370
+ ```
371
+
372
+ **Mapping rules:**
373
+
374
+ | Wire field | Source |
375
+ |---|---|
376
+ | `eventName` | `"<appName> installed"` / `"<appName> uninstalled"` |
377
+ | `contact.email` | `payload.email` |
378
+ | `contact.first_name` / `last_name` | split from `shopDetails.ownerName` (override with `firstName` / `lastName`) |
379
+ | `contact.email_opt_in` | `payload.emailOptIn`, default `"SUBSCRIBED"` |
380
+ | `eventData` | `eventType`, `appName`, `appSlug`, `storeDomain`, `countryCode` + all other `shopDetails` + any `payload.eventData` |
381
+ | `uniqueIdentifier` | `payload.uniqueIdentifier`, default `"<shopDomain>-<installed\|uninstalled>"` |
382
+
383
+ Need full control? Use the escape hatch — `retainful.events.track(rawEvent)` posts an
384
+ `{ eventName, description, contact, eventData, uniqueIdentifier }` body verbatim with no mapping.
385
+
386
+ ---
387
+
388
+ ## Error Handling
389
+
390
+ ```typescript
391
+ import { RetainfulApiError, RetainfulConfigError } from "@yuko-app/retainful-shopify-sdk";
392
+
393
+ try {
394
+ await retainful.events.installed({ ... });
395
+ } catch (error) {
396
+ if (error instanceof RetainfulApiError) {
397
+ console.error(error.statusCode); // e.g. 401, 403, 422
398
+ console.error(error.statusText); // e.g. "Unauthorized"
399
+ console.error(error.responseData); // Full response body
400
+ }
401
+
402
+ if (error instanceof RetainfulConfigError) {
403
+ // Thrown at construction time if apiKey is missing
404
+ console.error(error.message);
405
+ }
406
+ }
407
+ ```
408
+
409
+ ---
410
+
411
+ ## Multiple Apps (Monorepo)
412
+
413
+ Each Shopify app creates its own client with its own `appName` / `appSlug`.
414
+ They all share the **same Retainful credentials** (scoped to Yuko Apps org via the API key).
415
+
416
+ ```typescript
417
+ // apps/order-tracking/lib/retainful.ts
418
+ export const retainful = new RetainfulClient(
419
+ { apiKey: process.env.RETAINFUL_API_KEY! },
420
+ { appName: "Order Tracking Pro", appSlug: "order-tracking-pro" }
421
+ );
422
+
423
+ // apps/reviews/lib/retainful.ts
424
+ export const retainful = new RetainfulClient(
425
+ { apiKey: process.env.RETAINFUL_API_KEY! },
426
+ { appName: "Reviews & Ratings", appSlug: "reviews-ratings" }
427
+ );
428
+ ```
429
+
430
+ ---
431
+
432
+ ## Building
433
+
434
+ ```bash
435
+ npm run build # compiles to /dist
436
+ npm run dev # watch mode
437
+ ```
438
+ # Retainful-Shopify-apps-sdk-beta
439
+ # Retainful-Shopify-apps-sdk
440
+ # Retainful-Shopify-apps-sdk
441
+ # Retainful-Shopify-apps-sdk
442
+ # Retainful-Shopify-apps-sdk
443
+ # Retainful-Shopify-apps-sdk
@@ -0,0 +1,45 @@
1
+ import { RetainfulSession } from "../session";
2
+ import { AppEventPayload, RetainfulEventRequest, RetainfulEventResponse, ShopifyAppConfig } from "../types/index";
3
+ /**
4
+ * EventsApi
5
+ *
6
+ * Sends Shopify app lifecycle events (install / uninstall) to Retainful.
7
+ * Automatically injects app_name and app_slug from the ShopifyAppConfig
8
+ * so you never have to repeat them at the call site.
9
+ *
10
+ * @example
11
+ * const events = new EventsApi(session, { appName: "Order Tracking Pro", appSlug: "order-tracking-pro" });
12
+ * await events.installed({ email, shopDomain, shopDetails });
13
+ * await events.uninstalled({ email, shopDomain, shopDetails });
14
+ */
15
+ export declare class EventsApi {
16
+ private readonly session;
17
+ private readonly appConfig;
18
+ constructor(session: RetainfulSession, appConfig: ShopifyAppConfig);
19
+ /**
20
+ * Track an app install event.
21
+ * Call this in your Shopify app's install webhook or auth callback.
22
+ */
23
+ installed(payload: Omit<AppEventPayload, "eventType">): Promise<RetainfulEventResponse>;
24
+ /**
25
+ * Track an app uninstall event.
26
+ * Call this in your Shopify app/uninstalled webhook handler.
27
+ */
28
+ uninstalled(payload: Omit<AppEventPayload, "eventType">): Promise<RetainfulEventResponse>;
29
+ /**
30
+ * Send any lifecycle event directly (lower-level).
31
+ * Use installed() / uninstalled() for most cases.
32
+ *
33
+ * Maps the ergonomic SDK payload onto Retainful's wire format
34
+ * (eventName / contact / eventData / uniqueIdentifier).
35
+ */
36
+ sendEvent(payload: AppEventPayload): Promise<RetainfulEventResponse>;
37
+ /**
38
+ * Send a fully-formed event to Retainful with no mapping.
39
+ * Escape hatch for events that don't fit the install/uninstall shape.
40
+ */
41
+ track(event: RetainfulEventRequest): Promise<RetainfulEventResponse>;
42
+ /** Translate an AppEventPayload into the Retainful wire format. */
43
+ private buildEvent;
44
+ }
45
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/api/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,sBAAsB,EACtB,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAIxB;;;;;;;;;;;GAWG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;gBAEjC,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,gBAAgB;IAKlE;;;OAGG;IACG,SAAS,CACb,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,CAAC,GAC1C,OAAO,CAAC,sBAAsB,CAAC;IAIlC;;;OAGG;IACG,WAAW,CACf,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,CAAC,GAC1C,OAAO,CAAC,sBAAsB,CAAC;IAIlC;;;;;;OAMG;IACG,SAAS,CACb,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,sBAAsB,CAAC;IAIlC;;;OAGG;IACG,KAAK,CACT,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,sBAAsB,CAAC;IAOlC,mEAAmE;IACnE,OAAO,CAAC,UAAU;CAsCnB"}
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventsApi = void 0;
4
+ const EVENTS_ENDPOINT = "/api/v1/events";
5
+ /**
6
+ * EventsApi
7
+ *
8
+ * Sends Shopify app lifecycle events (install / uninstall) to Retainful.
9
+ * Automatically injects app_name and app_slug from the ShopifyAppConfig
10
+ * so you never have to repeat them at the call site.
11
+ *
12
+ * @example
13
+ * const events = new EventsApi(session, { appName: "Order Tracking Pro", appSlug: "order-tracking-pro" });
14
+ * await events.installed({ email, shopDomain, shopDetails });
15
+ * await events.uninstalled({ email, shopDomain, shopDetails });
16
+ */
17
+ class EventsApi {
18
+ constructor(session, appConfig) {
19
+ this.session = session;
20
+ this.appConfig = appConfig;
21
+ }
22
+ /**
23
+ * Track an app install event.
24
+ * Call this in your Shopify app's install webhook or auth callback.
25
+ */
26
+ async installed(payload) {
27
+ return this.sendEvent({ ...payload, eventType: "app/installed" });
28
+ }
29
+ /**
30
+ * Track an app uninstall event.
31
+ * Call this in your Shopify app/uninstalled webhook handler.
32
+ */
33
+ async uninstalled(payload) {
34
+ return this.sendEvent({ ...payload, eventType: "app/uninstalled" });
35
+ }
36
+ /**
37
+ * Send any lifecycle event directly (lower-level).
38
+ * Use installed() / uninstalled() for most cases.
39
+ *
40
+ * Maps the ergonomic SDK payload onto Retainful's wire format
41
+ * (eventName / contact / eventData / uniqueIdentifier).
42
+ */
43
+ async sendEvent(payload) {
44
+ return this.track(this.buildEvent(payload));
45
+ }
46
+ /**
47
+ * Send a fully-formed event to Retainful with no mapping.
48
+ * Escape hatch for events that don't fit the install/uninstall shape.
49
+ */
50
+ async track(event) {
51
+ return this.session.post(EVENTS_ENDPOINT, event);
52
+ }
53
+ /** Translate an AppEventPayload into the Retainful wire format. */
54
+ buildEvent(payload) {
55
+ var _a, _b, _c, _d, _e;
56
+ // "app/installed" -> "installed", "app/uninstalled" -> "uninstalled"
57
+ const shortType = payload.eventType.replace(/^app\//, "");
58
+ const { ownerName, phone, country, ...restDetails } = payload.shopDetails;
59
+ const nameTokens = (ownerName !== null && ownerName !== void 0 ? ownerName : "").trim().split(/\s+/).filter(Boolean);
60
+ const firstName = (_a = payload.firstName) !== null && _a !== void 0 ? _a : nameTokens[0];
61
+ const lastName = (_b = payload.lastName) !== null && _b !== void 0 ? _b : (nameTokens.slice(1).join(" ") || undefined);
62
+ return {
63
+ eventName: `${this.appConfig.appName} ${shortType}`,
64
+ description: (_c = payload.description) !== null && _c !== void 0 ? _c : `App ${shortType}`,
65
+ contact: {
66
+ email: payload.email,
67
+ first_name: firstName,
68
+ last_name: lastName,
69
+ phone: phone !== null && phone !== void 0 ? phone : undefined,
70
+ email_opt_in: (_d = payload.emailOptIn) !== null && _d !== void 0 ? _d : "SUBSCRIBED",
71
+ },
72
+ eventData: {
73
+ eventType: shortType,
74
+ appName: this.appConfig.appName,
75
+ appSlug: this.appConfig.appSlug,
76
+ storeDomain: payload.shopDomain,
77
+ countryCode: country,
78
+ ...restDetails,
79
+ ...payload.eventData,
80
+ },
81
+ uniqueIdentifier: (_e = payload.uniqueIdentifier) !== null && _e !== void 0 ? _e : `${payload.shopDomain}-${shortType}`,
82
+ };
83
+ }
84
+ }
85
+ exports.EventsApi = EventsApi;
86
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/api/events.ts"],"names":[],"mappings":";;;AAQA,MAAM,eAAe,GAAG,gBAAgB,CAAC;AAEzC;;;;;;;;;;;GAWG;AACH,MAAa,SAAS;IAIpB,YAAY,OAAyB,EAAE,SAA2B;QAChE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS,CACb,OAA2C;QAE3C,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC,CAAC;IACpE,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,WAAW,CACf,OAA2C;QAE3C,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAC,CAAC;IACtE,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,SAAS,CACb,OAAwB;QAExB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK,CACT,KAA4B;QAE5B,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CACtB,eAAe,EACf,KAAK,CACN,CAAC;IACJ,CAAC;IAED,mEAAmE;IAC3D,UAAU,CAAC,OAAwB;;QACzC,qEAAqE;QACrE,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAE1D,MAAM,EACJ,SAAS,EACT,KAAK,EACL,OAAO,EACP,GAAG,WAAW,EACf,GAAG,OAAO,CAAC,WAAW,CAAC;QAExB,MAAM,UAAU,GAAG,CAAC,SAAS,aAAT,SAAS,cAAT,SAAS,GAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACzE,MAAM,SAAS,GAAG,MAAA,OAAO,CAAC,SAAS,mCAAI,UAAU,CAAC,CAAC,CAAC,CAAC;QACrD,MAAM,QAAQ,GAAG,MAAA,OAAO,CAAC,QAAQ,mCAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,CAAC;QAElF,OAAO;YACL,SAAS,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,IAAI,SAAS,EAAE;YACnD,WAAW,EAAE,MAAA,OAAO,CAAC,WAAW,mCAAI,OAAO,SAAS,EAAE;YACtD,OAAO,EAAE;gBACP,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,UAAU,EAAE,SAAS;gBACrB,SAAS,EAAE,QAAQ;gBACnB,KAAK,EAAE,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,SAAS;gBACzB,YAAY,EAAE,MAAA,OAAO,CAAC,UAAU,mCAAI,YAAY;aACjD;YACD,SAAS,EAAE;gBACT,SAAS,EAAE,SAAS;gBACpB,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO;gBAC/B,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO;gBAC/B,WAAW,EAAE,OAAO,CAAC,UAAU;gBAC/B,WAAW,EAAE,OAAO;gBACpB,GAAG,WAAW;gBACd,GAAG,OAAO,CAAC,SAAS;aACrB;YACD,gBAAgB,EACd,MAAA,OAAO,CAAC,gBAAgB,mCAAI,GAAG,OAAO,CAAC,UAAU,IAAI,SAAS,EAAE;SACnE,CAAC;IACJ,CAAC;CACF;AA9FD,8BA8FC"}
@@ -0,0 +1,30 @@
1
+ import { EventsApi } from "./api/events";
2
+ import { RetainfulSessionConfig, ShopifyAppConfig, RetryConfig } from "./types/index";
3
+ export { RetainfulApiError, RetainfulConfigError } from "./types/index";
4
+ export type { RetainfulSessionConfig, ShopifyAppConfig, RetryConfig, AppEventPayload, ShopDetails, RetainfulEventType, RetainfulEventResponse, RetainfulEventRequest, RetainfulContact, EmailOptInStatus, } from "./types/index";
5
+ /**
6
+ * RetainfulClient
7
+ *
8
+ * The main entry point for the Retainful SDK.
9
+ * Instantiate once per Shopify app and reuse across your request handlers.
10
+ *
11
+ * @example
12
+ * import { RetainfulClient } from "@akashm-cartrabbit/retainful-shopify-sdk";
13
+ *
14
+ * const retainful = new RetainfulClient(
15
+ * { apiKey: process.env.RETAINFUL_API_KEY },
16
+ * { appName: "Order Tracking Pro", appSlug: "order-tracking-pro" }
17
+ * );
18
+ *
19
+ * // On install:
20
+ * await retainful.events.installed({ email, shopDomain, shopDetails });
21
+ *
22
+ * // On uninstall:
23
+ * await retainful.events.uninstalled({ email, shopDomain, shopDetails });
24
+ */
25
+ export declare class RetainfulClient {
26
+ readonly events: EventsApi;
27
+ private readonly session;
28
+ constructor(sessionConfig: RetainfulSessionConfig, appConfig: ShopifyAppConfig, retryConfig?: RetryConfig);
29
+ }
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,WAAW,EACZ,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACxE,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,WAAW,EACX,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AAEvB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,eAAe;IAC1B,SAAgB,MAAM,EAAE,SAAS,CAAC;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;gBAGzC,aAAa,EAAE,sBAAsB,EACrC,SAAS,EAAE,gBAAgB,EAC3B,WAAW,CAAC,EAAE,WAAW;CAK5B"}
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RetainfulClient = exports.RetainfulConfigError = exports.RetainfulApiError = void 0;
4
+ const session_1 = require("./session");
5
+ const events_1 = require("./api/events");
6
+ var index_1 = require("./types/index");
7
+ Object.defineProperty(exports, "RetainfulApiError", { enumerable: true, get: function () { return index_1.RetainfulApiError; } });
8
+ Object.defineProperty(exports, "RetainfulConfigError", { enumerable: true, get: function () { return index_1.RetainfulConfigError; } });
9
+ /**
10
+ * RetainfulClient
11
+ *
12
+ * The main entry point for the Retainful SDK.
13
+ * Instantiate once per Shopify app and reuse across your request handlers.
14
+ *
15
+ * @example
16
+ * import { RetainfulClient } from "@akashm-cartrabbit/retainful-shopify-sdk";
17
+ *
18
+ * const retainful = new RetainfulClient(
19
+ * { apiKey: process.env.RETAINFUL_API_KEY },
20
+ * { appName: "Order Tracking Pro", appSlug: "order-tracking-pro" }
21
+ * );
22
+ *
23
+ * // On install:
24
+ * await retainful.events.installed({ email, shopDomain, shopDetails });
25
+ *
26
+ * // On uninstall:
27
+ * await retainful.events.uninstalled({ email, shopDomain, shopDetails });
28
+ */
29
+ class RetainfulClient {
30
+ constructor(sessionConfig, appConfig, retryConfig) {
31
+ this.session = new session_1.RetainfulSession(sessionConfig, retryConfig);
32
+ this.events = new events_1.EventsApi(this.session, appConfig);
33
+ }
34
+ }
35
+ exports.RetainfulClient = RetainfulClient;
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,uCAA6C;AAC7C,yCAAyC;AAOzC,uCAAwE;AAA/D,0GAAA,iBAAiB,OAAA;AAAE,6GAAA,oBAAoB,OAAA;AAchD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAa,eAAe;IAI1B,YACE,aAAqC,EACrC,SAA2B,EAC3B,WAAyB;QAEzB,IAAI,CAAC,OAAO,GAAG,IAAI,0BAAgB,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;QAChE,IAAI,CAAC,MAAM,GAAG,IAAI,kBAAS,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACvD,CAAC;CACF;AAZD,0CAYC"}
@@ -0,0 +1,8 @@
1
+ import { RetainfulSessionConfig, RetryConfig } from "./types/index";
2
+ export declare class RetainfulSession {
3
+ private readonly client;
4
+ private readonly retryConfig?;
5
+ constructor(config: RetainfulSessionConfig, retryConfig?: RetryConfig);
6
+ post<TBody, TResponse>(path: string, body: TBody): Promise<TResponse>;
7
+ }
8
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AACA,OAAO,EACL,sBAAsB,EAGtB,WAAW,EACZ,MAAM,eAAe,CAAC;AAKvB,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAc;gBAE/B,MAAM,EAAE,sBAAsB,EAAE,WAAW,CAAC,EAAE,WAAW;IAkB/D,IAAI,CAAC,KAAK,EAAE,SAAS,EACzB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,KAAK,GACV,OAAO,CAAC,SAAS,CAAC;CAqBtB"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RetainfulSession = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const index_1 = require("./types/index");
9
+ const retry_1 = require("./utils/retry");
10
+ const BASE_URL = "https://api.retainful.net";
11
+ class RetainfulSession {
12
+ constructor(config, retryConfig) {
13
+ if (!config.apiKey) {
14
+ throw new index_1.RetainfulConfigError("Retainful API key is required. Get it from Settings → API Keys.");
15
+ }
16
+ this.retryConfig = retryConfig;
17
+ this.client = axios_1.default.create({
18
+ baseURL: BASE_URL,
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ "Retainful-Api-Key": config.apiKey,
22
+ },
23
+ timeout: 30000,
24
+ });
25
+ }
26
+ async post(path, body) {
27
+ return (0, retry_1.withRetry)(async () => {
28
+ try {
29
+ const response = await this.client.post(path, body);
30
+ return response.data;
31
+ }
32
+ catch (error) {
33
+ const axiosError = error;
34
+ if (axiosError.response) {
35
+ throw new index_1.RetainfulApiError(axiosError.response.status, axiosError.response.statusText, axiosError.response.data);
36
+ }
37
+ throw error;
38
+ }
39
+ }, this.retryConfig);
40
+ }
41
+ }
42
+ exports.RetainfulSession = RetainfulSession;
43
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":";;;;;;AAAA,kDAAwE;AACxE,yCAKuB;AACvB,yCAA0C;AAE1C,MAAM,QAAQ,GAAG,2BAA2B,CAAC;AAE7C,MAAa,gBAAgB;IAI3B,YAAY,MAA8B,EAAE,WAAyB;QACnE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACnB,MAAM,IAAI,4BAAoB,CAC5B,iEAAiE,CAClE,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAE/B,IAAI,CAAC,MAAM,GAAG,eAAK,CAAC,MAAM,CAAC;YACzB,OAAO,EAAE,QAAQ;YACjB,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,mBAAmB,EAAE,MAAM,CAAC,MAAM;aACnC;YACD,OAAO,EAAE,KAAM;SAChB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,IAAI,CACR,IAAY,EACZ,IAAW;QAEX,OAAO,IAAA,iBAAS,EAAC,KAAK,IAAI,EAAE;YAC1B,IAAI,CAAC;gBACH,MAAM,QAAQ,GAA6B,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAC/D,IAAI,EACJ,IAAI,CACL,CAAC;gBACF,OAAO,QAAQ,CAAC,IAAI,CAAC;YACvB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,UAAU,GAAG,KAAmB,CAAC;gBACvC,IAAI,UAAU,CAAC,QAAQ,EAAE,CAAC;oBACxB,MAAM,IAAI,yBAAiB,CACzB,UAAU,CAAC,QAAQ,CAAC,MAAM,EAC1B,UAAU,CAAC,QAAQ,CAAC,UAAU,EAC9B,UAAU,CAAC,QAAQ,CAAC,IAAI,CACzB,CAAC;gBACJ,CAAC;gBACD,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IACvB,CAAC;CACF;AA9CD,4CA8CC"}
@@ -0,0 +1,98 @@
1
+ export interface RetainfulSessionConfig {
2
+ /** Your Retainful API Key (from Settings → API Keys) */
3
+ apiKey: string;
4
+ }
5
+ export interface ShopifyAppConfig {
6
+ /** Human-readable name of your Shopify app, e.g. "Order Tracking Pro" */
7
+ appName: string;
8
+ /** URL-safe slug for your app, e.g. "order-tracking-pro" */
9
+ appSlug: string;
10
+ }
11
+ export interface RetryConfig {
12
+ /** HTTP status codes to retry on. Default: [429, 503, 504] */
13
+ retryCodes?: number[];
14
+ /** Number of retry attempts. Default: 3 */
15
+ numRetries?: number;
16
+ /** Maximum backoff interval in seconds. Default: 60 */
17
+ maxInterval?: number;
18
+ }
19
+ export interface ShopDetails {
20
+ /** Shopify store name */
21
+ name?: string;
22
+ /** Store owner's name */
23
+ ownerName?: string;
24
+ /** Store owner's phone */
25
+ phone?: string | null;
26
+ /** Shopify plan name, e.g. "basic", "shopify", "advanced" */
27
+ plan?: string;
28
+ /** Store's primary country */
29
+ country?: string;
30
+ /** Store currency, e.g. "USD" */
31
+ currency?: string;
32
+ /** Store's timezone */
33
+ timezone?: string;
34
+ /** Any extra Shopify store fields you want to forward */
35
+ [key: string]: unknown;
36
+ }
37
+ export type RetainfulEventType = "app/installed" | "app/uninstalled";
38
+ /** Email subscription status as understood by Retainful. */
39
+ export type EmailOptInStatus = "SUBSCRIBED" | "UNSUBSCRIBED" | (string & {});
40
+ export interface AppEventPayload {
41
+ /** Type of lifecycle event */
42
+ eventType: RetainfulEventType;
43
+ /** Store owner / admin email — used as the contact identifier in Retainful */
44
+ email: string;
45
+ /** Shopify shop domain, e.g. "my-store.myshopify.com" — unique identifier */
46
+ shopDomain: string;
47
+ /** Full shop details object from Shopify */
48
+ shopDetails: ShopDetails;
49
+ /** Contact first name. Defaults to the first token of shopDetails.ownerName. */
50
+ firstName?: string;
51
+ /** Contact last name. Defaults to the remaining tokens of shopDetails.ownerName. */
52
+ lastName?: string;
53
+ /** Email subscription status. Default: "SUBSCRIBED" */
54
+ emailOptIn?: EmailOptInStatus;
55
+ /** Human-readable description. Default: "App installed" / "App uninstalled". */
56
+ description?: string;
57
+ /**
58
+ * Idempotency key for this event.
59
+ * Default: `${shopDomain}-${installed|uninstalled}`.
60
+ */
61
+ uniqueIdentifier?: string;
62
+ /** Extra fields merged into the eventData object sent to Retainful. */
63
+ eventData?: Record<string, unknown>;
64
+ }
65
+ export interface RetainfulContact {
66
+ email: string;
67
+ first_name?: string;
68
+ last_name?: string;
69
+ phone?: string | null;
70
+ email_opt_in?: EmailOptInStatus;
71
+ }
72
+ export interface RetainfulEventRequest {
73
+ /** Display name of the event, e.g. "Order Tracking Pro installed". */
74
+ eventName: string;
75
+ /** Human-readable description of what happened. */
76
+ description?: string;
77
+ /** The contact this event is associated with. */
78
+ contact: RetainfulContact;
79
+ /** Arbitrary structured data attached to the event. */
80
+ eventData: Record<string, unknown>;
81
+ /** Idempotency key — dedupes repeated deliveries of the same event. */
82
+ uniqueIdentifier: string;
83
+ }
84
+ export interface RetainfulEventResponse {
85
+ success: boolean;
86
+ message?: string;
87
+ [key: string]: unknown;
88
+ }
89
+ export declare class RetainfulApiError extends Error {
90
+ readonly statusCode: number;
91
+ readonly statusText: string;
92
+ readonly responseData?: unknown;
93
+ constructor(statusCode: number, statusText: string, responseData?: unknown);
94
+ }
95
+ export declare class RetainfulConfigError extends Error {
96
+ constructor(message: string);
97
+ }
98
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ // ─────────────────────────────────────────────
3
+ // Auth & Session Types
4
+ // ─────────────────────────────────────────────
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RetainfulConfigError = exports.RetainfulApiError = void 0;
7
+ // ─────────────────────────────────────────────
8
+ // Errors
9
+ // ─────────────────────────────────────────────
10
+ class RetainfulApiError extends Error {
11
+ constructor(statusCode, statusText, responseData) {
12
+ super(`Retainful API Error ${statusCode}: ${statusText}`);
13
+ this.name = "RetainfulApiError";
14
+ this.statusCode = statusCode;
15
+ this.statusText = statusText;
16
+ this.responseData = responseData;
17
+ }
18
+ }
19
+ exports.RetainfulApiError = RetainfulApiError;
20
+ class RetainfulConfigError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = "RetainfulConfigError";
24
+ }
25
+ }
26
+ exports.RetainfulConfigError = RetainfulConfigError;
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":";AAAA,gDAAgD;AAChD,uBAAuB;AACvB,gDAAgD;;;AAgIhD,gDAAgD;AAChD,SAAS;AACT,gDAAgD;AAEhD,MAAa,iBAAkB,SAAQ,KAAK;IAK1C,YAAY,UAAkB,EAAE,UAAkB,EAAE,YAAsB;QACxE,KAAK,CAAC,uBAAuB,UAAU,KAAK,UAAU,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;CACF;AAZD,8CAYC;AAED,MAAa,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AALD,oDAKC"}
@@ -0,0 +1,7 @@
1
+ import { RetryConfig } from "../types";
2
+ /**
3
+ * Wraps an async function with retry logic using exponential backoff.
4
+ * Retries only on configured HTTP status codes.
5
+ */
6
+ export declare function withRetry<T>(fn: () => Promise<T>, config?: RetryConfig): Promise<T>;
7
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAyBvC;;;GAGG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,CAAC,CAAC,CA+BZ"}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withRetry = withRetry;
4
+ const DEFAULT_RETRY_CONFIG = {
5
+ retryCodes: [429, 503, 504],
6
+ numRetries: 3,
7
+ maxInterval: 60,
8
+ };
9
+ /**
10
+ * Sleep for a given number of milliseconds.
11
+ */
12
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+ /**
14
+ * Calculate exponential backoff delay with jitter.
15
+ * Formula: min(maxInterval, (2^attempt) * 1000) + random jitter
16
+ */
17
+ const getBackoffDelay = (attempt, maxInterval) => {
18
+ const exponential = Math.pow(2, attempt) * 1000;
19
+ const capped = Math.min(maxInterval * 1000, exponential);
20
+ const jitter = Math.random() * 1000;
21
+ return capped + jitter;
22
+ };
23
+ /**
24
+ * Wraps an async function with retry logic using exponential backoff.
25
+ * Retries only on configured HTTP status codes.
26
+ */
27
+ async function withRetry(fn, config) {
28
+ const { retryCodes, numRetries, maxInterval } = {
29
+ ...DEFAULT_RETRY_CONFIG,
30
+ ...config,
31
+ };
32
+ let lastError;
33
+ for (let attempt = 0; attempt <= numRetries; attempt++) {
34
+ try {
35
+ return await fn();
36
+ }
37
+ catch (error) {
38
+ lastError = error;
39
+ // Only retry on specific HTTP status codes
40
+ const statusCode = extractStatusCode(error);
41
+ const shouldRetry = attempt < numRetries &&
42
+ statusCode !== null &&
43
+ retryCodes.includes(statusCode);
44
+ if (!shouldRetry) {
45
+ throw error;
46
+ }
47
+ const delay = getBackoffDelay(attempt, maxInterval);
48
+ await sleep(delay);
49
+ }
50
+ }
51
+ throw lastError;
52
+ }
53
+ function extractStatusCode(error) {
54
+ if (error &&
55
+ typeof error === "object" &&
56
+ "response" in error &&
57
+ error.response &&
58
+ typeof error.response === "object" &&
59
+ "status" in error.response &&
60
+ typeof error.response.status === "number") {
61
+ return error.response.status;
62
+ }
63
+ return null;
64
+ }
65
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":";;AA6BA,8BAkCC;AA7DD,MAAM,oBAAoB,GAA0B;IAClD,UAAU,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;IAC3B,UAAU,EAAE,CAAC;IACb,WAAW,EAAE,EAAE;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAC1C,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAEpD;;;GAGG;AACH,MAAM,eAAe,GAAG,CAAC,OAAe,EAAE,WAAmB,EAAU,EAAE;IACvE,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,IAAI,EAAE,WAAW,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC;IACpC,OAAO,MAAM,GAAG,MAAM,CAAC;AACzB,CAAC,CAAC;AAEF;;;GAGG;AACI,KAAK,UAAU,SAAS,CAC7B,EAAoB,EACpB,MAAoB;IAEpB,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG;QAC9C,GAAG,oBAAoB;QACvB,GAAG,MAAM;KACV,CAAC;IAEF,IAAI,SAAkB,CAAC;IAEvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,SAAS,GAAG,KAAK,CAAC;YAElB,2CAA2C;YAC3C,MAAM,UAAU,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAC5C,MAAM,WAAW,GACf,OAAO,GAAG,UAAU;gBACpB,UAAU,KAAK,IAAI;gBACnB,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YAElC,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,MAAM,KAAK,CAAC;YACd,CAAC;YAED,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;YACpD,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,MAAM,SAAS,CAAC;AAClB,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAc;IACvC,IACE,KAAK;QACL,OAAO,KAAK,KAAK,QAAQ;QACzB,UAAU,IAAI,KAAK;QACnB,KAAK,CAAC,QAAQ;QACd,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ;QAClC,QAAQ,IAAI,KAAK,CAAC,QAAQ;QAC1B,OAAO,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,QAAQ,EACzC,CAAC;QACD,OAAO,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC/B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "retainful-shopify-sdk",
3
+ "version": "1.0.2",
4
+ "description": "Retainful SDK for Shopify apps — Shopify lifecycle event tracking",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "test": "jest",
14
+ "trigger": "npm run build && node scripts/trigger-event.js"
15
+ },
16
+ "keywords": [
17
+ "retainful",
18
+ "shopify",
19
+ "yuko-apps",
20
+ "marketing-automation"
21
+ ],
22
+ "author": "Shopify Apps",
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "axios": "^1.6.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^20.0.0",
29
+ "typescript": "^5.0.0"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/yuko-app/Retainful-Shopify-apps-sdk.git"
34
+ },
35
+ "publishConfig": {
36
+ "registry": "https://registry.npmjs.org",
37
+ "access": "public"
38
+ }
39
+ }