insert-affiliate-js-sdk 1.2.0 → 1.4.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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to the Insert Affiliate JavaScript SDK will be documented in
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.3.1] - 2026-03-29
9
+
10
+ ### Fixed
11
+ - **Offer code sanitization** - Fixed offer codes with dashes/underscores being stripped (e.g. `pro-v3-ext` was incorrectly becoming `prov3ext`)
12
+ - Now checks for API error responses before cleaning the offer code
13
+ - Sanitization regex updated to preserve dashes and underscores
14
+
8
15
  ## [1.1.0] - 2025-11-23
9
16
 
10
17
  ### Added
package/README.md CHANGED
@@ -89,10 +89,23 @@ await InsertAffiliate.initialize('YOUR_COMPANY_CODE');
89
89
  <summary><strong>Advanced Initialization Options</strong> (click to expand)</summary>
90
90
 
91
91
  ```javascript
92
- // Enable verbose logging for debugging
93
- await InsertAffiliate.initialize('YOUR_COMPANY_CODE', true);
92
+ // Full initialization with all options
93
+ await InsertAffiliate.initialize(
94
+ 'YOUR_COMPANY_CODE', // Company code (required)
95
+ true, // Enable verbose logging (optional, default: false)
96
+ 86400000, // Attribution timeout in milliseconds (optional, e.g., 24 hours)
97
+ true // Prevent affiliate transfer (optional, default: false)
98
+ );
94
99
  ```
95
100
 
101
+ **Parameters:**
102
+ - `companyCode` (required): Your Insert Affiliate company code
103
+ - `verboseLogging` (optional): Enable detailed console logs for debugging
104
+ - `affiliateAttributionActiveTime` (optional): Time in milliseconds before attribution expires (e.g., `86400000` for 24 hours)
105
+ - `preventAffiliateTransfer` (optional): When `true`, prevents a new affiliate link from overwriting an existing affiliate attribution (defaults to `false`)
106
+ - Use this to ensure the first affiliate who acquired the user always gets credit
107
+ - New affiliate links will be silently ignored if the user already has an affiliate
108
+
96
109
  **Verbose logging shows:**
97
110
  - Initialization process and company code validation
98
111
  - Deep link processing and short code detection
@@ -125,17 +138,57 @@ import { InsertAffiliate } from 'insert-affiliate-js-sdk';
125
138
  import { Purchases } from '@revenuecat/purchases-capacitor';
126
139
 
127
140
  window.addEventListener('DOMContentLoaded', async () => {
128
- await InsertAffiliate.initialize('YOUR_COMPANY_CODE');
141
+ await InsertAffiliate.initialize(
142
+ 'YOUR_COMPANY_CODE',
143
+ false, // verbose logging
144
+ 86400000, // 24 hour attribution timeout (optional)
145
+ true // prevent affiliate transfer (optional)
146
+ );
129
147
  await Purchases.configure({ apiKey: 'YOUR_REVENUECAT_API_KEY' });
130
148
 
131
- const affiliateIdentifier = await InsertAffiliate.returnInsertAffiliateIdentifier();
149
+ // Set up callback for when affiliate identifier changes
150
+ // Note: Use preventAffiliateTransfer in initialize() to block affiliate changes in the SDK
151
+ InsertAffiliate.setInsertAffiliateIdentifierChangeCallback(async (identifier, offerCode) => {
152
+ if (!identifier) return;
132
153
 
133
- if (affiliateIdentifier) {
134
- await Purchases.setAttributes({ insert_affiliate: affiliateIdentifier });
135
- }
154
+ // Ensure RevenueCat subscriber exists before setting attributes
155
+ const customerInfo = await Purchases.getCustomerInfo();
156
+
157
+ // OPTIONAL: Prevent attribution for existing subscribers
158
+ // Uncomment to ensure affiliates only earn from users they actually brought:
159
+ // const hasActiveEntitlement = Object.keys(customerInfo.entitlements.active).length > 0;
160
+ // if (hasActiveEntitlement) return; // User already subscribed, don't attribute
161
+
162
+ // Get expiry timestamp for RevenueCat targeting
163
+ const expiryTimestamp = await InsertAffiliate.getAffiliateExpiryTimestamp();
164
+
165
+ // Set attributes for RevenueCat
166
+ const attributes = {
167
+ insert_affiliate: identifier,
168
+ insert_timedout: expiryTimestamp?.toString() || '',
169
+ };
170
+
171
+ // Add offer code for RevenueCat Targeting (if available)
172
+ if (offerCode) {
173
+ attributes.affiliateOfferCode = offerCode;
174
+ }
175
+
176
+ await Purchases.setAttributes(attributes);
177
+ await Purchases.syncAttributesAndOfferingsIfNeeded();
178
+ });
136
179
  });
137
180
  ```
138
181
 
182
+ **Using RevenueCat Targeting (Recommended)**
183
+
184
+ RevenueCat Targeting automatically shows different offerings based on the `affiliateOfferCode` attribute. Simply display `offerings.current`:
185
+
186
+ ```javascript
187
+ const offerings = await Purchases.getOfferings();
188
+ const currentOffering = offerings.current;
189
+ // RevenueCat targeting automatically shows the correct offering based on affiliateOfferCode
190
+ ```
191
+
139
192
  **Step 2: Webhook Setup**
140
193
 
141
194
  1. In RevenueCat, [create a new webhook](https://www.revenuecat.com/docs/integrations/webhooks)
@@ -386,15 +439,16 @@ Learn more: [Short Codes Documentation](https://docs.insertaffiliate.com/short-c
386
439
  Get notified when the affiliate identifier changes:
387
440
 
388
441
  ```javascript
