@progus/connector 0.6.7 → 0.7.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
@@ -8,80 +8,124 @@ Headless connector for Progus partner/affiliate tracking and partner ID assignme
8
8
  npm install @progus/connector
9
9
  ```
10
10
 
11
- ## Usage (Shopify app, Node/TS)
11
+ ## Minimal Integration
12
+
13
+ Integrate the partner program in 4 steps:
14
+
15
+ ### 1. Server connector setup
12
16
 
13
17
  ```ts
18
+ // utils/progus-connector.server.ts
14
19
  import { createProgusConnector } from "@progus/connector";
15
20
 
16
- const connector = createProgusConnector({
17
- appKey: "progus-store-locator",
18
- apiBaseUrl: process.env.PARTNERS_API_URL!,
19
- signingSecret: process.env.PARTNERS_SECRET_KEY!,
20
- apiKey: process.env.PARTNERS_API_KEY,
21
- });
21
+ export function isPartnersConnectorConfigured(): boolean {
22
+ return Boolean(process.env.PARTNERS_API_URL && process.env.PARTNERS_SECRET_KEY);
23
+ }
24
+
25
+ export function getPartnersConnector() {
26
+ return createProgusConnector({
27
+ appKey: "your-app-name",
28
+ apiBaseUrl: process.env.PARTNERS_API_URL || "",
29
+ signingSecret: process.env.PARTNERS_SECRET_KEY || "",
30
+ apiKey: process.env.PARTNERS_API_KEY,
31
+ });
32
+ }
33
+ ```
34
+
35
+ ### 2. Cookie-based install tracking (client-side)
22
36
 
23
- await connector.trackInstall({
37
+ ```ts
38
+ // On app load (e.g. in root route useEffect)
39
+ const cookieMatch = document.cookie.match(/(?:^|; )progus_partner_id=([^;]*)/);
40
+ const partnerId = cookieMatch ? decodeURIComponent(cookieMatch[1]) : null;
41
+
42
+ if (partnerId) {
43
+ await connector.trackInstall({ shopDomain: session.shop, partnerId });
44
+ document.cookie = "progus_partner_id=; Path=/; Max-Age=0"; // clear after tracking
45
+ }
46
+ ```
47
+
48
+ ### 3. Partner ID assignment (Save Partner ID button)
49
+
50
+ ```ts
51
+ // On "Save Partner ID" action
52
+ const result = await connector.assignPartnerIdWithSubscription({
24
53
  shopDomain: session.shop,
25
- partnerId: partnerIdFromCookie,
54
+ partnerId: inputValue,
55
+ admin, // Shopify Admin GraphQL client
26
56
  });
27
57
 
58
+ if (result.success) {
59
+ // Lock the input field, show success
60
+ }
61
+ ```
62
+
63
+ ### 4. Required webhooks
64
+
65
+ **APP_SUBSCRIPTIONS_UPDATE** - track subscription changes:
66
+ ```ts
28
67
  await connector.trackSubscriptionUpdated({
29
- shopDomain: session.shop,
68
+ shopDomain: shop,
30
69
  data: {
31
- subscriptionStatus: "ACTIVE",
32
- subscriptionName: "pro",
33
- subscriptionId: "gid://shopify/AppSubscription/123",
70
+ subscriptionId, // numeric ID (required)
71
+ subscriptionPrice, // number (required)
72
+ subscriptionPeriod, // "EVERY_30_DAYS" or "ANNUAL" (required)
73
+ subscriptionStatus, // e.g. "ACTIVE", "CANCELLED"
74
+ subscriptionName, // plan name
34
75
  },
35
76
  });
77
+ ```
36
78
 
37
- await connector.trackUninstall({ shopDomain: session.shop });
79
+ **APP_UNINSTALLED** - track uninstalls:
80
+ ```ts
81
+ await connector.trackUninstall({ shopDomain: shop });
38
82
  ```
39
83
 
40
- ## Prompt: Partner program implementation (copy/paste)
84
+ ## AI Prompt: Partner program implementation
85
+
86
+ Copy/paste for AI assistants:
41
87
 
42
88
  ```
43
89
  You are integrating Progus Partner Program using @progus/connector in a Shopify app.
44
90
 
45
- Implement these pieces:
46
- 1) Server connector:
47
- - createProgusConnector({ appKey, PARTNERS_API_URL, PARTNERS_SECRET_KEY, PARTNERS_API_KEY })
48
- - expose getPartnersConnector() + isPartnersConnectorConfigured()
91
+ Required files to create/modify:
49
92
 
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
93
+ 1) Server connector (utils/progus-connector.server.ts):
94
+ - createProgusConnector({ appKey, apiBaseUrl, signingSecret, apiKey })
95
+ - export getPartnersConnector() and isPartnersConnectorConfigured()
55
96
 
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 })
97
+ 2) Cookie-based install tracking (client-side, on app load):
98
+ - read cookie "progus_partner_id" (HttpOnly=false, SameSite=None, Secure)
99
+ - if exists: call connector.trackInstall({ shopDomain, partnerId })
100
+ - clear cookie after successful tracking
101
+ - if no cookie, skip tracking
61
102
 
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
103
+ 3) Partner ID assignment UI (on "Save Partner ID" button):
104
+ - call connector.assignPartnerIdWithSubscription({ shopDomain, partnerId, admin })
105
+ - admin = Shopify Admin GraphQL client
106
+ - connector normalizes partnerId and fetches active subscription automatically
107
+ - subscription event sent ONLY if price + period are available
108
+ - lock the Partner ID field after success
109
+ - optionally: check existing partnerId on load with connector.checkPartnerId({ shopDomain })
66
110
 
