@progus/connector 0.5.4 → 0.6.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 CHANGED
@@ -37,6 +37,39 @@ await connector.trackSubscriptionUpdated({
37
37
  await connector.trackUninstall({ shopDomain: session.shop });
38
38
  ```
39
39
 
40
+ ## Prompt: Partner program implementation (copy/paste)
41
+
42
+ ```
43
+ You are integrating Progus Partner Program using @progus/connector in a Shopify app.
44
+
45
+ Implement these pieces:
46
+ 1) Server connector:
47
+ - createProgusConnector({ appKey, PARTNERS_API_URL, PARTNERS_SECRET_KEY, PARTNERS_API_KEY })
48
+ - expose getPartnersConnector() + isPartnersConnectorConfigured()
49
+
50
+ 2) Install tracking:
51
+ - after app install, call trackInstall({ shopDomain, partnerId })
52
+ - partnerId source: read cookie "progus_partner_id" on progus.com
53
+ - cookie must be JS-readable (HttpOnly=false) and cross-site (SameSite=None; Secure)
54
+ - if partnerId is missing, skip tracking
55
+
56
+ 3) Partner ID assignment UI:
57
+ - save partnerId from merchant input (Partner ID field)
58
+ - call assignPartnerId({ shopDomain, partnerId }) on save
59
+ - lock the field after successful save
60
+ - optionally check partnerId on load with checkPartnerId({ shopDomain })
61
+
62
+ 4) Subscription tracking:
63
+ - on app_subscriptions/update webhook, send trackSubscriptionUpdated
64
+ - payload must include: subscriptionId (digits), subscriptionPrice (number), subscriptionPeriod ("EVERY_30_DAYS" or "ANNUAL"), subscriptionStatus, subscriptionName
65
+ - if webhook payload is missing price/interval, fetch details via Admin GraphQL node(id) -> AppSubscription -> lineItems.plan.pricingDetails
66
+
67
+ 5) Uninstall tracking:
68
+ - on app/uninstalled webhook, call trackUninstall({ shopDomain })
69
+
70
+ Return errors gracefully; do not break webhooks if partner tracking fails.
71
+ ```
72
+
40
73
  ## Partner ID assignment
41
74
 
42
75
  ```ts
@@ -58,9 +91,25 @@ import { signPayload } from "@progus/connector";
58
91
  const signature = signPayload(JSON.stringify({ test: true }), process.env.PARTNERS_SECRET_KEY!);