389
- InsertAffiliate.setInsertAffiliateIdentifierChangeCallback((identifier) => {
442
+ InsertAffiliate.setInsertAffiliateIdentifierChangeCallback((identifier, offerCode) => {
390
443
  if (identifier) {
391
444
  console.log('Affiliate changed:', identifier);
445
+ console.log('Offer code:', offerCode || 'none');
392
446
 
393
447
  // Update UI
394
448
  document.getElementById('affiliate-banner').style.display = 'block';
395
449
 
396
450
  // Track in analytics
397
- analytics.track('affiliate_link_clicked', { identifier });
451
+ analytics.track('affiliate_link_clicked', { identifier, offerCode });
398
452
  }
399
453
  });
400
454
 
@@ -402,8 +456,33 @@ InsertAffiliate.setInsertAffiliateIdentifierChangeCallback((identifier) => {
402
456
  InsertAffiliate.setInsertAffiliateIdentifierChangeCallback(null);
403
457
  ```
404
458
 
459
+ **Callback Parameters:**
460
+ - `identifier` (string | null): The full affiliate identifier (shortCode-userId)
461
+ - `offerCode` (string | null): The offer code associated with this affiliate (if any)
462
+
405
463
  </details>
406
464
 
465
+ ### Prevent Affiliate Transfer
466
+
467
+ By default, clicking a new affiliate link will overwrite any existing attribution. Enable `preventAffiliateTransfer` to lock the first affiliate:
468
+
469
+ ```javascript
470
+ await InsertAffiliate.initialize(
471
+ "YOUR_COMPANY_CODE",
472
+ false, // verboseLogging
473
+ 604800, // 7-day attribution timeout
474
+ true // preventAffiliateTransfer - locks first affiliate
475
+ );
476
+ ```
477
+
478
+ **How it works:**
479
+ - When enabled, once a user is attributed to an affiliate, that attribution is locked
480
+ - New affiliate links will not overwrite the existing attribution
481
+ - The callback still fires with the existing affiliate data (not the new one)
482
+ - Useful for preventing "affiliate stealing" where users click competitor links
483
+
484
+ Learn more: [Prevent Affiliate Transfer Documentation](https://docs.insertaffiliate.com/prevent-affiliate-transfer)
485
+
407
486
  ---
408
487
 
409
488
  ## 📖 API Reference
@@ -412,7 +491,7 @@ InsertAffiliate.setInsertAffiliateIdentifierChangeCallback(null);
412
491
 
413
492
  | Method | Description | Returns |
414
493
  |--------|-------------|---------|
415
- | `initialize(companyCode, verbose?)` | Initialize the SDK | `Promise<void>` |
494
+ | `initialize(companyCode, verbose?, timeout?, preventTransfer?)` | Initialize the SDK | `Promise<void>` |
416
495
  | `returnInsertAffiliateIdentifier(ignoreTimeout?)` | Get current affiliate identifier | `Promise<string \| null>` |
417
496
  | `returnCompanyId()` | Get company ID | `Promise<string \| null>` |
418
497
  | `setInsertAffiliateIdentifier(link)` | Set affiliate from deep link | `Promise<string \| null>` |
@@ -420,6 +499,9 @@ InsertAffiliate.setInsertAffiliateIdentifierChangeCallback(null);
420
499
  | `getAffiliateDetails(code)` | Get affiliate info without storing | `Promise<AffiliateDetails \| null>` |
421
500
  | `trackEvent(eventName)` | Track custom event | `Promise<void>` |
422
501
  | `getOfferCode()` | Get offer code modifier | `Promise<string \| null>` |
502
+ | `getAffiliateExpiryTimestamp()` | Get Unix timestamp (ms) when attribution expires | `Promise<number \| null>` |
503
+ | `getAffiliateStoredDate()` | Get ISO date string when affiliate was stored | `Promise<string \| null>` |
504
+ | `isAffiliateAttributionValid()` | Check if attribution is still valid | `Promise<boolean>` |
423
505
  | `setInsertAffiliateIdentifierChangeCallback(fn)` | Set change callback | `void` |
424
506
 
425
507
  <details>
package/dist/index.d.ts CHANGED
@@ -6,16 +6,22 @@ interface AffiliateDetails {
6
6
  affiliateShortCode: string;
7
7
  deeplinkUrl: string;
8
8
  }
9
- type InsertAffiliateIdentifierChangeCallback = (identifier: string | null) => void;
9
+ type AffiliateLookupStatus = 'found' | 'not_found' | 'lookup_failed' | 'not_configured';
10
+ interface AffiliateLookupResult {
11
+ status: AffiliateLookupStatus;
12
+ details: AffiliateDetails | null;
13
+ }
14
+ type InsertAffiliateIdentifierChangeCallback = (identifier: string | null, offerCode: string | null) => void;
10
15
  declare class InsertAffiliate {
11
16
  private static isInitialized;
12
17
  private static companyCode;
13
18
  private static verboseLogging;
14
19
  private static insertAffiliateIdentifierChangeCallback;
15
20
  private static affiliateAttributionActiveTime;
21
+ private static preventAffiliateTransfer;
16
22
  private static offerCode;
17
23
  private static verboseLog;
18
- static initialize(code: string | null, verboseLogging?: boolean, affiliateAttributionActiveTime?: number): Promise<void>;
24
+ static initialize(code: string | null, verboseLogging?: boolean, affiliateAttributionActiveTime?: number, preventAffiliateTransfer?: boolean): Promise<void>;
19
25
  private static checkForInsertAffiliateParam;
20
26
  static returnInsertAffiliateIdentifier(ignoreTimeout?: boolean): Promise<string | null>;
21
27
  static setInsertAffiliateIdentifier(referringLink: string): Promise<string | null>;
@@ -23,19 +29,43 @@ declare class InsertAffiliate {
23
29
  * Validates and sets a short code for affiliate tracking
24
30
  * Validates the short code against the API before storing
25
31
  * @param shortCode The short code to validate and set
32
+ * @param options.onLookupFailed called when the lookup itself couldn't be completed
33
+ * (not just an invalid code) — use it to offer a retry instead of proceeding unattributed.
26
34
  * @returns true if the code exists and was successfully validated and stored, false otherwise
27
35
  */
28
- static setShortCode(shortCode: string): Promise<boolean>;
36
+ static setShortCode(shortCode: string, options?: {
37
+ onLookupFailed?: () => void;
38
+ }): Promise<boolean>;
29
39
  static setInsertAffiliateIdentifierChangeCallback(callback: InsertAffiliateIdentifierChangeCallback | null): void;
30
40
  static isAffiliateAttributionValid(): Promise<boolean>;
31
41
  static getAffiliateStoredDate(): Promise<string | null>;
32
42
  /**
33
- * Retrieve detailed information about an affiliate by their short code or deep link
34
- * This method queries the API and does not store or set the affiliate identifier
43
+ * Get the Unix timestamp (in milliseconds) when the affiliate attribution will expire
44
+ * @returns The expiry timestamp in milliseconds, or null if no attribution or no timeout configured
45
+ */
46
+ static getAffiliateExpiryTimestamp(): Promise<number | null>;
47
+ /**
48
+ * Retrieve detailed information about an affiliate by their short code or deep link,
49
+ * distinguishing "no affiliate matches this code" from "couldn't check" (backend
50
+ * outage, timeout, rate limit). This method queries the API and does not store or set
51
+ * the affiliate identifier.
52
+ * @param affiliateCode The short code or deep link to look up
53
+ * @returns an AffiliateLookupResult with a status of 'found', 'not_found', 'lookup_failed', or 'not_configured'
54
+ */
55
+ static getAffiliateLookupResult(affiliateCode: string, options?: {
56
+ trackUsage?: boolean;
57
+ }): Promise<AffiliateLookupResult>;
58
+ /**
59
+ * Retrieve detailed information about an affiliate by their short code or deep link.
60
+ * Kept for backward compatibility: collapses 'not_found' and 'lookup_failed' into the
61
+ * same null result, exactly as before. Use getAffiliateLookupResult if you need to tell
62
+ * an invalid code apart from a backend outage.
35
63
  * @param affiliateCode The short code or deep link to look up
36
64
  * @returns AffiliateDetails if found, null otherwise
37
65
  */
38
- static getAffiliateDetails(affiliateCode: string): Promise<AffiliateDetails | null>;
66
+ static getAffiliateDetails(affiliateCode: string, options?: {
67
+ trackUsage?: boolean;
68
+ }): Promise<AffiliateDetails | null>;
39
69
  static returnCompanyId(): Promise<string | null>;
40
70
  /**
41
71
  * Get the offer code for the current affiliate
@@ -68,4 +98,4 @@ declare class InsertAffiliate {
68
98
  private static fetchShortLink;
69
99
  }
70
100
 
71
- export { type AffiliateDetails, InsertAffiliate, type InsertAffiliateIdentifierChangeCallback };
101
+ export { type AffiliateDetails, type AffiliateLookupResult, type AffiliateLookupStatus, InsertAffiliate, type InsertAffiliateIdentifierChangeCallback };
package/dist/index.js CHANGED
@@ -80,12 +80,14 @@ var InsertAffiliate = class {
80
80
  console.log(`[Insert Affiliate] [VERBOSE] ${message}`);
81
81
  }
82
82
  }
83
- static async initialize(code, verboseLogging = false, affiliateAttributionActiveTime) {
83
+ static async initialize(code, verboseLogging = false, affiliateAttributionActiveTime, preventAffiliateTransfer = false) {
84
84
  this.verboseLogging = verboseLogging;
85
+ this.preventAffiliateTransfer = preventAffiliateTransfer;
85
86
  if (verboseLogging) {
86
87
  this.verboseLog("Starting SDK initialization...");
87
88
  this.verboseLog(`Company code provided: ${code ? "Yes" : "No"}`);
88
89
  this.verboseLog("Verbose logging enabled");
90
+ this.verboseLog(`Prevent affiliate transfer: ${preventAffiliateTransfer}`);
89
91
  }
90
92
  if (this.isInitialized) {
91
93
  this.verboseLog("SDK already initialized, skipping");
@@ -165,6 +167,11 @@ var InsertAffiliate = class {
165
167
  this.verboseLog(`Returning existing identifier: ${identifier2}`);
166
168
  return identifier2;
167
169
  }
170
+ if (this.preventAffiliateTransfer && existingShortCode && existingShortCode !== shortCode) {
171
+ this.verboseLog(`Transfer blocked: existing affiliate "${existingShortCode}" protected from being replaced by "${shortCode}"`);
172
+ const identifier2 = `${existingShortCode}-${userId}`;
173
+ return identifier2;
174
+ }
168
175
  this.verboseLog(`Saving short code to storage: ${shortCode}`);
169
176
  await saveValue("referrerLink", shortCode);
170
177
  const storedDate = (/* @__PURE__ */ new Date()).toISOString();
@@ -172,10 +179,10 @@ var InsertAffiliate = class {
172
179
  this.verboseLog(`Short code saved successfully with stored date: ${storedDate}`);
173
180
  const identifier = `${shortCode}-${userId}`;
174
181
  this.verboseLog(`Returning identifier: ${identifier}`);
175
- await this.fetchAndStoreOfferCode(shortCode);
182
+ const offerCode = await this.fetchAndStoreOfferCode(shortCode);
176
183
  if (this.insertAffiliateIdentifierChangeCallback) {
177
- this.verboseLog(`Triggering callback with identifier: ${identifier}`);
178
- this.insertAffiliateIdentifierChangeCallback(identifier);
184
+ this.verboseLog(`Triggering callback with identifier: ${identifier}, offerCode: ${offerCode || "none"}`);
185
+ this.insertAffiliateIdentifierChangeCallback(identifier, offerCode);
179
186
  }
180
187
  return identifier;
181
188
  }
@@ -183,9 +190,12 @@ var InsertAffiliate = class {
183
190
  * Validates and sets a short code for affiliate tracking
184
191
  * Validates the short code against the API before storing
185
192
  * @param shortCode The short code to validate and set
193
+ * @param options.onLookupFailed called when the lookup itself couldn't be completed
194
+ * (not just an invalid code) — use it to offer a retry instead of proceeding unattributed.
186
195
  * @returns true if the code exists and was successfully validated and stored, false otherwise
187
196
  */
188
- static async setShortCode(shortCode) {
197
+ static async setShortCode(shortCode, options) {
198
+ var _a;
189
199
  this.verboseLog(`Setting short code. Input: ${shortCode}`);
190
200
  const valid = /^[a-zA-Z0-9]{3,25}$/.test(shortCode);
191
201
  this.verboseLog(`Short code validation: ${valid ? "Valid" : "Invalid"}`);
@@ -193,12 +203,16 @@ var InsertAffiliate = class {
193
203
  this.verboseLog("Invalid short code format, aborting");
194
204
  return false;
195
205
  }
196
- const affiliateDetails = await this.getAffiliateDetails(shortCode);
197
- if (!affiliateDetails) {
198
- this.verboseLog(`Short code '${shortCode}' does not exist or validation failed`);
206
+ const lookup = await this.getAffiliateLookupResult(shortCode, { trackUsage: true });
207
+ if (lookup.status !== "found" || !lookup.details) {
208
+ this.verboseLog(`Short code '${shortCode}' does not exist or validation failed (status: ${lookup.status})`);
199
209
  console.error(`[Insert Affiliate] Error: Short code '${shortCode}' does not exist or validation failed.`);
210
+ if (lookup.status === "lookup_failed") {
211
+ (_a = options == null ? void 0 : options.onLookupFailed) == null ? void 0 : _a.call(options);
212
+ }
200
213
  return false;
201
214
  }
215
+ const affiliateDetails = lookup.details;
202
216
  this.verboseLog(`Short code validated successfully for affiliate: ${affiliateDetails.affiliateName}`);
203
217
  console.log(`[Insert Affiliate] Short code validated successfully for affiliate: ${affiliateDetails.affiliateName}`);
204
218
  this.verboseLog("Calling setInsertAffiliateIdentifier with short code");
@@ -239,17 +253,44 @@ var InsertAffiliate = class {
239
253
  return storedDate;
240
254
  }
241
255
  /**
242
- * Retrieve detailed information about an affiliate by their short code or deep link
243
- * This method queries the API and does not store or set the affiliate identifier
256
+ * Get the Unix timestamp (in milliseconds) when the affiliate attribution will expire
257
+ * @returns The expiry timestamp in milliseconds, or null if no attribution or no timeout configured
258
+ */
259
+ static async getAffiliateExpiryTimestamp() {
260
+ this.verboseLog("Getting affiliate expiry timestamp...");
261
+ const storedDateStr = await getValue("affiliateStoredDate");
262
+ if (!storedDateStr) {
263
+ this.verboseLog("No stored date found, returning null");
264
+ return null;
265
+ }
266
+ let timeoutMs = this.affiliateAttributionActiveTime;
267
+ if (timeoutMs === null) {
268
+ const storedTimeout = await getValue("affiliateAttributionActiveTime");
269
+ timeoutMs = storedTimeout ? parseInt(storedTimeout, 10) : null;
270
+ }
271
+ if (timeoutMs === null) {
272
+ this.verboseLog("No attribution timeout configured, returning null");
273
+ return null;
274
+ }
275
+ const storedDate = new Date(storedDateStr);
276
+ const expiryTimestamp = storedDate.getTime() + timeoutMs;
277
+ this.verboseLog(`Expiry timestamp: ${expiryTimestamp} (stored: ${storedDateStr}, timeout: ${timeoutMs}ms)`);
278
+ return expiryTimestamp;
279
+ }
280
+ /**
281
+ * Retrieve detailed information about an affiliate by their short code or deep link,
282
+ * distinguishing "no affiliate matches this code" from "couldn't check" (backend
283
+ * outage, timeout, rate limit). This method queries the API and does not store or set
284
+ * the affiliate identifier.
244
285
  * @param affiliateCode The short code or deep link to look up
245
- * @returns AffiliateDetails if found, null otherwise
286
+ * @returns an AffiliateLookupResult with a status of 'found', 'not_found', 'lookup_failed', or 'not_configured'
246
287
  */
247
- static async getAffiliateDetails(affiliateCode) {
288
+ static async getAffiliateLookupResult(affiliateCode, options) {
248
289
  this.verboseLog(`Getting affiliate details for: ${affiliateCode}`);
249
290
  const companyCode = this.companyCode || await getValue("companyCode");
250
291
  if (!companyCode) {
251
292
  this.verboseLog("Cannot get affiliate details: no company code available");
252
- return null;
293
+ return { status: "not_configured", details: null };
253
294
  }
254
295
  const cleanCode = affiliateCode.includes("-") ? affiliateCode.split("-")[0] : affiliateCode;
255
296
  this.verboseLog(`Clean code: ${cleanCode}`);
@@ -259,6 +300,9 @@ var InsertAffiliate = class {
259
300
  companyId: companyCode,
260
301
  affiliateCode: cleanCode
261
302
  };
303
+ if (options == null ? void 0 : options.trackUsage) {
304
+ payload.trackUsage = true;
305
+ }
262
306
  this.verboseLog(`Making API call to: ${url}`);
263
307
  this.verboseLog(`Payload: ${JSON.stringify(payload)}`);
264
308
  const response = await fetch(url, {
@@ -269,26 +313,42 @@ var InsertAffiliate = class {
269
313
  this.verboseLog(`API response status: ${response.status}`);
270
314
  if (!response.ok) {
271
315
  this.verboseLog(`Failed to get affiliate details, status: ${response.status}`);
272
- return null;
316
+ return { status: "lookup_failed", details: null };
273
317
  }
274
318
  const data = await response.json();
275
319
  this.verboseLog(`API response data: ${JSON.stringify(data)}`);
276
- if (data.exists && data.affiliate) {
320
+ if (data.exists) {
321
+ if (!data.affiliate) {
322
+ this.verboseLog("Affiliate exists but response is missing affiliate details");
323
+ return { status: "lookup_failed", details: null };
324
+ }
277
325
  const details = {
278
326
  affiliateName: data.affiliate.affiliateName,
279
327
  affiliateShortCode: data.affiliate.affiliateShortCode,
280
328
  deeplinkUrl: data.affiliate.deeplinkurl
281
329
  };
282
330
  this.verboseLog(`Successfully retrieved affiliate details for: ${details.affiliateName}`);
283
- return details;
331
+ return { status: "found", details };
284
332
  }
285
333
  this.verboseLog("Affiliate does not exist");
286
- return null;
334
+ return { status: "not_found", details: null };
287
335
  } catch (error) {
288
336
  this.verboseLog(`Error fetching affiliate details: ${error}`);
289
- return null;
337
+ return { status: "lookup_failed", details: null };
290
338
  }
291
339
  }
340
+ /**
341
+ * Retrieve detailed information about an affiliate by their short code or deep link.
342
+ * Kept for backward compatibility: collapses 'not_found' and 'lookup_failed' into the
343
+ * same null result, exactly as before. Use getAffiliateLookupResult if you need to tell
344
+ * an invalid code apart from a backend outage.
345
+ * @param affiliateCode The short code or deep link to look up
346
+ * @returns AffiliateDetails if found, null otherwise
347
+ */
348
+ static async getAffiliateDetails(affiliateCode, options) {
349
+ const result = await this.getAffiliateLookupResult(affiliateCode, options);
350
+ return result.details;
351
+ }
292
352
  static async returnCompanyId() {
293
353
  this.verboseLog("Getting company ID...");
294
354
  const companyCode = this.companyCode || await getValue("companyCode");
@@ -355,18 +415,13 @@ var InsertAffiliate = class {
355
415
  this.verboseLog(`Failed to fetch offer code, status: ${response.status}`);
356
416
  return null;
357
417
  }
358
- const offerCode = (await response.text()).replace(/[^a-zA-Z0-9]/g, "");
359
- this.verboseLog(`Received offer code: ${offerCode}`);
360
- const errorCodes = [
361
- "errorofferCodeNotFound",
362
- "errorAffiliateoffercodenotfoundinanycompany",
363
- "errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas",
364
- "Routenotfound"
365
- ];
366
- if (errorCodes.includes(offerCode)) {
418
+ const rawOfferCode = await response.text();
419
+ if (rawOfferCode.includes("errorofferCodeNotFound") || rawOfferCode.includes("errorAffiliateoffercodenotfoundinanycompany") || rawOfferCode.includes("errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas") || rawOfferCode.includes("Routenotfound")) {
367
420
  this.verboseLog("Offer code not found or invalid");
368
421
  return null;
369
422
  }
423
+ const offerCode = rawOfferCode.replace(/[^a-zA-Z0-9_-]/g, "");
424
+ this.verboseLog(`Received offer code: ${offerCode}`);
370
425
  const storageKey = platformType === "stripe" ? "offerCode" : `offerCode_${platformType}`;
371
426
  await saveValue(storageKey, offerCode);
372
427
  if (platformType === "stripe") {
@@ -385,7 +440,7 @@ var InsertAffiliate = class {
385
440
  const companyCode = this.companyCode || await getValue("companyCode");
386
441
  if (!companyCode) {
387
442
  this.verboseLog("Cannot fetch offer code: no company code available");
388
- return;
443
+ return null;
389
444
  }
390
445
  const encoded = encodeURIComponent(shortCode);
391
446
  const url = `https://api.insertaffiliate.com/v1/affiliateReturnOfferCode/${companyCode}/${encoded}`;
@@ -394,25 +449,22 @@ var InsertAffiliate = class {
394
449
  this.verboseLog(`API response status: ${response.status}`);
395
450
  if (!response.ok) {
396
451
  this.verboseLog(`Failed to fetch offer code, status: ${response.status}`);
397
- return;
452
+ return null;
398
453
  }
399
- const offerCode = (await response.text()).replace(/[^a-zA-Z0-9]/g, "");
400
- this.verboseLog(`Received offer code: ${offerCode}`);
401
- const errorCodes = [
402
- "errorofferCodeNotFound",
403
- "errorAffiliateoffercodenotfoundinanycompany",
404
- "errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas",
405
- "Routenotfound"
406
- ];
407
- if (errorCodes.includes(offerCode)) {
454
+ const rawOfferCode = await response.text();
455
+ if (rawOfferCode.includes("errorofferCodeNotFound") || rawOfferCode.includes("errorAffiliateoffercodenotfoundinanycompany") || rawOfferCode.includes("errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas") || rawOfferCode.includes("Routenotfound")) {
408
456
  this.verboseLog("Offer code not found or invalid");
409
- return;
457
+ return null;
410
458
  }
459
+ const offerCode = rawOfferCode.replace(/[^a-zA-Z0-9_-]/g, "");
460
+ this.verboseLog(`Received offer code: ${offerCode}`);
411
461
  this.offerCode = offerCode;
412
462
  await saveValue("offerCode", offerCode);
413
463
  this.verboseLog(`Offer code stored successfully: ${offerCode}`);
464
+ return offerCode;
414
465
  } catch (error) {
415
466
  this.verboseLog(`Error fetching offer code: ${error}`);
467
+ return null;
416
468
  }
417
469
  }
418
470
  static async trackEvent(eventName) {
@@ -574,18 +626,13 @@ var InsertAffiliate = class {
574
626
  this.verboseLog(`API URL: ${url}`);
575
627
  const res = await fetch(url);
576
628
  this.verboseLog(`API response status: ${res.status}`);
577
- const offerCode = (await res.text()).replace(/[^a-zA-Z0-9]/g, "");
578
- this.verboseLog(`Received offer code: ${offerCode}`);
579
- const errorCodes = [
580
- "errorofferCodeNotFound",
581
- "errorAffiliateoffercodenotfoundinanycompany",
582
- "errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas",
583
- "Routenotfound"
584
- ];
585
- if (errorCodes.includes(offerCode)) {
629
+ const rawOfferCode = await res.text();
630
+ if (rawOfferCode.includes("errorofferCodeNotFound") || rawOfferCode.includes("errorAffiliateoffercodenotfoundinanycompany") || rawOfferCode.includes("errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas") || rawOfferCode.includes("Routenotfound")) {
586
631
  this.verboseLog("Offer code not found or invalid");
587
632
  return;
588
633
  }
634
+ const offerCode = rawOfferCode.replace(/[^a-zA-Z0-9_-]/g, "");
635
+ this.verboseLog(`Received offer code: ${offerCode}`);
589
636
  const redeemUrl = `https://apps.apple.com/redeem?ctx=offercodes&id=${offerCodeUrlId}&code=${offerCode}`;
590
637
  this.verboseLog(`Opening redeem URL: ${redeemUrl}`);
591
638
  window.open(redeemUrl, "_blank");
@@ -636,6 +683,7 @@ InsertAffiliate.verboseLogging = false;
636
683
  InsertAffiliate.insertAffiliateIdentifierChangeCallback = null;
637
684
  InsertAffiliate.affiliateAttributionActiveTime = null;
638
685
  // in milliseconds
686
+ InsertAffiliate.preventAffiliateTransfer = false;
639
687
  InsertAffiliate.offerCode = null;
640
688
  // Annotate the CommonJS export names for ESM import in node:
641
689
  0 && (module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insert-affiliate-js-sdk",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,6 +1,6 @@
1
1
  // src/sdk/InsertAffiliate.ts
2
2
  import { getValue, saveValue } from '../utils/asyncStorage';
3
- import { generateUUID, generateShortDeviceID } from '../utils/helpers';
3
+ import { generateShortDeviceID, generateUUID } from '../utils/helpers';
4
4
 
5
5
  interface IapticAndroidReceipt {
6
6
  orderId: string;
@@ -25,7 +25,16 @@ export interface AffiliateDetails {
25
25
  deeplinkUrl: string;
26
26
  }
27
27
 
28
- export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null) => void;
28
+ // 'lookup_failed' (outage, timeout) may be worth retrying. 'not_configured' (no company
29
+ // code set) never will be — it's always a bug in the calling app, not the code or backend.
30
+ export type AffiliateLookupStatus = 'found' | 'not_found' | 'lookup_failed' | 'not_configured';
31
+
32
+ export interface AffiliateLookupResult {
33
+ status: AffiliateLookupStatus;
34
+ details: AffiliateDetails | null;
35
+ }
36
+
37
+ export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null, offerCode: string | null) => void;
29
38
 
30
39
  export class InsertAffiliate {
31
40
  private static isInitialized: boolean = false;
@@ -33,6 +42,7 @@ export class InsertAffiliate {
33
42
  private static verboseLogging: boolean = false;
34
43
  private static insertAffiliateIdentifierChangeCallback: InsertAffiliateIdentifierChangeCallback | null = null;
35
44
  private static affiliateAttributionActiveTime: number | null = null; // in milliseconds
45
+ private static preventAffiliateTransfer: boolean = false;
36
46
  private static offerCode: string | null = null;
37
47
 
38
48
  private static verboseLog(message: string): void {
@@ -41,13 +51,20 @@ export class InsertAffiliate {
41
51
  }
42
52
  }
43
53
 
44
- static async initialize(code: string | null, verboseLogging: boolean = false, affiliateAttributionActiveTime?: number): Promise<void> {
54
+ static async initialize(
55
+ code: string | null,
56
+ verboseLogging: boolean = false,
57
+ affiliateAttributionActiveTime?: number,
58
+ preventAffiliateTransfer: boolean = false
59
+ ): Promise<void> {
45
60
  this.verboseLogging = verboseLogging;
61
+ this.preventAffiliateTransfer = preventAffiliateTransfer;
46
62
 
47
63
  if (verboseLogging) {
48
64
  this.verboseLog('Starting SDK initialization...');
49
65
  this.verboseLog(`Company code provided: ${code ? 'Yes' : 'No'}`);
50
66
  this.verboseLog('Verbose logging enabled');
67
+ this.verboseLog(`Prevent affiliate transfer: ${preventAffiliateTransfer}`);
51
68
  }
52
69
 
53
70
  if (this.isInitialized) {
@@ -154,6 +171,13 @@ export class InsertAffiliate {
154
171
  return identifier;
155
172
  }
156
173
 
174
+ // Check if transfer is blocked
175
+ if (this.preventAffiliateTransfer && existingShortCode && existingShortCode !== shortCode) {
176
+ this.verboseLog(`Transfer blocked: existing affiliate "${existingShortCode}" protected from being replaced by "${shortCode}"`);
177
+ const identifier = `${existingShortCode}-${userId}`;
178
+ return identifier;
179
+ }
180
+
157
181
  this.verboseLog(`Saving short code to storage: ${shortCode}`);
158
182
  await saveValue('referrerLink', shortCode);
159
183
 
@@ -166,12 +190,12 @@ export class InsertAffiliate {
166
190
  this.verboseLog(`Returning identifier: ${identifier}`);
167
191
 
168
192
  // Auto-fetch and store offer code (use just the short code, not the full identifier)
169
- await this.fetchAndStoreOfferCode(shortCode);
193
+ const offerCode = await this.fetchAndStoreOfferCode(shortCode);
170
194
 
171
195
  // Trigger callback if one is registered
172
196
  if (this.insertAffiliateIdentifierChangeCallback) {
173
- this.verboseLog(`Triggering callback with identifier: ${identifier}`);
174
- this.insertAffiliateIdentifierChangeCallback(identifier);
197
+ this.verboseLog(`Triggering callback with identifier: ${identifier}, offerCode: ${offerCode || 'none'}`);
198
+ this.insertAffiliateIdentifierChangeCallback(identifier, offerCode);
175
199
  }
176
200
 
177
201
  return identifier;
@@ -181,9 +205,11 @@ export class InsertAffiliate {
181
205
  * Validates and sets a short code for affiliate tracking
182
206
  * Validates the short code against the API before storing
183
207
  * @param shortCode The short code to validate and set
208
+ * @param options.onLookupFailed called when the lookup itself couldn't be completed
209
+ * (not just an invalid code) — use it to offer a retry instead of proceeding unattributed.
184
210
  * @returns true if the code exists and was successfully validated and stored, false otherwise
185
211
  */
186
- static async setShortCode(shortCode: string): Promise<boolean> {
212
+ static async setShortCode(shortCode: string, options?: { onLookupFailed?: () => void }): Promise<boolean> {
187
213
  this.verboseLog(`Setting short code. Input: ${shortCode}`);
188
214
 
189
215
  const valid = /^[a-zA-Z0-9]{3,25}$/.test(shortCode);
@@ -195,12 +221,16 @@ export class InsertAffiliate {
195
221
  }
196
222
 
197
223
  // Validate that the short code exists in the system
198
- const affiliateDetails = await this.getAffiliateDetails(shortCode);
199
- if (!affiliateDetails) {
200
- this.verboseLog(`Short code '${shortCode}' does not exist or validation failed`);
224
+ const lookup = await this.getAffiliateLookupResult(shortCode, { trackUsage: true });
225
+ if (lookup.status !== 'found' || !lookup.details) {
226
+ this.verboseLog(`Short code '${shortCode}' does not exist or validation failed (status: ${lookup.status})`);
201
227
  console.error(`[Insert Affiliate] Error: Short code '${shortCode}' does not exist or validation failed.`);
228
+ if (lookup.status === 'lookup_failed') {
229
+ options?.onLookupFailed?.();
230
+ }
202
231
  return false;
203
232
  }
233
+ const affiliateDetails = lookup.details;
204
234
 
205
235
  this.verboseLog(`Short code validated successfully for affiliate: ${affiliateDetails.affiliateName}`);
206
236
  console.log(`[Insert Affiliate] Short code validated successfully for affiliate: ${affiliateDetails.affiliateName}`);
@@ -256,18 +286,53 @@ export class InsertAffiliate {
256
286
  }
257
287
 
258
288
  /**
259
- * Retrieve detailed information about an affiliate by their short code or deep link
260
- * This method queries the API and does not store or set the affiliate identifier
289
+ * Get the Unix timestamp (in milliseconds) when the affiliate attribution will expire
290
+ * @returns The expiry timestamp in milliseconds, or null if no attribution or no timeout configured
291
+ */
292
+ static async getAffiliateExpiryTimestamp(): Promise<number | null> {
293
+ this.verboseLog('Getting affiliate expiry timestamp...');
294
+
295
+ const storedDateStr = await getValue('affiliateStoredDate');
296
+ if (!storedDateStr) {
297
+ this.verboseLog('No stored date found, returning null');
298
+ return null;
299
+ }
300
+
301
+ // Get timeout value from storage or class property
302
+ let timeoutMs = this.affiliateAttributionActiveTime;
303
+ if (timeoutMs === null) {
304
+ const storedTimeout = await getValue('affiliateAttributionActiveTime');
305
+ timeoutMs = storedTimeout ? parseInt(storedTimeout, 10) : null;
306
+ }
307
+
308
+ // If no timeout is set, return null (attribution never expires)
309
+ if (timeoutMs === null) {
310
+ this.verboseLog('No attribution timeout configured, returning null');
311
+ return null;
312
+ }
313
+
314
+ const storedDate = new Date(storedDateStr);
315
+ const expiryTimestamp = storedDate.getTime() + timeoutMs;
316
+ this.verboseLog(`Expiry timestamp: ${expiryTimestamp} (stored: ${storedDateStr}, timeout: ${timeoutMs}ms)`);
317
+
318
+ return expiryTimestamp;
319
+ }
320
+
321
+ /**
322
+ * Retrieve detailed information about an affiliate by their short code or deep link,
323
+ * distinguishing "no affiliate matches this code" from "couldn't check" (backend
324
+ * outage, timeout, rate limit). This method queries the API and does not store or set
325
+ * the affiliate identifier.
261
326
  * @param affiliateCode The short code or deep link to look up
262
- * @returns AffiliateDetails if found, null otherwise
327
+ * @returns an AffiliateLookupResult with a status of 'found', 'not_found', 'lookup_failed', or 'not_configured'
263
328
  */
264
- static async getAffiliateDetails(affiliateCode: string): Promise<AffiliateDetails | null> {
329
+ static async getAffiliateLookupResult(affiliateCode: string, options?: { trackUsage?: boolean }): Promise<AffiliateLookupResult> {
265
330
  this.verboseLog(`Getting affiliate details for: ${affiliateCode}`);
266
331
 
267
332
  const companyCode = this.companyCode || await getValue('companyCode');
268
333
  if (!companyCode) {
269
334
  this.verboseLog('Cannot get affiliate details: no company code available');
270
- return null;
335
+ return { status: 'not_configured', details: null };
271
336
  }
272
337
 
273
338
  // Strip UUID from code if present (e.g., "ABC123-uuid" becomes "ABC123")
@@ -276,11 +341,15 @@ export class InsertAffiliate {
276
341
 
277
342
  try {
278
343
  const url = 'https://api.insertaffiliate.com/V1/checkAffiliateExists';
279
- const payload = {
344
+ const payload: Record<string, any> = {
280
345
  companyId: companyCode,
281
346
  affiliateCode: cleanCode,
282
347
  };
283
348
 
349
+ if (options?.trackUsage) {
350
+ payload.trackUsage = true;
351
+ }
352
+
284
353
  this.verboseLog(`Making API call to: ${url}`);
285
354
  this.verboseLog(`Payload: ${JSON.stringify(payload)}`);
286
355
 
@@ -294,13 +363,19 @@ export class InsertAffiliate {
294
363
 
295
364
  if (!response.ok) {
296
365
  this.verboseLog(`Failed to get affiliate details, status: ${response.status}`);
297
- return null;
366
+ return { status: 'lookup_failed', details: null };
298
367
  }
299
368
 
300
369
  const data = await response.json();
301
370
  this.verboseLog(`API response data: ${JSON.stringify(data)}`);
302
371
 
303
- if (data.exists && data.affiliate) {
372
+ if (data.exists) {
373
+ if (!data.affiliate) {
374
+ // Malformed response, not a real not-found.
375
+ this.verboseLog('Affiliate exists but response is missing affiliate details');
376
+ return { status: 'lookup_failed', details: null };
377
+ }
378
+
304
379
  const details: AffiliateDetails = {
305
380
  affiliateName: data.affiliate.affiliateName,
306
381
  affiliateShortCode: data.affiliate.affiliateShortCode,
@@ -308,17 +383,30 @@ export class InsertAffiliate {
308
383
  };
309
384
 
310
385
  this.verboseLog(`Successfully retrieved affiliate details for: ${details.affiliateName}`);
311
- return details;
386
+ return { status: 'found', details };
312
387
  }
313
388
 
314
389
  this.verboseLog('Affiliate does not exist');
315
- return null;
390
+ return { status: 'not_found', details: null };
316
391
  } catch (error) {
317
392
  this.verboseLog(`Error fetching affiliate details: ${error}`);
318
- return null;
393
+ return { status: 'lookup_failed', details: null };
319
394
  }
320
395
  }
321
396
 
397
+ /**
398
+ * Retrieve detailed information about an affiliate by their short code or deep link.
399
+ * Kept for backward compatibility: collapses 'not_found' and 'lookup_failed' into the
400
+ * same null result, exactly as before. Use getAffiliateLookupResult if you need to tell
401
+ * an invalid code apart from a backend outage.
402
+ * @param affiliateCode The short code or deep link to look up
403
+ * @returns AffiliateDetails if found, null otherwise
404
+ */
405
+ static async getAffiliateDetails(affiliateCode: string, options?: { trackUsage?: boolean }): Promise<AffiliateDetails | null> {
406
+ const result = await this.getAffiliateLookupResult(affiliateCode, options);
407
+ return result.details;
408
+ }
409
+
322
410
  static async returnCompanyId(): Promise<string | null> {
323
411
  this.verboseLog('Getting company ID...');
324
412
  const companyCode = this.companyCode || await getValue('companyCode');
@@ -401,21 +489,23 @@ export class InsertAffiliate {
401
489
  return null;
402
490
  }
403
491
 
404
- const offerCode = (await response.text()).replace(/[^a-zA-Z0-9]/g, '');
405
- this.verboseLog(`Received offer code: ${offerCode}`);
406
-
407
- const errorCodes = [
408
- 'errorofferCodeNotFound',
409
- 'errorAffiliateoffercodenotfoundinanycompany',
410
- 'errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas',
411
- 'Routenotfound'
412
- ];
492
+ const rawOfferCode = await response.text();
413
493
 
414
- if (errorCodes.includes(offerCode)) {
494
+ // Check for specific error strings from API before cleaning
495
+ if (
496
+ rawOfferCode.includes('errorofferCodeNotFound') ||
497
+ rawOfferCode.includes('errorAffiliateoffercodenotfoundinanycompany') ||
498
+ rawOfferCode.includes('errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas') ||
499
+ rawOfferCode.includes('Routenotfound')
500
+ ) {
415
501
  this.verboseLog('Offer code not found or invalid');
416
502
  return null;
417
503
  }
418
504
 
505
+ // Remove special characters, keep only alphanumeric, underscores, and dashes
506
+ const offerCode = rawOfferCode.replace(/[^a-zA-Z0-9_-]/g, '');
507
+ this.verboseLog(`Received offer code: ${offerCode}`);
508
+
419
509
  // Store offer code with platform-specific key
420
510
  const storageKey = platformType === 'stripe' ? 'offerCode' : `offerCode_${platformType}`;
421
511
  await saveValue(storageKey, offerCode);
@@ -431,7 +521,7 @@ export class InsertAffiliate {
431
521
  }
432
522
  }
433
523
 
434
- private static async fetchAndStoreOfferCode(shortCode: string): Promise<void> {
524
+ private static async fetchAndStoreOfferCode(shortCode: string): Promise<string | null> {
435
525
  this.verboseLog(`Fetching offer code for short code: ${shortCode}`);
436
526
 
437
527
  try {
@@ -439,7 +529,7 @@ export class InsertAffiliate {
439
529
  const companyCode = this.companyCode || await getValue('companyCode');
440
530
  if (!companyCode) {
441
531
  this.verboseLog('Cannot fetch offer code: no company code available');
442
- return;
532
+ return null;
443
533
  }
444
534
 
445
535
  // Use the more efficient endpoint with company code and just the short code
@@ -452,31 +542,34 @@ export class InsertAffiliate {
452
542
 
453
543
  if (!response.ok) {
454
544
  this.verboseLog(`Failed to fetch offer code, status: ${response.status}`);
455
- return;
545
+ return null;
456
546
  }
457
547
 
458
- const offerCode = (await response.text()).replace(/[^a-zA-Z0-9]/g, '');
459
- this.verboseLog(`Received offer code: ${offerCode}`);
460
-
461
- // Check for error codes
462
- const errorCodes = [
463
- 'errorofferCodeNotFound',
464
- 'errorAffiliateoffercodenotfoundinanycompany',
465
- 'errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas',
466
- 'Routenotfound'
467
- ];
548
+ const rawOfferCode = await response.text();
468
549
 
469
- if (errorCodes.includes(offerCode)) {
550
+ // Check for specific error strings from API before cleaning
551
+ if (
552
+ rawOfferCode.includes('errorofferCodeNotFound') ||
553
+ rawOfferCode.includes('errorAffiliateoffercodenotfoundinanycompany') ||
554
+ rawOfferCode.includes('errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas') ||
555
+ rawOfferCode.includes('Routenotfound')
556
+ ) {
470
557
  this.verboseLog('Offer code not found or invalid');
471
- return;
558
+ return null;
472
559
  }
473
560
 
561
+ // Remove special characters, keep only alphanumeric, underscores, and dashes
562
+ const offerCode = rawOfferCode.replace(/[^a-zA-Z0-9_-]/g, '');
563
+ this.verboseLog(`Received offer code: ${offerCode}`);
564
+
474
565
  // Store offer code
475
566
  this.offerCode = offerCode;
476
567
  await saveValue('offerCode', offerCode);
477
568
  this.verboseLog(`Offer code stored successfully: ${offerCode}`);
569
+ return offerCode;
478
570
  } catch (error) {
479
571
  this.verboseLog(`Error fetching offer code: ${error}`);
572
+ return null;
480
573
  }
481
574
  }
482
575
 
@@ -685,21 +778,23 @@ export class InsertAffiliate {
685
778
  const res = await fetch(url);
686
779
  this.verboseLog(`API response status: ${res.status}`);
687
780
 
688
- const offerCode = (await res.text()).replace(/[^a-zA-Z0-9]/g, '');
689
- this.verboseLog(`Received offer code: ${offerCode}`);
690
-
691
- const errorCodes = [
692
- 'errorofferCodeNotFound',
693
- 'errorAffiliateoffercodenotfoundinanycompany',
694
- 'errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas',
695
- 'Routenotfound'
696
- ];
781
+ const rawOfferCode = await res.text();
697
782
 
698
- if (errorCodes.includes(offerCode)) {
783
+ // Check for specific error strings from API before cleaning
784
+ if (
785
+ rawOfferCode.includes('errorofferCodeNotFound') ||
786
+ rawOfferCode.includes('errorAffiliateoffercodenotfoundinanycompany') ||
787
+ rawOfferCode.includes('errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas') ||
788
+ rawOfferCode.includes('Routenotfound')
789
+ ) {
699
790
  this.verboseLog('Offer code not found or invalid');
700
791
  return;
701
792
  }
702
793
 
794
+ // Remove special characters, keep only alphanumeric, underscores, and dashes
795
+ const offerCode = rawOfferCode.replace(/[^a-zA-Z0-9_-]/g, '');
796
+ this.verboseLog(`Received offer code: ${offerCode}`);
797
+
703
798
  const redeemUrl = `https://apps.apple.com/redeem?ctx=offercodes&id=${offerCodeUrlId}&code=${offerCode}`;
704
799
  this.verboseLog(`Opening redeem URL: ${redeemUrl}`);
705
800
  window.open(redeemUrl, '_blank');
@@ -1,10 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "WebFetch(domain:github.com)",
5
- "WebFetch(domain:www.revenuecat.com)"
6
- ],
7
- "deny": [],
8
- "ask": []
9
- }
10
- }