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 +7 -0
- package/README.md +92 -10
- package/dist/index.d.ts +37 -7
- package/dist/index.js +96 -48
- package/package.json +1 -1
- package/src/sdk/InsertAffiliate.ts +151 -56
- package/.claude/settings.local.json +0 -10
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
|
-
//
|
|
93
|
-
await InsertAffiliate.initialize(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
await Purchases.
|
|
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
|
|
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
|
|
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
|
-
*
|
|
34
|
-
*
|
|
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
|
|
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
|
|
197
|
-
if (!
|
|
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
|
-
*
|
|
243
|
-
*
|
|
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
|
|
286
|
+
* @returns an AffiliateLookupResult with a status of 'found', 'not_found', 'lookup_failed', or 'not_configured'
|
|
246
287
|
*/
|
|
247
|
-
static async
|
|
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
|
|
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
|
|
359
|
-
|
|
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
|
|
400
|
-
|
|
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
|
|
578
|
-
|
|
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
|
// src/sdk/InsertAffiliate.ts
|
|
2
2
|
import { getValue, saveValue } from '../utils/asyncStorage';
|
|
3
|
-
import {
|
|
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
|
-
|
|
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(
|
|
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
|
|
199
|
-
if (!
|
|
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
|
-
*
|
|
260
|
-
*
|
|
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
|
|
327
|
+
* @returns an AffiliateLookupResult with a status of 'found', 'not_found', 'lookup_failed', or 'not_configured'
|
|
263
328
|
*/
|
|
264
|
-
static async
|
|
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
|
|
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
|
|
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
|
-
|
|
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<
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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');
|