67
- 5) Uninstall tracking:
68
- - on app/uninstalled webhook, call trackUninstall({ shopDomain })
111
+ 4) APP_SUBSCRIPTIONS_UPDATE webhook (required):
112
+ - call connector.trackSubscriptionUpdated({ shopDomain, data })
113
+ - data must include: subscriptionId (number), subscriptionPrice (number), subscriptionPeriod ("EVERY_30_DAYS" | "ANNUAL")
114
+ - if webhook payload missing price/interval, query: node(id) -> AppSubscription -> lineItems.plan.pricingDetails
115
+ - connector validates; event skipped if price/period missing
69
116
 
70
- Return errors gracefully; do not break webhooks if partner tracking fails.
71
- ```
117
+ 5) APP_UNINSTALLED webhook (required):
118
+ - call connector.trackUninstall({ shopDomain })
72
119
 
73
- ## Partner ID assignment
120
+ Error handling: catch and log errors; never break webhooks on partner tracking failure.
121
+ ```
74
122
 
75
- ```ts
76
- const result = await connector.assignPartnerId({
77
- shopDomain: "my-shop.myshopify.com",
78
- partnerId: "partner_123",
79
- });
123
+ ## Validation
80
124
 
81
- if (result.success) {
82
- // persist partnerId in your app (db/cookie) as needed
83
- }
84
- ```
125
+ The connector handles all validation internally:
126
+ - `trackInstall` normalizes partnerId (trims, checks format)
127
+ - `trackSubscriptionUpdated` validates price/period; skips if missing
128
+ - Apps do not need to validate before calling connector methods
85
129
 
86
130
  ## Optional signing helper
87
131
 
@@ -40,9 +40,10 @@ type CheckPartnerIdResult = {
40
40
  message?: string;
41
41
  status?: number;
42
42
  };