59
92
  ```
60
93
 
61
- ## UI components and cross-sell
94
+ ## Cross-sell recommendations (server-side)
95
+
96
+ Use the connector to fetch and filter the Progus apps catalog on the backend,
97
+ then pass the list to your frontend UI.
98
+
99
+ ```ts
100
+ import { getCrossSellOffersFromApi } from "@progus/connector";
101
+
102
+ const recommendations = await getCrossSellOffersFromApi({
103
+ appName: "progus-store-locator",
104
+ limit: 3,
105
+ fetch, // pass your server-side fetch
106
+ });
107
+ ```
108
+
109
+ ## UI components
62
110
 
63
- Use `@progus/connector-ui` for browser-safe helpers and UI components.
111
+ Use `@progus/connector-ui` for UI components. It re-exports cross-sell helpers
112
+ from `@progus/connector`.
64
113
 
65
114
  ## Environment variables
66
115
 
package/dist/index.cjs CHANGED
@@ -21,6 +21,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  createProgusConnector: () => createProgusConnector,
24
+ fetchAppsCatalog: () => fetchAppsCatalog,
25
+ getCrossSellOffers: () => getCrossSellOffers,
26
+ getCrossSellOffersFromApi: () => getCrossSellOffersFromApi,
24
27
  normalizePartnerId: () => normalizePartnerId,
25
28
  signPayload: () => signPayload
26
29
  });
@@ -61,7 +64,19 @@ function createProgusConnector(config) {
61
64
  const enableIdempotency = config.enableIdempotency !== false;
62
65
  const validSubscriptionPeriods = /* @__PURE__ */ new Set(["ANNUAL", "EVERY_30_DAYS"]);
63
66
  if (!fetchImpl) {
64
- throw new Error("Fetch implementation is required");
67
+ const message = "Fetch implementation is required";
68
+ logger?.error?.(message);
69
+ const fail = async () => ({ success: false, message });
70
+ return {
71
+ track: async () => ({ success: false, message }),
72
+ trackInstall: fail,
73
+ trackUninstall: fail,
74
+ trackSubscriptionPurchased: fail,
75
+ trackSubscriptionUpdated: fail,
76
+ trackSubscriptionCancelled: fail,
77
+ assignPartnerId: fail,
78
+ checkPartnerId: async () => ({ success: false, message, status: 500 })
79
+ };
65
80
  }
66
81
  async function postEvent(eventName, payload) {
67
82
  const { shopDomain, partnerId, data, externalId } = payload;
@@ -234,9 +249,111 @@ function createProgusConnector(config) {
234
249
  checkPartnerId
235
250
  };
236
251
  }
252
+
253
+ // src/crossSell.ts
254
+ function getCrossSellOffers(options = {}) {
255
+ const appsCatalog = options.appsCatalog ?? [];
256
+ const installedKeys = new Set(
257
+ [options.appName, ...options.installedAppKeys ?? []].filter(
258
+ (key) => Boolean(key)
259
+ )
260
+ );
261
+ const locale = options.locale;
262
+ return appsCatalog.filter((app) => app.enabled !== false).filter((app) => locale ? !app.locales || app.locales.includes(locale) : true).filter((app) => !installedKeys.has(app.key)).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
263
+ }
264
+ var DEFAULT_APPS_CATALOG_URL = "https://appsdata.progus.com/recommendations";
265
+ var FALLBACK_APPS_CATALOG = [
266
+ {
267
+ key: "progus_ai_studio",
268
+ type: "app",
269
+ title: "Progus AI Studio",
270
+ company: "Progus",
271
+ companyUrl: "https://progus.com",
272
+ desc: "Generate on-brand AI product & variant images in bulk.",
273
+ url: "https://apps.shopify.com/progus-ai-studio",
274
+ icon: "https://cdn.shopify.com/app-store/listing_images/9428a41ed53c66cd7329a2583cd2f3d9/icon/COje1rnS4pEDEAE=.png",
275
+ priority: 100,
276
+ enabled: true
277
+ },
278
+ {
279
+ key: "progus_trust_badges",
280
+ type: "app",
281
+ title: "Progus Trust Badges",
282
+ company: "Progus",
283
+ companyUrl: "https://progus.com",
284
+ desc: "Add Trust Badges to your store to build trust and credibility.",
285
+ url: "https://apps.shopify.com/progus-trust-badges-1",
286
+ icon: "https://cdn.shopify.com/app-store/listing_images/f9d0009e237f27d2db35b41ef99be858/icon/CJ3y1qDn1JEDEAE=.png",
287
+ priority: 80,
288
+ enabled: true
289
+ },
290
+ {
291
+ key: "progus_cod",
292
+ type: "app",
293
+ title: "Progus COD",
294
+ company: "Progus",
295
+ companyUrl: "https://progus.com",
296
+ desc: "Automate COD Fees & Hide/Show Cash on Delivery by Rules.",
297
+ url: "https://apps.shopify.com/progus-cod",
298
+ icon: "https://cdn.shopify.com/app-store/listing_images/bc537219cc3ed2bd4e7e3e683fe6b74a/icon/CMi_6dTEkIoDEAE=.png",
299
+ priority: 90,
300
+ enabled: true
301
+ }
302
+ ];
303
+ function buildFallbackCatalog(options) {
304
+ const appName = options.appName;
305
+ const limit = typeof options.limit === "number" ? options.limit : 3;
306
+ let items = FALLBACK_APPS_CATALOG.filter((app) => app.enabled !== false);
307
+ if (appName) {
308
+ items = items.filter((app) => app.key !== appName);
309
+ }
310
+ if (limit > 0) {
311
+ items = items.slice(0, limit);
312
+ }
313
+ return items;
314
+ }
315
+ async function fetchAppsCatalog(options = {}) {
316
+ const fetchImpl = options.fetch ?? globalThis.fetch;
317
+ const logger = options.logger ?? console;
318
+ const appName = options.appName;
319
+ const baseUrl = stripTrailingSlash(options.appsCatalogUrl ?? DEFAULT_APPS_CATALOG_URL);
320
+ const limit = typeof options.limit === "number" ? options.limit : 3;
321
+ const params = new URLSearchParams();
322
+ if (appName) params.set("appName", appName);
323
+ if (limit > 0) params.set("limit", String(limit));
324
+ const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
325
+ if (!fetchImpl) {
326
+ logger?.error?.("Failed to fetch apps catalog", {
327
+ error: "Fetch implementation is required"
328
+ });
329
+ return buildFallbackCatalog(options);
330
+ }
331
+ try {
332
+ const response = await fetchImpl(url);
333
+ const text = await response.text();
334
+ const parsed = safeJsonParse(text);
335
+ if (!response.ok || !parsed) {
336
+ logger?.error?.("Failed to fetch apps catalog", { status: response.status });
337
+ return buildFallbackCatalog(options);
338
+ }
339
+ return parsed;
340
+ } catch (error) {
341
+ logger?.error?.("Failed to fetch apps catalog", {
342
+ error: error instanceof Error ? error.message : String(error)
343
+ });
344
+ return buildFallbackCatalog(options);
345
+ }
346
+ }
347
+ async function getCrossSellOffersFromApi(options = {}) {
348
+ const appsCatalog = options.appsCatalog ?? await fetchAppsCatalog(options);
349
+ return getCrossSellOffers({ ...options, appsCatalog });
350
+ }
237
351
  // Annotate the CommonJS export names for ESM import in node:
238
352
  0 && (module.exports = {
239
353
  createProgusConnector,
354
+ fetchAppsCatalog,
355
+ getCrossSellOffers,
356
+ getCrossSellOffersFromApi,
240
357
  normalizePartnerId,
241
358
  signPayload
242
359
  });
package/dist/index.d.cts CHANGED
@@ -44,6 +44,32 @@ type SubscriptionEventData = {
44
44
  subscriptionPrice?: number | string;
45
45
  subscriptionPeriod?: string;
46
46
  };
47
+ type AppsCatalogEntry = {
48
+ key: string;
49
+ type: string;
50
+ title: string;
51
+ company?: string;
52
+ companyUrl?: string;
53
+ desc?: string;
54
+ url: string;
55
+ icon?: string;
56
+ enabled?: boolean;
57
+ priority?: number;
58
+ locales?: string[];
59
+ };
60
+ type CrossSellOptions = {
61
+ appName?: string;
62
+ installedAppKeys?: string[];
63
+ locale?: string;
64
+ shopPlan?: string;
65
+ appsCatalog?: AppsCatalogEntry[];
66
+ };
67
+ type CrossSellFetchOptions = CrossSellOptions & {
68
+ appsCatalogUrl?: string;
69
+ limit?: number;
70
+ fetch?: FetchLike;
71
+ logger?: Logger;
72
+ };
47
73
 
48
74
  type Connector = {
49
75
  track: (eventName: TrackEventParams["eventName"], payload: Omit<TrackEventParams, "eventName">) => Promise<TrackResult>;
@@ -75,8 +101,12 @@ type Connector = {
75
101
  };
76
102
  declare function createProgusConnector(config: ConnectorConfig): Connector;
77
103
 
104
+ declare function getCrossSellOffers(options?: CrossSellOptions): AppsCatalogEntry[];
105
+ declare function fetchAppsCatalog(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
106
+ declare function getCrossSellOffersFromApi(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
107
+
78
108
  declare function signPayload(body: string, secret: string): string;
79
109
 
80
110
  declare function normalizePartnerId(value?: string | null): string | null;
81
111
 
82
- export { type AssignPartnerIdInput, type CheckPartnerIdResult, type ConnectorConfig, type Logger, type SubscriptionEventData, type TrackEventName, type TrackEventParams, type TrackResult, createProgusConnector, normalizePartnerId, signPayload };
112
+ export { type AppsCatalogEntry, type AssignPartnerIdInput, type CheckPartnerIdResult, type ConnectorConfig, type CrossSellFetchOptions, type CrossSellOptions, type Logger, type SubscriptionEventData, type TrackEventName, type TrackEventParams, type TrackResult, createProgusConnector, fetchAppsCatalog, getCrossSellOffers, getCrossSellOffersFromApi, normalizePartnerId, signPayload };
package/dist/index.d.ts CHANGED
@@ -44,6 +44,32 @@ type SubscriptionEventData = {
44
44
  subscriptionPrice?: number | string;
45
45
  subscriptionPeriod?: string;
46
46
  };
47
+ type AppsCatalogEntry = {
48
+ key: string;
49
+ type: string;
50
+ title: string;
51
+ company?: string;
52
+ companyUrl?: string;
53
+ desc?: string;
54
+ url: string;
55
+ icon?: string;
56
+ enabled?: boolean;
57
+ priority?: number;
58
+ locales?: string[];
59
+ };
60
+ type CrossSellOptions = {
61
+ appName?: string;
62
+ installedAppKeys?: string[];
63
+ locale?: string;
64
+ shopPlan?: string;
65
+ appsCatalog?: AppsCatalogEntry[];
66
+ };
67
+ type CrossSellFetchOptions = CrossSellOptions & {
68
+ appsCatalogUrl?: string;
69
+ limit?: number;
70
+ fetch?: FetchLike;
71
+ logger?: Logger;
72
+ };
47
73
 
48
74
  type Connector = {
49
75
  track: (eventName: TrackEventParams["eventName"], payload: Omit<TrackEventParams, "eventName">) => Promise<TrackResult>;
@@ -75,8 +101,12 @@ type Connector = {
75
101
  };
76
102
  declare function createProgusConnector(config: ConnectorConfig): Connector;
77
103
 
104
+ declare function getCrossSellOffers(options?: CrossSellOptions): AppsCatalogEntry[];
105
+ declare function fetchAppsCatalog(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
106
+ declare function getCrossSellOffersFromApi(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
107
+
78
108
  declare function signPayload(body: string, secret: string): string;
79
109
 
80
110
  declare function normalizePartnerId(value?: string | null): string | null;
81
111
 
82
- export { type AssignPartnerIdInput, type CheckPartnerIdResult, type ConnectorConfig, type Logger, type SubscriptionEventData, type TrackEventName, type TrackEventParams, type TrackResult, createProgusConnector, normalizePartnerId, signPayload };
112
+ export { type AppsCatalogEntry, type AssignPartnerIdInput, type CheckPartnerIdResult, type ConnectorConfig, type CrossSellFetchOptions, type CrossSellOptions, type Logger, type SubscriptionEventData, type TrackEventName, type TrackEventParams, type TrackResult, createProgusConnector, fetchAppsCatalog, getCrossSellOffers, getCrossSellOffersFromApi, normalizePartnerId, signPayload };
package/dist/index.js CHANGED
@@ -33,7 +33,19 @@ function createProgusConnector(config) {
33
33
  const enableIdempotency = config.enableIdempotency !== false;
34
34
  const validSubscriptionPeriods = /* @__PURE__ */ new Set(["ANNUAL", "EVERY_30_DAYS"]);
35
35
  if (!fetchImpl) {
36
- throw new Error("Fetch implementation is required");
36
+ const message = "Fetch implementation is required";
37
+ logger?.error?.(message);
38
+ const fail = async () => ({ success: false, message });
39
+ return {
40
+ track: async () => ({ success: false, message }),
41
+ trackInstall: fail,
42
+ trackUninstall: fail,
43
+ trackSubscriptionPurchased: fail,
44
+ trackSubscriptionUpdated: fail,
45
+ trackSubscriptionCancelled: fail,
46
+ assignPartnerId: fail,
47
+ checkPartnerId: async () => ({ success: false, message, status: 500 })
48
+ };
37
49
  }
38
50
  async function postEvent(eventName, payload) {
39
51
  const { shopDomain, partnerId, data, externalId } = payload;
@@ -206,8 +218,110 @@ function createProgusConnector(config) {
206
218
  checkPartnerId
207
219
  };
208
220
  }
221
+
222
+ // src/crossSell.ts
223
+ function getCrossSellOffers(options = {}) {
224
+ const appsCatalog = options.appsCatalog ?? [];
225
+ const installedKeys = new Set(
226
+ [options.appName, ...options.installedAppKeys ?? []].filter(
227
+ (key) => Boolean(key)
228
+ )
229
+ );
230
+ const locale = options.locale;
231
+ return appsCatalog.filter((app) => app.enabled !== false).filter((app) => locale ? !app.locales || app.locales.includes(locale) : true).filter((app) => !installedKeys.has(app.key)).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
232
+ }
233
+ var DEFAULT_APPS_CATALOG_URL = "https://appsdata.progus.com/recommendations";
234
+ var FALLBACK_APPS_CATALOG = [
235
+ {
236
+ key: "progus_ai_studio",
237
+ type: "app",
238
+ title: "Progus AI Studio",
239
+ company: "Progus",
240
+ companyUrl: "https://progus.com",
241
+ desc: "Generate on-brand AI product & variant images in bulk.",
242
+ url: "https://apps.shopify.com/progus-ai-studio",
243
+ icon: "https://cdn.shopify.com/app-store/listing_images/9428a41ed53c66cd7329a2583cd2f3d9/icon/COje1rnS4pEDEAE=.png",
244
+ priority: 100,
245
+ enabled: true
246
+ },
247
+ {
248
+ key: "progus_trust_badges",
249
+ type: "app",
250
+ title: "Progus Trust Badges",
251
+ company: "Progus",
252
+ companyUrl: "https://progus.com",
253
+ desc: "Add Trust Badges to your store to build trust and credibility.",
254
+ url: "https://apps.shopify.com/progus-trust-badges-1",
255
+ icon: "https://cdn.shopify.com/app-store/listing_images/f9d0009e237f27d2db35b41ef99be858/icon/CJ3y1qDn1JEDEAE=.png",
256
+ priority: 80,
257
+ enabled: true
258
+ },
259
+ {
260
+ key: "progus_cod",
261
+ type: "app",
262
+ title: "Progus COD",
263
+ company: "Progus",
264
+ companyUrl: "https://progus.com",
265
+ desc: "Automate COD Fees & Hide/Show Cash on Delivery by Rules.",
266
+ url: "https://apps.shopify.com/progus-cod",
267
+ icon: "https://cdn.shopify.com/app-store/listing_images/bc537219cc3ed2bd4e7e3e683fe6b74a/icon/CMi_6dTEkIoDEAE=.png",
268
+ priority: 90,
269
+ enabled: true
270
+ }
271
+ ];
272
+ function buildFallbackCatalog(options) {
273
+ const appName = options.appName;
274
+ const limit = typeof options.limit === "number" ? options.limit : 3;
275
+ let items = FALLBACK_APPS_CATALOG.filter((app) => app.enabled !== false);
276
+ if (appName) {
277
+ items = items.filter((app) => app.key !== appName);
278
+ }
279
+ if (limit > 0) {
280
+ items = items.slice(0, limit);
281
+ }
282
+ return items;
283
+ }
284
+ async function fetchAppsCatalog(options = {}) {
285
+ const fetchImpl = options.fetch ?? globalThis.fetch;
286
+ const logger = options.logger ?? console;
287
+ const appName = options.appName;
288
+ const baseUrl = stripTrailingSlash(options.appsCatalogUrl ?? DEFAULT_APPS_CATALOG_URL);
289
+ const limit = typeof options.limit === "number" ? options.limit : 3;
290
+ const params = new URLSearchParams();
291
+ if (appName) params.set("appName", appName);
292
+ if (limit > 0) params.set("limit", String(limit));
293
+ const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
294
+ if (!fetchImpl) {
295
+ logger?.error?.("Failed to fetch apps catalog", {
296
+ error: "Fetch implementation is required"
297
+ });
298
+ return buildFallbackCatalog(options);
299
+ }
300
+ try {
301
+ const response = await fetchImpl(url);
302
+ const text = await response.text();
303
+ const parsed = safeJsonParse(text);
304
+ if (!response.ok || !parsed) {
305
+ logger?.error?.("Failed to fetch apps catalog", { status: response.status });
306
+ return buildFallbackCatalog(options);
307
+ }
308
+ return parsed;
309
+ } catch (error) {
310
+ logger?.error?.("Failed to fetch apps catalog", {
311
+ error: error instanceof Error ? error.message : String(error)
312
+ });
313
+ return buildFallbackCatalog(options);
314
+ }
315
+ }
316
+ async function getCrossSellOffersFromApi(options = {}) {
317
+ const appsCatalog = options.appsCatalog ?? await fetchAppsCatalog(options);
318
+ return getCrossSellOffers({ ...options, appsCatalog });
319
+ }
209
320
  export {
210
321
  createProgusConnector,
322
+ fetchAppsCatalog,
323
+ getCrossSellOffers,
324
+ getCrossSellOffersFromApi,
211
325
  normalizePartnerId,
212
326
  signPayload
213
327
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@progus/connector",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "Progus partner/affiliate connector helpers",
5
5
  "license": "MIT",
6
6
  "type": "module",