43
- type AssignPartnerIdInput = {
43
+ type AssignPartnerIdWithSubscriptionInput = {
44
44
  shopDomain: string;
45
45
  partnerId: string;
46
+ admin: ShopifyAdminGraphQLClient;
46
47
  };
47
48
  type SubscriptionEventData = {
48
49
  subscriptionStatus?: string;
@@ -82,4 +83,4 @@ declare function getCrossSellOffers(options?: CrossSellOptions): AppsCatalogEntr
82
83
  declare function fetchAppsCatalog(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
83
84
  declare function getCrossSellOffersFromApi(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
84
85
 
85
- export { type AssignPartnerIdInput as A, type ConnectorConfig as C, type Logger as L, type SubscriptionEventData as S, type TrackEventParams as T, type TrackResult as a, type CheckPartnerIdResult as b, type ShopifyAdminGraphQLClient as c, getCrossSellOffersFromApi as d, type AppsCatalogEntry as e, fetchAppsCatalog as f, getCrossSellOffers as g, type CrossSellFetchOptions as h, type CrossSellOptions as i, type TrackEventName as j };
86
+ export { type AssignPartnerIdWithSubscriptionInput as A, type ConnectorConfig as C, type Logger as L, type SubscriptionEventData as S, type TrackEventParams as T, type TrackResult as a, type CheckPartnerIdResult as b, type ShopifyAdminGraphQLClient as c, getCrossSellOffersFromApi as d, type AppsCatalogEntry as e, fetchAppsCatalog as f, getCrossSellOffers as g, type CrossSellFetchOptions as h, type CrossSellOptions as i, type TrackEventName as j };
@@ -40,9 +40,10 @@ type CheckPartnerIdResult = {
40
40
  message?: string;
41
41
  status?: number;
42
42
  };
43
- type AssignPartnerIdInput = {
43
+ type AssignPartnerIdWithSubscriptionInput = {
44
44
  shopDomain: string;
45
45
  partnerId: string;
46
+ admin: ShopifyAdminGraphQLClient;
46
47
  };
47
48
  type SubscriptionEventData = {
48
49
  subscriptionStatus?: string;
@@ -82,4 +83,4 @@ declare function getCrossSellOffers(options?: CrossSellOptions): AppsCatalogEntr
82
83
  declare function fetchAppsCatalog(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
83
84
  declare function getCrossSellOffersFromApi(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
84
85
 
85
- export { type AssignPartnerIdInput as A, type ConnectorConfig as C, type Logger as L, type SubscriptionEventData as S, type TrackEventParams as T, type TrackResult as a, type CheckPartnerIdResult as b, type ShopifyAdminGraphQLClient as c, getCrossSellOffersFromApi as d, type AppsCatalogEntry as e, fetchAppsCatalog as f, getCrossSellOffers as g, type CrossSellFetchOptions as h, type CrossSellOptions as i, type TrackEventName as j };
86
+ export { type AssignPartnerIdWithSubscriptionInput as A, type ConnectorConfig as C, type Logger as L, type SubscriptionEventData as S, type TrackEventParams as T, type TrackResult as a, type CheckPartnerIdResult as b, type ShopifyAdminGraphQLClient as c, getCrossSellOffersFromApi as d, type AppsCatalogEntry as e, fetchAppsCatalog as f, getCrossSellOffers as g, type CrossSellFetchOptions as h, type CrossSellOptions as i, type TrackEventName as j };
@@ -1 +1 @@
1
- export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-Bs_6xA6f.cjs';
1
+ export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-BdqPj2g7.cjs';
@@ -1 +1 @@
1
- export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-Bs_6xA6f.js';
1
+ export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-BdqPj2g7.js';
package/dist/index.cjs CHANGED
@@ -30,6 +30,142 @@ __export(index_exports, {
30
30
  });
31
31
  module.exports = __toCommonJS(index_exports);
32
32
 
33
+ // src/subscription.ts
34
+ var ACTIVE_SUBSCRIPTIONS_QUERY = `
35
+ query {
36
+ currentAppInstallation {
37
+ activeSubscriptions {
38
+ id
39
+ name
40
+ status
41
+ lineItems {
42
+ plan {
43
+ pricingDetails {
44
+ __typename
45
+ ... on AppRecurringPricing {
46
+ interval
47
+ price {
48
+ amount
49
+ }
50
+ }
51
+ ... on AppUsagePricing {
52
+ cappedAmount {
53
+ amount
54
+ }
55
+ terms
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ `;
64
+ var SUBSCRIPTION_DETAILS_QUERY = `
65
+ query AppSubscriptionDetails($id: ID!) {
66
+ node(id: $id) {
67
+ ... on AppSubscription {
68
+ id
69
+ name
70
+ status
71
+ lineItems {
72
+ plan {
73
+ pricingDetails {
74
+ __typename
75
+ ... on AppRecurringPricing {
76
+ interval
77
+ price {
78
+ amount
79
+ }
80
+ }
81
+ ... on AppUsagePricing {
82
+ cappedAmount {
83
+ amount
84
+ }
85
+ terms
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ `;
94
+ function normalizeInterval(intervalRaw, nameHint) {
95
+ const interval = intervalRaw ? String(intervalRaw).toUpperCase() : "";
96
+ if (interval === "ANNUAL" || interval === "EVERY_30_DAYS") return interval;
97
+ if (interval === "MONTHLY" || interval === "EVERY_30DAYS") return "EVERY_30_DAYS";
98
+ const name = (nameHint ?? "").toLowerCase();
99
+ if (name.includes("annual") || name.includes("year")) return "ANNUAL";
100
+ if (name) return "EVERY_30_DAYS";
101
+ return "";
102
+ }
103
+ function parseSubscriptionId(rawId) {
104
+ if (!rawId) return null;
105
+ return rawId.match(/\d+$/)?.[0] ?? rawId.split("/").pop() ?? rawId;
106
+ }
107
+ function parsePrice(rawPrice) {
108
+ if (typeof rawPrice === "number") return rawPrice;
109
+ if (typeof rawPrice === "string") {
110
+ const parsed = Number(rawPrice);
111
+ return Number.isFinite(parsed) ? parsed : null;
112
+ }
113
+ return null;
114
+ }
115
+ function extractPricing(subscription) {
116
+ const pricingDetails = subscription?.lineItems?.[0]?.plan?.pricingDetails;
117
+ const rawPrice = pricingDetails?.price?.amount ?? pricingDetails?.cappedAmount?.amount;
118
+ const subscriptionPrice = parsePrice(rawPrice);
119
+ const subscriptionPeriod = normalizeInterval(pricingDetails?.interval, subscription?.name);
120
+ return { subscriptionPrice, subscriptionPeriod };
121
+ }
122
+ async function fetchActiveSubscriptionEventData(admin, logger = console) {
123
+ const response = await admin.graphql(ACTIVE_SUBSCRIPTIONS_QUERY);
124
+ const result = await response.json();
125
+ if (result.errors?.length) {
126
+ logger.error?.("Partner subscription query errors", { errors: result.errors });
127
+ }
128
+ const subscriptions = result.data?.currentAppInstallation?.activeSubscriptions ?? [];
129
+ const active = subscriptions.find((sub) => String(sub.status ?? "").toUpperCase() === "ACTIVE") ?? subscriptions[0];
130
+ const subscriptionId = parseSubscriptionId(active?.id);
131
+ if (!subscriptionId) return null;
132
+ let { subscriptionPrice, subscriptionPeriod } = extractPricing(active);
133
+ if (!subscriptionPeriod || subscriptionPrice === null) {
134
+ try {
135
+ const detailsResponse = await admin.graphql(SUBSCRIPTION_DETAILS_QUERY, {
136
+ variables: { id: active?.id }
137
+ });
138
+ const detailsJson = await detailsResponse.json();
139
+ if (detailsJson.errors?.length) {
140
+ logger.error?.("Partner subscription details query errors", { errors: detailsJson.errors });
141
+ }
142
+ const details = detailsJson.data?.node;
143
+ ({ subscriptionPrice, subscriptionPeriod } = extractPricing(details));
144
+ if (!subscriptionPeriod || subscriptionPrice === null) return null;
145
+ return {
146
+ subscriptionStatus: details?.status ?? active?.status ?? "ACTIVE",
147
+ subscriptionName: details?.name ?? active?.name,
148
+ subscriptionId,
149
+ subscriptionPrice,
150
+ subscriptionPeriod
151
+ };
152
+ } catch (error) {
153
+ logger.error?.("Partner subscription details query failed", {
154
+ error: error instanceof Error ? error.message : String(error)
155
+ });
156
+ return null;
157
+ }
158
+ }
159
+ if (!subscriptionPeriod || subscriptionPrice === null) return null;
160
+ return {
161
+ subscriptionStatus: active?.status ?? "ACTIVE",
162
+ subscriptionName: active?.name,
163
+ subscriptionId,
164
+ subscriptionPrice,
165
+ subscriptionPeriod
166
+ };
167
+ }
168
+
33
169
  // src/signing.ts
34
170
  var import_crypto = require("crypto");
35
171
  function signPayload(body, secret) {
@@ -75,7 +211,7 @@ function createProgusConnector(config) {
75
211
  trackSubscriptionPurchased: fail,
76
212
  trackSubscriptionUpdated: fail,
77
213
  trackSubscriptionCancelled: fail,
78
- assignPartnerId: fail,
214
+ assignPartnerIdWithSubscription: fail,
79
215
  checkPartnerId: async () => ({ success: false, message, status: 500 })
80
216
  };
81
217
  }
@@ -169,12 +305,27 @@ function createProgusConnector(config) {
169
305
  externalId: String(normalized.subscriptionId)
170
306
  });
171
307
  }
172
- async function assignPartnerId(payload) {
173
- const normalized = normalizePartnerId(payload.partnerId);
308
+ async function assignPartnerIdWithSubscription(payload) {
309
+ const { admin, shopDomain, partnerId } = payload;
310
+ const normalized = normalizePartnerId(partnerId);
174
311
  if (!normalized) {
175
312
  return { success: false, message: "Partner ID is required" };
176
313
  }
177
- const result = await trackInstall({ shopDomain: payload.shopDomain, partnerId: normalized });
314
+ const result = await trackInstall({ shopDomain, partnerId: normalized });
315
+ if (!result.success) return { ...result, partnerId: normalized };
316
+ try {
317
+ const subscriptionPayload = await fetchActiveSubscriptionEventData(admin, logger);
318
+ if (subscriptionPayload) {
319
+ await trackSubscription({
320
+ shopDomain,
321
+ data: subscriptionPayload
322
+ });
323
+ }
324
+ } catch (error) {
325
+ logger?.error?.("Partner subscription sync failed", {
326
+ error: error instanceof Error ? error.message : String(error)
327
+ });
328
+ }
178
329
  return { ...result, partnerId: normalized };
179
330
  }
180
331
  async function checkPartnerId(payload) {
@@ -246,7 +397,7 @@ function createProgusConnector(config) {
246
397
  trackSubscriptionPurchased: trackSubscription,
247
398
  trackSubscriptionUpdated: trackSubscription,
248
399
  trackSubscriptionCancelled: trackSubscription,
249
- assignPartnerId,
400
+ assignPartnerIdWithSubscription,
250
401
  checkPartnerId
251
402
  };
252
403
  }
@@ -349,142 +500,6 @@ async function getCrossSellOffersFromApi(options = {}) {
349
500
  const appsCatalog = options.appsCatalog ?? await fetchAppsCatalog(options);
350
501
  return getCrossSellOffers({ ...options, appsCatalog });
351
502
  }
352
-
353
- // src/subscription.ts
354
- var ACTIVE_SUBSCRIPTIONS_QUERY = `
355
- query {
356
- currentAppInstallation {
357
- activeSubscriptions {
358
- id
359
- name
360
- status
361
- lineItems {
362
- plan {
363
- pricingDetails {
364
- __typename
365
- ... on AppRecurringPricing {
366
- interval
367
- price {
368
- amount
369
- }
370
- }
371
- ... on AppUsagePricing {
372
- cappedAmount {
373
- amount
374
- }
375
- terms
376
- }
377
- }
378
- }
379
- }
380
- }
381
- }
382
- }
383
- `;
384
- var SUBSCRIPTION_DETAILS_QUERY = `
385
- query AppSubscriptionDetails($id: ID!) {
386
- node(id: $id) {
387
- ... on AppSubscription {
388
- id
389
- name
390
- status
391
- lineItems {
392
- plan {
393
- pricingDetails {
394
- __typename
395
- ... on AppRecurringPricing {
396
- interval
397
- price {
398
- amount
399
- }
400
- }
401
- ... on AppUsagePricing {
402
- cappedAmount {
403
- amount
404
- }
405
- terms
406
- }
407
- }
408
- }
409
- }
410
- }
411
- }
412
- }
413
- `;
414
- function normalizeInterval(intervalRaw, nameHint) {
415
- const interval = intervalRaw ? String(intervalRaw).toUpperCase() : "";
416
- if (interval === "ANNUAL" || interval === "EVERY_30_DAYS") return interval;
417
- if (interval === "MONTHLY" || interval === "EVERY_30DAYS") return "EVERY_30_DAYS";
418
- const name = (nameHint ?? "").toLowerCase();
419
- if (name.includes("annual") || name.includes("year")) return "ANNUAL";
420
- if (name) return "EVERY_30_DAYS";
421
- return "";
422
- }
423
- function parseSubscriptionId(rawId) {
424
- if (!rawId) return null;
425
- return rawId.match(/\d+$/)?.[0] ?? rawId.split("/").pop() ?? rawId;
426
- }
427
- function parsePrice(rawPrice) {
428
- if (typeof rawPrice === "number") return rawPrice;
429
- if (typeof rawPrice === "string") {
430
- const parsed = Number(rawPrice);
431
- return Number.isFinite(parsed) ? parsed : null;
432
- }
433
- return null;
434
- }
435
- function extractPricing(subscription) {
436
- const pricingDetails = subscription?.lineItems?.[0]?.plan?.pricingDetails;
437
- const rawPrice = pricingDetails?.price?.amount ?? pricingDetails?.cappedAmount?.amount;
438
- const subscriptionPrice = parsePrice(rawPrice);
439
- const subscriptionPeriod = normalizeInterval(pricingDetails?.interval, subscription?.name);
440
- return { subscriptionPrice, subscriptionPeriod };
441
- }
442
- async function fetchActiveSubscriptionEventData(admin, logger = console) {
443
- const response = await admin.graphql(ACTIVE_SUBSCRIPTIONS_QUERY);
444
- const result = await response.json();
445
- if (result.errors?.length) {
446
- logger.error?.("Partner subscription query errors", { errors: result.errors });
447
- }
448
- const subscriptions = result.data?.currentAppInstallation?.activeSubscriptions ?? [];
449
- const active = subscriptions.find((sub) => String(sub.status ?? "").toUpperCase() === "ACTIVE") ?? subscriptions[0];
450
- const subscriptionId = parseSubscriptionId(active?.id);
451
- if (!subscriptionId) return null;
452
- let { subscriptionPrice, subscriptionPeriod } = extractPricing(active);
453
- if (!subscriptionPeriod || subscriptionPrice === null) {
454
- try {
455
- const detailsResponse = await admin.graphql(SUBSCRIPTION_DETAILS_QUERY, {
456
- variables: { id: active?.id }
457
- });
458
- const detailsJson = await detailsResponse.json();
459
- if (detailsJson.errors?.length) {
460
- logger.error?.("Partner subscription details query errors", { errors: detailsJson.errors });
461
- }
462
- const details = detailsJson.data?.node;
463
- ({ subscriptionPrice, subscriptionPeriod } = extractPricing(details));
464
- if (!subscriptionPeriod || subscriptionPrice === null) return null;
465
- return {
466
- subscriptionStatus: details?.status ?? active?.status ?? "ACTIVE",
467
- subscriptionName: details?.name ?? active?.name,
468
- subscriptionId,
469
- subscriptionPrice,
470
- subscriptionPeriod
471
- };
472
- } catch (error) {
473
- logger.error?.("Partner subscription details query failed", {
474
- error: error instanceof Error ? error.message : String(error)
475
- });
476
- return null;
477
- }
478
- }
479
- if (!subscriptionPeriod || subscriptionPrice === null) return null;
480
- return {
481
- subscriptionStatus: active?.status ?? "ACTIVE",
482
- subscriptionName: active?.name,
483
- subscriptionId,
484
- subscriptionPrice,
485
- subscriptionPeriod
486
- };
487
- }
488
503
  // Annotate the CommonJS export names for ESM import in node:
489
504
  0 && (module.exports = {
490
505
  createProgusConnector,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as ConnectorConfig, T as TrackEventParams, a as TrackResult, S as SubscriptionEventData, A as AssignPartnerIdInput, b as CheckPartnerIdResult, c as ShopifyAdminGraphQLClient } from './crossSell-Bs_6xA6f.cjs';
2
- export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, L as Logger, j as TrackEventName, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-Bs_6xA6f.cjs';
1
+ import { C as ConnectorConfig, T as TrackEventParams, a as TrackResult, S as SubscriptionEventData, A as AssignPartnerIdWithSubscriptionInput, b as CheckPartnerIdResult, c as ShopifyAdminGraphQLClient } from './crossSell-BdqPj2g7.cjs';
2
+ export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, L as Logger, j as TrackEventName, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-BdqPj2g7.cjs';
3
3
 
4
4
  type Connector = {
5
5
  track: (eventName: TrackEventParams["eventName"], payload: Omit<TrackEventParams, "eventName">) => Promise<TrackResult>;
@@ -22,7 +22,7 @@ type Connector = {
22
22
  shopDomain: string;
23
23
  data: SubscriptionEventData;
24
24
  }) => Promise<TrackResult>;
25
- assignPartnerId: (payload: AssignPartnerIdInput) => Promise<TrackResult & {
25
+ assignPartnerIdWithSubscription: (payload: AssignPartnerIdWithSubscriptionInput) => Promise<TrackResult & {
26
26
  partnerId?: string;
27
27
  }>;
28
28
  checkPartnerId: (payload: {
@@ -39,4 +39,4 @@ declare function signPayload(body: string, secret: string): string;
39
39
 
40
40
  declare function normalizePartnerId(value?: string | null): string | null;
41
41
 
42
- export { AssignPartnerIdInput, CheckPartnerIdResult, ConnectorConfig, ShopifyAdminGraphQLClient, SubscriptionEventData, TrackEventParams, TrackResult, createProgusConnector, fetchActiveSubscriptionEventData, normalizePartnerId, signPayload };
42
+ export { AssignPartnerIdWithSubscriptionInput, CheckPartnerIdResult, ConnectorConfig, ShopifyAdminGraphQLClient, SubscriptionEventData, TrackEventParams, TrackResult, createProgusConnector, fetchActiveSubscriptionEventData, normalizePartnerId, signPayload };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as ConnectorConfig, T as TrackEventParams, a as TrackResult, S as SubscriptionEventData, A as AssignPartnerIdInput, b as CheckPartnerIdResult, c as ShopifyAdminGraphQLClient } from './crossSell-Bs_6xA6f.js';
2
- export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, L as Logger, j as TrackEventName, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-Bs_6xA6f.js';
1
+ import { C as ConnectorConfig, T as TrackEventParams, a as TrackResult, S as SubscriptionEventData, A as AssignPartnerIdWithSubscriptionInput, b as CheckPartnerIdResult, c as ShopifyAdminGraphQLClient } from './crossSell-BdqPj2g7.js';
2
+ export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, L as Logger, j as TrackEventName, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-BdqPj2g7.js';
3
3
 
4
4
  type Connector = {
5
5
  track: (eventName: TrackEventParams["eventName"], payload: Omit<TrackEventParams, "eventName">) => Promise<TrackResult>;
@@ -22,7 +22,7 @@ type Connector = {
22
22
  shopDomain: string;
23
23
  data: SubscriptionEventData;
24
24
  }) => Promise<TrackResult>;
25
- assignPartnerId: (payload: AssignPartnerIdInput) => Promise<TrackResult & {
25
+ assignPartnerIdWithSubscription: (payload: AssignPartnerIdWithSubscriptionInput) => Promise<TrackResult & {
26
26
  partnerId?: string;
27
27
  }>;
28
28
  checkPartnerId: (payload: {
@@ -39,4 +39,4 @@ declare function signPayload(body: string, secret: string): string;
39
39
 
40
40
  declare function normalizePartnerId(value?: string | null): string | null;
41
41
 
42
- export { AssignPartnerIdInput, CheckPartnerIdResult, ConnectorConfig, ShopifyAdminGraphQLClient, SubscriptionEventData, TrackEventParams, TrackResult, createProgusConnector, fetchActiveSubscriptionEventData, normalizePartnerId, signPayload };
42
+ export { AssignPartnerIdWithSubscriptionInput, CheckPartnerIdResult, ConnectorConfig, ShopifyAdminGraphQLClient, SubscriptionEventData, TrackEventParams, TrackResult, createProgusConnector, fetchActiveSubscriptionEventData, normalizePartnerId, signPayload };
package/dist/index.js CHANGED
@@ -8,6 +8,142 @@ import {
8
8
  stripTrailingSlash
9
9
  } from "./chunk-WZ5FAA44.js";
10
10
 
11
+ // src/subscription.ts
12
+ var ACTIVE_SUBSCRIPTIONS_QUERY = `
13
+ query {
14
+ currentAppInstallation {
15
+ activeSubscriptions {
16
+ id
17
+ name
18
+ status
19
+ lineItems {
20
+ plan {
21
+ pricingDetails {
22
+ __typename
23
+ ... on AppRecurringPricing {
24
+ interval
25
+ price {
26
+ amount
27
+ }
28
+ }
29
+ ... on AppUsagePricing {
30
+ cappedAmount {
31
+ amount
32
+ }
33
+ terms
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ `;
42
+ var SUBSCRIPTION_DETAILS_QUERY = `
43
+ query AppSubscriptionDetails($id: ID!) {
44
+ node(id: $id) {
45
+ ... on AppSubscription {
46
+ id
47
+ name
48
+ status
49
+ lineItems {
50
+ plan {
51
+ pricingDetails {
52
+ __typename
53
+ ... on AppRecurringPricing {
54
+ interval
55
+ price {
56
+ amount
57
+ }
58
+ }
59
+ ... on AppUsagePricing {
60
+ cappedAmount {
61
+ amount
62
+ }
63
+ terms
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ `;
72
+ function normalizeInterval(intervalRaw, nameHint) {
73
+ const interval = intervalRaw ? String(intervalRaw).toUpperCase() : "";
74
+ if (interval === "ANNUAL" || interval === "EVERY_30_DAYS") return interval;
75
+ if (interval === "MONTHLY" || interval === "EVERY_30DAYS") return "EVERY_30_DAYS";
76
+ const name = (nameHint ?? "").toLowerCase();
77
+ if (name.includes("annual") || name.includes("year")) return "ANNUAL";
78
+ if (name) return "EVERY_30_DAYS";
79
+ return "";
80
+ }
81
+ function parseSubscriptionId(rawId) {
82
+ if (!rawId) return null;
83
+ return rawId.match(/\d+$/)?.[0] ?? rawId.split("/").pop() ?? rawId;
84
+ }
85
+ function parsePrice(rawPrice) {
86
+ if (typeof rawPrice === "number") return rawPrice;
87
+ if (typeof rawPrice === "string") {
88
+ const parsed = Number(rawPrice);
89
+ return Number.isFinite(parsed) ? parsed : null;
90
+ }
91
+ return null;
92
+ }
93
+ function extractPricing(subscription) {
94
+ const pricingDetails = subscription?.lineItems?.[0]?.plan?.pricingDetails;
95
+ const rawPrice = pricingDetails?.price?.amount ?? pricingDetails?.cappedAmount?.amount;
96
+ const subscriptionPrice = parsePrice(rawPrice);
97
+ const subscriptionPeriod = normalizeInterval(pricingDetails?.interval, subscription?.name);
98
+ return { subscriptionPrice, subscriptionPeriod };
99
+ }
100
+ async function fetchActiveSubscriptionEventData(admin, logger = console) {
101
+ const response = await admin.graphql(ACTIVE_SUBSCRIPTIONS_QUERY);
102
+ const result = await response.json();
103
+ if (result.errors?.length) {
104
+ logger.error?.("Partner subscription query errors", { errors: result.errors });
105
+ }
106
+ const subscriptions = result.data?.currentAppInstallation?.activeSubscriptions ?? [];
107
+ const active = subscriptions.find((sub) => String(sub.status ?? "").toUpperCase() === "ACTIVE") ?? subscriptions[0];
108
+ const subscriptionId = parseSubscriptionId(active?.id);
109
+ if (!subscriptionId) return null;
110
+ let { subscriptionPrice, subscriptionPeriod } = extractPricing(active);
111
+ if (!subscriptionPeriod || subscriptionPrice === null) {
112
+ try {
113
+ const detailsResponse = await admin.graphql(SUBSCRIPTION_DETAILS_QUERY, {
114
+ variables: { id: active?.id }
115
+ });
116
+ const detailsJson = await detailsResponse.json();
117
+ if (detailsJson.errors?.length) {
118
+ logger.error?.("Partner subscription details query errors", { errors: detailsJson.errors });
119
+ }
120
+ const details = detailsJson.data?.node;
121
+ ({ subscriptionPrice, subscriptionPeriod } = extractPricing(details));
122
+ if (!subscriptionPeriod || subscriptionPrice === null) return null;
123
+ return {
124
+ subscriptionStatus: details?.status ?? active?.status ?? "ACTIVE",
125
+ subscriptionName: details?.name ?? active?.name,
126
+ subscriptionId,
127
+ subscriptionPrice,
128
+ subscriptionPeriod
129
+ };
130
+ } catch (error) {
131
+ logger.error?.("Partner subscription details query failed", {
132
+ error: error instanceof Error ? error.message : String(error)
133
+ });
134
+ return null;
135
+ }
136
+ }
137
+ if (!subscriptionPeriod || subscriptionPrice === null) return null;
138
+ return {
139
+ subscriptionStatus: active?.status ?? "ACTIVE",
140
+ subscriptionName: active?.name,
141
+ subscriptionId,
142
+ subscriptionPrice,
143
+ subscriptionPeriod
144
+ };
145
+ }
146
+
11
147
  // src/signing.ts
12
148
  import { createHmac } from "crypto";
13
149
  function signPayload(body, secret) {
@@ -32,7 +168,7 @@ function createProgusConnector(config) {
32
168
  trackSubscriptionPurchased: fail,
33
169
  trackSubscriptionUpdated: fail,
34
170
  trackSubscriptionCancelled: fail,
35
- assignPartnerId: fail,
171
+ assignPartnerIdWithSubscription: fail,
36
172
  checkPartnerId: async () => ({ success: false, message, status: 500 })
37
173
  };
38
174
  }
@@ -126,12 +262,27 @@ function createProgusConnector(config) {
126
262
  externalId: String(normalized.subscriptionId)
127
263
  });
128
264
  }
129
- async function assignPartnerId(payload) {
130
- const normalized = normalizePartnerId(payload.partnerId);
265
+ async function assignPartnerIdWithSubscription(payload) {
266
+ const { admin, shopDomain, partnerId } = payload;
267
+ const normalized = normalizePartnerId(partnerId);
131
268
  if (!normalized) {
132
269
  return { success: false, message: "Partner ID is required" };
133
270
  }
134
- const result = await trackInstall({ shopDomain: payload.shopDomain, partnerId: normalized });
271
+ const result = await trackInstall({ shopDomain, partnerId: normalized });
272
+ if (!result.success) return { ...result, partnerId: normalized };
273
+ try {
274
+ const subscriptionPayload = await fetchActiveSubscriptionEventData(admin, logger);
275
+ if (subscriptionPayload) {
276
+ await trackSubscription({
277
+ shopDomain,
278
+ data: subscriptionPayload
279
+ });
280
+ }
281
+ } catch (error) {
282
+ logger?.error?.("Partner subscription sync failed", {
283
+ error: error instanceof Error ? error.message : String(error)
284
+ });
285
+ }
135
286
  return { ...result, partnerId: normalized };
136
287
  }
137
288
  async function checkPartnerId(payload) {
@@ -203,146 +354,10 @@ function createProgusConnector(config) {
203
354
  trackSubscriptionPurchased: trackSubscription,
204
355
  trackSubscriptionUpdated: trackSubscription,
205
356
  trackSubscriptionCancelled: trackSubscription,
206
- assignPartnerId,
357
+ assignPartnerIdWithSubscription,
207
358
  checkPartnerId
208
359
  };
209
360
  }
210
-
211
- // src/subscription.ts
212
- var ACTIVE_SUBSCRIPTIONS_QUERY = `
213
- query {
214
- currentAppInstallation {
215
- activeSubscriptions {
216
- id
217
- name
218
- status
219
- lineItems {
220
- plan {
221
- pricingDetails {
222
- __typename
223
- ... on AppRecurringPricing {
224
- interval
225
- price {
226
- amount
227
- }
228
- }
229
- ... on AppUsagePricing {
230
- cappedAmount {
231
- amount
232
- }
233
- terms
234
- }
235
- }
236
- }
237
- }
238
- }
239
- }
240
- }
241
- `;
242
- var SUBSCRIPTION_DETAILS_QUERY = `
243
- query AppSubscriptionDetails($id: ID!) {
244
- node(id: $id) {
245
- ... on AppSubscription {
246
- id
247
- name
248
- status
249
- lineItems {
250
- plan {
251
- pricingDetails {
252
- __typename
253
- ... on AppRecurringPricing {
254
- interval
255
- price {
256
- amount
257
- }
258
- }
259
- ... on AppUsagePricing {
260
- cappedAmount {
261
- amount
262
- }
263
- terms
264
- }
265
- }
266
- }
267
- }
268
- }
269
- }
270
- }
271
- `;
272
- function normalizeInterval(intervalRaw, nameHint) {
273
- const interval = intervalRaw ? String(intervalRaw).toUpperCase() : "";
274
- if (interval === "ANNUAL" || interval === "EVERY_30_DAYS") return interval;
275
- if (interval === "MONTHLY" || interval === "EVERY_30DAYS") return "EVERY_30_DAYS";
276
- const name = (nameHint ?? "").toLowerCase();
277
- if (name.includes("annual") || name.includes("year")) return "ANNUAL";
278
- if (name) return "EVERY_30_DAYS";
279
- return "";
280
- }
281
- function parseSubscriptionId(rawId) {
282
- if (!rawId) return null;
283
- return rawId.match(/\d+$/)?.[0] ?? rawId.split("/").pop() ?? rawId;
284
- }
285
- function parsePrice(rawPrice) {
286
- if (typeof rawPrice === "number") return rawPrice;
287
- if (typeof rawPrice === "string") {
288
- const parsed = Number(rawPrice);
289
- return Number.isFinite(parsed) ? parsed : null;
290
- }
291
- return null;
292
- }
293
- function extractPricing(subscription) {
294
- const pricingDetails = subscription?.lineItems?.[0]?.plan?.pricingDetails;
295
- const rawPrice = pricingDetails?.price?.amount ?? pricingDetails?.cappedAmount?.amount;
296
- const subscriptionPrice = parsePrice(rawPrice);
297
- const subscriptionPeriod = normalizeInterval(pricingDetails?.interval, subscription?.name);
298
- return { subscriptionPrice, subscriptionPeriod };
299
- }
300
- async function fetchActiveSubscriptionEventData(admin, logger = console) {
301
- const response = await admin.graphql(ACTIVE_SUBSCRIPTIONS_QUERY);
302
- const result = await response.json();
303
- if (result.errors?.length) {
304
- logger.error?.("Partner subscription query errors", { errors: result.errors });
305
- }
306
- const subscriptions = result.data?.currentAppInstallation?.activeSubscriptions ?? [];
307
- const active = subscriptions.find((sub) => String(sub.status ?? "").toUpperCase() === "ACTIVE") ?? subscriptions[0];
308
- const subscriptionId = parseSubscriptionId(active?.id);
309
- if (!subscriptionId) return null;
310
- let { subscriptionPrice, subscriptionPeriod } = extractPricing(active);
311
- if (!subscriptionPeriod || subscriptionPrice === null) {
312
- try {
313
- const detailsResponse = await admin.graphql(SUBSCRIPTION_DETAILS_QUERY, {
314
- variables: { id: active?.id }
315
- });
316
- const detailsJson = await detailsResponse.json();
317
- if (detailsJson.errors?.length) {
318
- logger.error?.("Partner subscription details query errors", { errors: detailsJson.errors });
319
- }
320
- const details = detailsJson.data?.node;
321
- ({ subscriptionPrice, subscriptionPeriod } = extractPricing(details));
322
- if (!subscriptionPeriod || subscriptionPrice === null) return null;
323
- return {
324
- subscriptionStatus: details?.status ?? active?.status ?? "ACTIVE",
325
- subscriptionName: details?.name ?? active?.name,
326
- subscriptionId,
327
- subscriptionPrice,
328
- subscriptionPeriod
329
- };
330
- } catch (error) {
331
- logger.error?.("Partner subscription details query failed", {
332
- error: error instanceof Error ? error.message : String(error)
333
- });
334
- return null;
335
- }
336
- }
337
- if (!subscriptionPeriod || subscriptionPrice === null) return null;
338
- return {
339
- subscriptionStatus: active?.status ?? "ACTIVE",
340
- subscriptionName: active?.name,
341
- subscriptionId,
342
- subscriptionPrice,
343
- subscriptionPeriod
344
- };
345
- }
346
361
  export {
347
362
  createProgusConnector,
348
363
  fetchActiveSubscriptionEventData,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@progus/connector",
3
- "version": "0.6.7",
3
+ "version": "0.7.0",
4
4
  "description": "Progus partner/affiliate connector helpers",
5
5
  "license": "MIT",
6
6
  "type": "module",