insert-affiliate-js-sdk 1.0.2 → 1.2.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/.claude/settings.local.json +10 -0
- package/CHANGELOG.md +60 -0
- package/README.md +414 -124
- package/dist/index.d.ts +53 -4
- package/dist/index.js +416 -37
- package/docs/deep-linking-web.md +199 -0
- package/docs/stripe-integration.md +237 -0
- package/package.json +1 -1
- package/src/sdk/InsertAffiliate.ts +543 -37
|
@@ -19,98 +19,547 @@ interface ExpectedTransactionPayload {
|
|
|
19
19
|
storedDate: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface AffiliateDetails {
|
|
23
|
+
affiliateName: string;
|
|
24
|
+
affiliateShortCode: string;
|
|
25
|
+
deeplinkUrl: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null) => void;
|
|
29
|
+
|
|
22
30
|
export class InsertAffiliate {
|
|
23
31
|
private static isInitialized: boolean = false;
|
|
24
32
|
private static companyCode: string | null = null;
|
|
33
|
+
private static verboseLogging: boolean = false;
|
|
34
|
+
private static insertAffiliateIdentifierChangeCallback: InsertAffiliateIdentifierChangeCallback | null = null;
|
|
35
|
+
private static affiliateAttributionActiveTime: number | null = null; // in milliseconds
|
|
36
|
+
private static offerCode: string | null = null;
|
|
37
|
+
|
|
38
|
+
private static verboseLog(message: string): void {
|
|
39
|
+
if (this.verboseLogging) {
|
|
40
|
+
console.log(`[Insert Affiliate] [VERBOSE] ${message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static async initialize(code: string | null, verboseLogging: boolean = false, affiliateAttributionActiveTime?: number): Promise<void> {
|
|
45
|
+
this.verboseLogging = verboseLogging;
|
|
46
|
+
|
|
47
|
+
if (verboseLogging) {
|
|
48
|
+
this.verboseLog('Starting SDK initialization...');
|
|
49
|
+
this.verboseLog(`Company code provided: ${code ? 'Yes' : 'No'}`);
|
|
50
|
+
this.verboseLog('Verbose logging enabled');
|
|
51
|
+
}
|
|
25
52
|
|
|
26
|
-
static async initialize(code: string | null): Promise<void> {
|
|
27
53
|
if (this.isInitialized) {
|
|
28
|
-
|
|
54
|
+
this.verboseLog('SDK already initialized, skipping');
|
|
29
55
|
return;
|
|
30
56
|
}
|
|
31
57
|
this.companyCode = code;
|
|
32
58
|
await saveValue('companyCode', code || '');
|
|
59
|
+
|
|
60
|
+
// Set attribution timeout if provided
|
|
61
|
+
if (affiliateAttributionActiveTime !== undefined) {
|
|
62
|
+
this.affiliateAttributionActiveTime = affiliateAttributionActiveTime;
|
|
63
|
+
await saveValue('affiliateAttributionActiveTime', affiliateAttributionActiveTime.toString());
|
|
64
|
+
this.verboseLog(`Attribution timeout set to: ${affiliateAttributionActiveTime}ms`);
|
|
65
|
+
}
|
|
66
|
+
|
|
33
67
|
this.isInitialized = true;
|
|
34
|
-
|
|
68
|
+
this.verboseLog(`SDK initialized ${code ? `with company code: ${code}` : 'without a company code.'}`);
|
|
69
|
+
this.verboseLog('Company code saved to storage');
|
|
70
|
+
this.verboseLog('SDK marked as initialized');
|
|
71
|
+
|
|
72
|
+
// Check for insertAffiliate URL parameter
|
|
73
|
+
await this.checkForInsertAffiliateParam();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private static async checkForInsertAffiliateParam(): Promise<void> {
|
|
77
|
+
this.verboseLog('Checking for insertAffiliate URL parameter...');
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Only run in browser environments
|
|
81
|
+
if (typeof window === 'undefined' || !window.location) {
|
|
82
|
+
this.verboseLog('Not in browser environment, skipping URL parameter check');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.verboseLog(`Current URL: ${window.location.href}`);
|
|
87
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
88
|
+
const insertAffiliateParam = urlParams.get('insertAffiliate');
|
|
89
|
+
|
|
90
|
+
if (insertAffiliateParam) {
|
|
91
|
+
this.verboseLog(`Found insertAffiliate URL parameter: ${insertAffiliateParam}`);
|
|
92
|
+
await this.setShortCode(insertAffiliateParam);
|
|
93
|
+
this.verboseLog('Successfully processed insertAffiliate URL parameter');
|
|
94
|
+
} else {
|
|
95
|
+
this.verboseLog('No insertAffiliate URL parameter found');
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
this.verboseLog(`Error checking for insertAffiliate URL parameter: ${error}`);
|
|
99
|
+
}
|
|
35
100
|
}
|
|
36
101
|
|
|
37
|
-
static async returnInsertAffiliateIdentifier(): Promise<string | null> {
|
|
102
|
+
static async returnInsertAffiliateIdentifier(ignoreTimeout: boolean = false): Promise<string | null> {
|
|
103
|
+
this.verboseLog('Getting insert affiliate identifier...');
|
|
38
104
|
const userId = await this.getOrCreateUserID();
|
|
39
105
|
const referrerLink = await getValue('referrerLink');
|
|
40
|
-
|
|
41
|
-
|
|
106
|
+
this.verboseLog(`User ID: ${userId || 'empty'}, Referrer link: ${referrerLink || 'empty'}`);
|
|
107
|
+
|
|
108
|
+
if (!referrerLink) {
|
|
109
|
+
this.verboseLog('No referrer link found, returning null');
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check attribution timeout unless explicitly ignored
|
|
114
|
+
if (!ignoreTimeout) {
|
|
115
|
+
const isValid = await this.isAffiliateAttributionValid();
|
|
116
|
+
if (!isValid) {
|
|
117
|
+
this.verboseLog('Attribution has expired, returning null');
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
this.verboseLog('Ignoring attribution timeout');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const identifier = `${referrerLink}-${userId}`;
|
|
125
|
+
this.verboseLog(`Returning affiliate identifier: ${identifier}`);
|
|
126
|
+
return identifier;
|
|
42
127
|
}
|
|
43
128
|
|
|
44
129
|
static async setInsertAffiliateIdentifier(referringLink: string): Promise<string | null> {
|
|
130
|
+
this.verboseLog(`Setting affiliate identifier. Input referringLink: ${referringLink}`);
|
|
131
|
+
|
|
45
132
|
const userId = await this.getOrCreateUserID();
|
|
46
|
-
|
|
133
|
+
this.verboseLog(`User ID: ${userId}`);
|
|
134
|
+
|
|
135
|
+
// Check if it's already a short code
|
|
136
|
+
const isShortCode = /^[a-zA-Z0-9]{3,25}$/.test(referringLink);
|
|
137
|
+
this.verboseLog(`Is short code: ${isShortCode}`);
|
|
138
|
+
|
|
139
|
+
const shortCode = isShortCode
|
|
47
140
|
? referringLink
|
|
48
141
|
: await this.fetchShortLink(referringLink);
|
|
49
142
|
|
|
50
|
-
if (!shortCode)
|
|
143
|
+
if (!shortCode) {
|
|
144
|
+
this.verboseLog('No short code found or generated, returning null');
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check if the same short code is already stored
|
|
149
|
+
const existingShortCode = await getValue('referrerLink');
|
|
150
|
+
if (existingShortCode === shortCode) {
|
|
151
|
+
this.verboseLog(`Short code ${shortCode} is already set, not updating attribution date`);
|
|
152
|
+
const identifier = `${shortCode}-${userId}`;
|
|
153
|
+
this.verboseLog(`Returning existing identifier: ${identifier}`);
|
|
154
|
+
return identifier;
|
|
155
|
+
}
|
|
51
156
|
|
|
157
|
+
this.verboseLog(`Saving short code to storage: ${shortCode}`);
|
|
52
158
|
await saveValue('referrerLink', shortCode);
|
|
53
|
-
|
|
159
|
+
|
|
160
|
+
// Store the attribution date
|
|
161
|
+
const storedDate = new Date().toISOString();
|
|
162
|
+
await saveValue('affiliateStoredDate', storedDate);
|
|
163
|
+
this.verboseLog(`Short code saved successfully with stored date: ${storedDate}`);
|
|
164
|
+
|
|
165
|
+
const identifier = `${shortCode}-${userId}`;
|
|
166
|
+
this.verboseLog(`Returning identifier: ${identifier}`);
|
|
167
|
+
|
|
168
|
+
// Auto-fetch and store offer code (use just the short code, not the full identifier)
|
|
169
|
+
await this.fetchAndStoreOfferCode(shortCode);
|
|
170
|
+
|
|
171
|
+
// Trigger callback if one is registered
|
|
172
|
+
if (this.insertAffiliateIdentifierChangeCallback) {
|
|
173
|
+
this.verboseLog(`Triggering callback with identifier: ${identifier}`);
|
|
174
|
+
this.insertAffiliateIdentifierChangeCallback(identifier);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return identifier;
|
|
54
178
|
}
|
|
55
179
|
|
|
56
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Validates and sets a short code for affiliate tracking
|
|
182
|
+
* Validates the short code against the API before storing
|
|
183
|
+
* @param shortCode The short code to validate and set
|
|
184
|
+
* @returns true if the code exists and was successfully validated and stored, false otherwise
|
|
185
|
+
*/
|
|
186
|
+
static async setShortCode(shortCode: string): Promise<boolean> {
|
|
187
|
+
this.verboseLog(`Setting short code. Input: ${shortCode}`);
|
|
188
|
+
|
|
57
189
|
const valid = /^[a-zA-Z0-9]{3,25}$/.test(shortCode);
|
|
190
|
+
this.verboseLog(`Short code validation: ${valid ? 'Valid' : 'Invalid'}`);
|
|
191
|
+
|
|
58
192
|
if (!valid) {
|
|
59
|
-
|
|
60
|
-
return;
|
|
193
|
+
this.verboseLog('Invalid short code format, aborting');
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 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`);
|
|
201
|
+
console.error(`[Insert Affiliate] Error: Short code '${shortCode}' does not exist or validation failed.`);
|
|
202
|
+
return false;
|
|
61
203
|
}
|
|
204
|
+
|
|
205
|
+
this.verboseLog(`Short code validated successfully for affiliate: ${affiliateDetails.affiliateName}`);
|
|
206
|
+
console.log(`[Insert Affiliate] Short code validated successfully for affiliate: ${affiliateDetails.affiliateName}`);
|
|
207
|
+
|
|
208
|
+
// If validation passes, set the Insert Affiliate Identifier
|
|
209
|
+
this.verboseLog('Calling setInsertAffiliateIdentifier with short code');
|
|
62
210
|
await this.setInsertAffiliateIdentifier(shortCode);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
static setInsertAffiliateIdentifierChangeCallback(callback: InsertAffiliateIdentifierChangeCallback | null): void {
|
|
215
|
+
this.verboseLog(`Setting affiliate identifier change callback: ${callback ? 'callback provided' : 'callback cleared'}`);
|
|
216
|
+
this.insertAffiliateIdentifierChangeCallback = callback;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
static async isAffiliateAttributionValid(): Promise<boolean> {
|
|
220
|
+
this.verboseLog('Checking if affiliate attribution is valid...');
|
|
221
|
+
|
|
222
|
+
const storedDateStr = await getValue('affiliateStoredDate');
|
|
223
|
+
if (!storedDateStr) {
|
|
224
|
+
this.verboseLog('No stored date found, attribution invalid');
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Get timeout value from storage or class property
|
|
229
|
+
let timeoutMs = this.affiliateAttributionActiveTime;
|
|
230
|
+
if (timeoutMs === null) {
|
|
231
|
+
const storedTimeout = await getValue('affiliateAttributionActiveTime');
|
|
232
|
+
timeoutMs = storedTimeout ? parseInt(storedTimeout, 10) : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// If no timeout is set, attribution never expires
|
|
236
|
+
if (timeoutMs === null) {
|
|
237
|
+
this.verboseLog('No attribution timeout configured, attribution is valid');
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const storedDate = new Date(storedDateStr);
|
|
242
|
+
const currentDate = new Date();
|
|
243
|
+
const elapsedMs = currentDate.getTime() - storedDate.getTime();
|
|
244
|
+
|
|
245
|
+
const isValid = elapsedMs <= timeoutMs;
|
|
246
|
+
this.verboseLog(`Attribution stored: ${storedDateStr}, elapsed: ${elapsedMs}ms, timeout: ${timeoutMs}ms, valid: ${isValid}`);
|
|
247
|
+
|
|
248
|
+
return isValid;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static async getAffiliateStoredDate(): Promise<string | null> {
|
|
252
|
+
this.verboseLog('Getting affiliate stored date...');
|
|
253
|
+
const storedDate = await getValue('affiliateStoredDate');
|
|
254
|
+
this.verboseLog(`Stored date: ${storedDate || 'none'}`);
|
|
255
|
+
return storedDate;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
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
|
|
261
|
+
* @param affiliateCode The short code or deep link to look up
|
|
262
|
+
* @returns AffiliateDetails if found, null otherwise
|
|
263
|
+
*/
|
|
264
|
+
static async getAffiliateDetails(affiliateCode: string): Promise<AffiliateDetails | null> {
|
|
265
|
+
this.verboseLog(`Getting affiliate details for: ${affiliateCode}`);
|
|
266
|
+
|
|
267
|
+
const companyCode = this.companyCode || await getValue('companyCode');
|
|
268
|
+
if (!companyCode) {
|
|
269
|
+
this.verboseLog('Cannot get affiliate details: no company code available');
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Strip UUID from code if present (e.g., "ABC123-uuid" becomes "ABC123")
|
|
274
|
+
const cleanCode = affiliateCode.includes('-') ? affiliateCode.split('-')[0] : affiliateCode;
|
|
275
|
+
this.verboseLog(`Clean code: ${cleanCode}`);
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const url = 'https://api.insertaffiliate.com/V1/checkAffiliateExists';
|
|
279
|
+
const payload = {
|
|
280
|
+
companyId: companyCode,
|
|
281
|
+
affiliateCode: cleanCode,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
this.verboseLog(`Making API call to: ${url}`);
|
|
285
|
+
this.verboseLog(`Payload: ${JSON.stringify(payload)}`);
|
|
286
|
+
|
|
287
|
+
const response = await fetch(url, {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: { 'Content-Type': 'application/json' },
|
|
290
|
+
body: JSON.stringify(payload),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
this.verboseLog(`API response status: ${response.status}`);
|
|
294
|
+
|
|
295
|
+
if (!response.ok) {
|
|
296
|
+
this.verboseLog(`Failed to get affiliate details, status: ${response.status}`);
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const data = await response.json();
|
|
301
|
+
this.verboseLog(`API response data: ${JSON.stringify(data)}`);
|
|
302
|
+
|
|
303
|
+
if (data.exists && data.affiliate) {
|
|
304
|
+
const details: AffiliateDetails = {
|
|
305
|
+
affiliateName: data.affiliate.affiliateName,
|
|
306
|
+
affiliateShortCode: data.affiliate.affiliateShortCode,
|
|
307
|
+
deeplinkUrl: data.affiliate.deeplinkurl,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
this.verboseLog(`Successfully retrieved affiliate details for: ${details.affiliateName}`);
|
|
311
|
+
return details;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.verboseLog('Affiliate does not exist');
|
|
315
|
+
return null;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
this.verboseLog(`Error fetching affiliate details: ${error}`);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
static async returnCompanyId(): Promise<string | null> {
|
|
323
|
+
this.verboseLog('Getting company ID...');
|
|
324
|
+
const companyCode = this.companyCode || await getValue('companyCode');
|
|
325
|
+
this.verboseLog(`Company ID: ${companyCode || 'none'}`);
|
|
326
|
+
return companyCode;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get the offer code for the current affiliate
|
|
331
|
+
* @param platformType Optional platform type: 'stripe' (default for web), 'ios', or 'android'
|
|
332
|
+
* @returns The offer code for the specified platform, or null if not found
|
|
333
|
+
*/
|
|
334
|
+
static async getOfferCode(platformType: 'ios' | 'android' | 'stripe' = 'stripe'): Promise<string | null> {
|
|
335
|
+
this.verboseLog(`Getting offer code for platform: ${platformType}...`);
|
|
336
|
+
|
|
337
|
+
const storageKey = platformType === 'stripe' ? 'offerCode' : `offerCode_${platformType}`;
|
|
338
|
+
|
|
339
|
+
// Return cached offer code if available (only for default Stripe)
|
|
340
|
+
if (platformType === 'stripe' && this.offerCode) {
|
|
341
|
+
this.verboseLog(`Returning cached offer code: ${this.offerCode}`);
|
|
342
|
+
return this.offerCode;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Try to get from storage
|
|
346
|
+
const storedOfferCode = await getValue(storageKey);
|
|
347
|
+
if (storedOfferCode) {
|
|
348
|
+
this.verboseLog(`Returning stored offer code: ${storedOfferCode}`);
|
|
349
|
+
if (platformType === 'stripe') {
|
|
350
|
+
this.offerCode = storedOfferCode;
|
|
351
|
+
}
|
|
352
|
+
return storedOfferCode;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// If not in storage, try to fetch it
|
|
356
|
+
const shortCode = await getValue('referrerLink');
|
|
357
|
+
if (shortCode) {
|
|
358
|
+
this.verboseLog(`No stored offer code, fetching for short code: ${shortCode}`);
|
|
359
|
+
const fetchedCode = await this.fetchOfferCodeForPlatform(shortCode, platformType);
|
|
360
|
+
return fetchedCode;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.verboseLog('No offer code found');
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get the Stripe coupon/promo code for the current affiliate
|
|
369
|
+
* Convenience method that calls getOfferCode with platformType='stripe'
|
|
370
|
+
* @returns The Stripe coupon/promo code, or null if not found
|
|
371
|
+
*/
|
|
372
|
+
static async getStripeCouponCode(): Promise<string | null> {
|
|
373
|
+
return this.getOfferCode('stripe');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Fetch offer code for a specific platform
|
|
378
|
+
* @param shortCode The affiliate short code
|
|
379
|
+
* @param platformType The platform type: 'ios', 'android', or 'stripe'
|
|
380
|
+
* @returns The offer code for the specified platform, or null if not found
|
|
381
|
+
*/
|
|
382
|
+
private static async fetchOfferCodeForPlatform(shortCode: string, platformType: 'ios' | 'android' | 'stripe'): Promise<string | null> {
|
|
383
|
+
this.verboseLog(`Fetching offer code for platform: ${platformType}, short code: ${shortCode}`);
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
const companyCode = this.companyCode || await getValue('companyCode');
|
|
387
|
+
if (!companyCode) {
|
|
388
|
+
this.verboseLog('Cannot fetch offer code: no company code available');
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const encoded = encodeURIComponent(shortCode);
|
|
393
|
+
const url = `https://api.insertaffiliate.com/v1/affiliateReturnOfferCode/${companyCode}/${encoded}?platformType=${platformType}`;
|
|
394
|
+
this.verboseLog(`Making API call to: ${url}`);
|
|
395
|
+
|
|
396
|
+
const response = await fetch(url);
|
|
397
|
+
this.verboseLog(`API response status: ${response.status}`);
|
|
398
|
+
|
|
399
|
+
if (!response.ok) {
|
|
400
|
+
this.verboseLog(`Failed to fetch offer code, status: ${response.status}`);
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
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
|
+
];
|
|
413
|
+
|
|
414
|
+
if (errorCodes.includes(offerCode)) {
|
|
415
|
+
this.verboseLog('Offer code not found or invalid');
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Store offer code with platform-specific key
|
|
420
|
+
const storageKey = platformType === 'stripe' ? 'offerCode' : `offerCode_${platformType}`;
|
|
421
|
+
await saveValue(storageKey, offerCode);
|
|
422
|
+
if (platformType === 'stripe') {
|
|
423
|
+
this.offerCode = offerCode;
|
|
424
|
+
}
|
|
425
|
+
this.verboseLog(`Offer code stored successfully with key ${storageKey}: ${offerCode}`);
|
|
426
|
+
|
|
427
|
+
return offerCode;
|
|
428
|
+
} catch (error) {
|
|
429
|
+
this.verboseLog(`Error fetching offer code: ${error}`);
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private static async fetchAndStoreOfferCode(shortCode: string): Promise<void> {
|
|
435
|
+
this.verboseLog(`Fetching offer code for short code: ${shortCode}`);
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
// Get company code
|
|
439
|
+
const companyCode = this.companyCode || await getValue('companyCode');
|
|
440
|
+
if (!companyCode) {
|
|
441
|
+
this.verboseLog('Cannot fetch offer code: no company code available');
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Use the more efficient endpoint with company code and just the short code
|
|
446
|
+
const encoded = encodeURIComponent(shortCode);
|
|
447
|
+
const url = `https://api.insertaffiliate.com/v1/affiliateReturnOfferCode/${companyCode}/${encoded}`;
|
|
448
|
+
this.verboseLog(`Making API call to: ${url}`);
|
|
449
|
+
|
|
450
|
+
const response = await fetch(url);
|
|
451
|
+
this.verboseLog(`API response status: ${response.status}`);
|
|
452
|
+
|
|
453
|
+
if (!response.ok) {
|
|
454
|
+
this.verboseLog(`Failed to fetch offer code, status: ${response.status}`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
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
|
+
];
|
|
468
|
+
|
|
469
|
+
if (errorCodes.includes(offerCode)) {
|
|
470
|
+
this.verboseLog('Offer code not found or invalid');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Store offer code
|
|
475
|
+
this.offerCode = offerCode;
|
|
476
|
+
await saveValue('offerCode', offerCode);
|
|
477
|
+
this.verboseLog(`Offer code stored successfully: ${offerCode}`);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
this.verboseLog(`Error fetching offer code: ${error}`);
|
|
480
|
+
}
|
|
63
481
|
}
|
|
64
482
|
|
|
65
483
|
static async trackEvent(eventName: string): Promise<void> {
|
|
484
|
+
this.verboseLog(`Tracking event: ${eventName}`);
|
|
485
|
+
|
|
66
486
|
const id = await this.returnInsertAffiliateIdentifier();
|
|
67
487
|
|
|
68
488
|
if (!id) {
|
|
69
|
-
|
|
489
|
+
this.verboseLog('Cannot track event: no affiliate identifier available');
|
|
70
490
|
return;
|
|
71
491
|
}
|
|
72
492
|
|
|
73
493
|
const companyCode = this.companyCode || await getValue('companyCode');
|
|
494
|
+
this.verboseLog(`Company code: ${companyCode || 'empty'}`);
|
|
74
495
|
|
|
75
|
-
if (!companyCode)
|
|
496
|
+
if (!companyCode) {
|
|
497
|
+
this.verboseLog('Cannot track event: no company code available');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const payload = {
|
|
502
|
+
eventName,
|
|
503
|
+
deepLinkParam: id,
|
|
504
|
+
companyId: companyCode,
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
this.verboseLog(`Track event payload: ${JSON.stringify(payload)}`);
|
|
508
|
+
this.verboseLog('Making API call to track event...');
|
|
76
509
|
|
|
77
510
|
try {
|
|
78
|
-
await fetch('https://api.insertaffiliate.com/v1/trackEvent', {
|
|
511
|
+
const response = await fetch('https://api.insertaffiliate.com/v1/trackEvent', {
|
|
79
512
|
method: 'POST',
|
|
80
513
|
headers: { 'Content-Type': 'application/json' },
|
|
81
|
-
body: JSON.stringify(
|
|
82
|
-
{
|
|
83
|
-
eventName,
|
|
84
|
-
deepLinkParam: id,
|
|
85
|
-
companyId: companyCode,
|
|
86
|
-
}),
|
|
514
|
+
body: JSON.stringify(payload),
|
|
87
515
|
});
|
|
88
|
-
|
|
516
|
+
|
|
517
|
+
this.verboseLog(`Track event API response status: ${response.status}`);
|
|
518
|
+
|
|
519
|
+
if (response.ok) {
|
|
520
|
+
this.verboseLog(`Event tracked successfully: ${eventName}`);
|
|
521
|
+
} else {
|
|
522
|
+
this.verboseLog(`Failed to track event with status code: ${response.status}`);
|
|
523
|
+
}
|
|
89
524
|
} catch (err) {
|
|
90
|
-
|
|
525
|
+
this.verboseLog(`Network error tracking event: ${err}`);
|
|
91
526
|
}
|
|
92
527
|
}
|
|
93
528
|
|
|
94
529
|
static async returnUserAccountTokenAndStoreExpectedTransaction(): Promise<string | null> {
|
|
530
|
+
this.verboseLog('Getting user account token and storing expected transaction...');
|
|
531
|
+
|
|
95
532
|
const shortCode = await this.returnInsertAffiliateIdentifier();
|
|
96
|
-
if (!shortCode)
|
|
533
|
+
if (!shortCode) {
|
|
534
|
+
this.verboseLog('No affiliate identifier found, not saving expected transaction');
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
97
537
|
|
|
98
538
|
let token = await getValue('userAccountToken');
|
|
539
|
+
this.verboseLog(`Existing token: ${token || 'none'}`);
|
|
540
|
+
|
|
99
541
|
if (!token) {
|
|
542
|
+
this.verboseLog('Generating new user account token...');
|
|
100
543
|
token = generateUUID();
|
|
101
544
|
await saveValue('userAccountToken', token);
|
|
545
|
+
this.verboseLog(`Generated and saved new token: ${token}`);
|
|
102
546
|
}
|
|
103
547
|
|
|
548
|
+
this.verboseLog(`User account token: ${token}`);
|
|
104
549
|
await this.storeExpectedStoreTransaction(token);
|
|
105
550
|
return token;
|
|
106
551
|
}
|
|
107
552
|
|
|
108
553
|
static async storeExpectedStoreTransaction(userAccountToken: string): Promise<void> {
|
|
554
|
+
this.verboseLog(`Storing expected store transaction with token: ${userAccountToken}`);
|
|
555
|
+
|
|
109
556
|
const companyCode = this.companyCode || await getValue('companyCode');
|
|
110
557
|
const shortCode = await this.returnInsertAffiliateIdentifier();
|
|
111
558
|
|
|
559
|
+
this.verboseLog(`Company code: ${companyCode || 'empty'}, Short code: ${shortCode || 'empty'}`);
|
|
560
|
+
|
|
112
561
|
if (!companyCode || !shortCode) {
|
|
113
|
-
|
|
562
|
+
this.verboseLog('Cannot store transaction: missing company code or identifier');
|
|
114
563
|
return;
|
|
115
564
|
}
|
|
116
565
|
|
|
@@ -121,6 +570,9 @@ export class InsertAffiliate {
|
|
|
121
570
|
storedDate: new Date().toISOString(),
|
|
122
571
|
};
|
|
123
572
|
|
|
573
|
+
this.verboseLog(`Payload: ${JSON.stringify(payload)}`);
|
|
574
|
+
this.verboseLog('Making API call to store expected transaction...');
|
|
575
|
+
|
|
124
576
|
try {
|
|
125
577
|
const res = await fetch('https://api.insertaffiliate.com/v1/api/app-store-webhook/create-expected-transaction', {
|
|
126
578
|
method: 'POST',
|
|
@@ -128,13 +580,15 @@ export class InsertAffiliate {
|
|
|
128
580
|
body: JSON.stringify(payload),
|
|
129
581
|
});
|
|
130
582
|
|
|
583
|
+
this.verboseLog(`API response status: ${res.status}`);
|
|
584
|
+
|
|
131
585
|
if (res.status === 200) {
|
|
132
|
-
|
|
586
|
+
this.verboseLog('Expected transaction stored successfully on server');
|
|
133
587
|
} else {
|
|
134
|
-
|
|
588
|
+
this.verboseLog(`Failed to store transaction with status: ${res.status}`);
|
|
135
589
|
}
|
|
136
590
|
} catch (error) {
|
|
137
|
-
|
|
591
|
+
this.verboseLog(`Network error storing transaction: ${error}`);
|
|
138
592
|
}
|
|
139
593
|
}
|
|
140
594
|
|
|
@@ -145,7 +599,9 @@ export class InsertAffiliate {
|
|
|
145
599
|
iapticPublicKey: string
|
|
146
600
|
): Promise<boolean> {
|
|
147
601
|
try {
|
|
602
|
+
this.verboseLog('Starting Iaptic purchase validation...');
|
|
148
603
|
const isIOS = typeof window !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
604
|
+
this.verboseLog(`Platform detected: ${isIOS ? 'iOS' : 'Android'}`);
|
|
149
605
|
|
|
150
606
|
const baseRequest = {
|
|
151
607
|
id: iapticAppId,
|
|
@@ -155,12 +611,14 @@ export class InsertAffiliate {
|
|
|
155
611
|
let transaction: any;
|
|
156
612
|
|
|
157
613
|
if (isIOS) {
|
|
614
|
+
this.verboseLog('Creating iOS transaction payload');
|
|
158
615
|
transaction = {
|
|
159
616
|
id: iapticAppId,
|
|
160
617
|
type: 'ios-appstore',
|
|
161
618
|
appStoreReceipt: jsonIapPurchase.transactionReceipt,
|
|
162
619
|
};
|
|
163
620
|
} else {
|
|
621
|
+
this.verboseLog('Creating Android transaction payload');
|
|
164
622
|
const receiptJson: IapticAndroidReceipt = JSON.parse(atob(jsonIapPurchase.transactionReceipt));
|
|
165
623
|
transaction = {
|
|
166
624
|
id: receiptJson.orderId,
|
|
@@ -172,6 +630,7 @@ export class InsertAffiliate {
|
|
|
172
630
|
}
|
|
173
631
|
|
|
174
632
|
const insertAffiliateIdentifier = await this.returnInsertAffiliateIdentifier();
|
|
633
|
+
this.verboseLog(`Affiliate identifier: ${insertAffiliateIdentifier || 'none'}`);
|
|
175
634
|
|
|
176
635
|
const payload = {
|
|
177
636
|
...baseRequest,
|
|
@@ -181,6 +640,7 @@ export class InsertAffiliate {
|
|
|
181
640
|
: undefined,
|
|
182
641
|
};
|
|
183
642
|
|
|
643
|
+
this.verboseLog('Making API call to Iaptic validator...');
|
|
184
644
|
const response = await fetch('https://validator.iaptic.com/v1/validate', {
|
|
185
645
|
method: 'POST',
|
|
186
646
|
headers: {
|
|
@@ -190,19 +650,43 @@ export class InsertAffiliate {
|
|
|
190
650
|
body: JSON.stringify(payload),
|
|
191
651
|
});
|
|
192
652
|
|
|
193
|
-
|
|
653
|
+
this.verboseLog(`Iaptic validation response status: ${response.status}`);
|
|
654
|
+
|
|
655
|
+
if (response.status === 200) {
|
|
656
|
+
this.verboseLog('Purchase validated successfully');
|
|
657
|
+
return true;
|
|
658
|
+
} else {
|
|
659
|
+
this.verboseLog(`Validation failed with status: ${response.status}`);
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
194
662
|
} catch (error) {
|
|
195
|
-
|
|
663
|
+
this.verboseLog(`Error during purchase validation: ${error}`);
|
|
196
664
|
return false;
|
|
197
665
|
}
|
|
198
666
|
}
|
|
199
667
|
|
|
200
668
|
static async fetchAndConditionallyOpenUrl(affiliateLink: string, offerCodeUrlId: string): Promise<void> {
|
|
669
|
+
this.verboseLog('Fetching offer code and opening URL...');
|
|
201
670
|
const encoded = encodeURIComponent(affiliateLink);
|
|
671
|
+
this.verboseLog(`Encoded affiliate link: ${encoded}`);
|
|
202
672
|
|
|
203
673
|
try {
|
|
204
|
-
|
|
674
|
+
// Get company code
|
|
675
|
+
const companyCode = this.companyCode || await getValue('companyCode');
|
|
676
|
+
if (!companyCode) {
|
|
677
|
+
this.verboseLog('Cannot fetch offer code: no company code available');
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
this.verboseLog('Making API call to fetch offer code...');
|
|
682
|
+
const url = `https://api.insertaffiliate.com/v1/affiliateReturnOfferCode/${companyCode}/${encoded}`;
|
|
683
|
+
this.verboseLog(`API URL: ${url}`);
|
|
684
|
+
|
|
685
|
+
const res = await fetch(url);
|
|
686
|
+
this.verboseLog(`API response status: ${res.status}`);
|
|
687
|
+
|
|
205
688
|
const offerCode = (await res.text()).replace(/[^a-zA-Z0-9]/g, '');
|
|
689
|
+
this.verboseLog(`Received offer code: ${offerCode}`);
|
|
206
690
|
|
|
207
691
|
const errorCodes = [
|
|
208
692
|
'errorofferCodeNotFound',
|
|
@@ -212,37 +696,59 @@ export class InsertAffiliate {
|
|
|
212
696
|
];
|
|
213
697
|
|
|
214
698
|
if (errorCodes.includes(offerCode)) {
|
|
215
|
-
|
|
699
|
+
this.verboseLog('Offer code not found or invalid');
|
|
216
700
|
return;
|
|
217
701
|
}
|
|
218
702
|
|
|
219
703
|
const redeemUrl = `https://apps.apple.com/redeem?ctx=offercodes&id=${offerCodeUrlId}&code=${offerCode}`;
|
|
704
|
+
this.verboseLog(`Opening redeem URL: ${redeemUrl}`);
|
|
220
705
|
window.open(redeemUrl, '_blank');
|
|
706
|
+
this.verboseLog('Redeem URL opened successfully');
|
|
221
707
|
} catch (err) {
|
|
222
|
-
|
|
708
|
+
this.verboseLog(`Error fetching/opening offer code: ${err}`);
|
|
223
709
|
}
|
|
224
710
|
}
|
|
225
711
|
|
|
226
712
|
private static async getOrCreateUserID(): Promise<string> {
|
|
713
|
+
this.verboseLog('Getting or creating user ID...');
|
|
227
714
|
let id = await getValue('userId');
|
|
715
|
+
this.verboseLog(`Existing user ID: ${id || 'none'}`);
|
|
716
|
+
|
|
228
717
|
if (!id) {
|
|
718
|
+
this.verboseLog('Generating new user ID...');
|
|
229
719
|
id = generateShortDeviceID();
|
|
230
720
|
await saveValue('userId', id);
|
|
721
|
+
this.verboseLog(`Generated and saved new user ID: ${id}`);
|
|
231
722
|
}
|
|
723
|
+
|
|
232
724
|
return id;
|
|
233
725
|
}
|
|
234
726
|
|
|
235
727
|
private static async fetchShortLink(link: string): Promise<string | null> {
|
|
236
728
|
try {
|
|
729
|
+
this.verboseLog('Converting deep link to short link...');
|
|
237
730
|
const encoded = encodeURIComponent(link);
|
|
238
731
|
const companyCode = this.companyCode || await getValue('companyCode');
|
|
239
|
-
|
|
732
|
+
this.verboseLog(`Company code: ${companyCode || 'empty'}`);
|
|
733
|
+
|
|
734
|
+
if (!companyCode) {
|
|
735
|
+
this.verboseLog('No company code available, cannot convert link');
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const url = `https://api.insertaffiliate.com/V1/convert-deep-link-to-short-link?companyId=${companyCode}&deepLinkUrl=${encoded}`;
|
|
740
|
+
this.verboseLog(`Making API call to: ${url}`);
|
|
741
|
+
|
|
742
|
+
const res = await fetch(url);
|
|
743
|
+
this.verboseLog(`API response status: ${res.status}`);
|
|
240
744
|
|
|
241
|
-
const res = await fetch(`https://api.insertaffiliate.com/V1/convert-deep-link-to-short-link?companyId=${companyCode}&deepLinkUrl=${encoded}`);
|
|
242
745
|
const data = await res.json();
|
|
243
|
-
|
|
746
|
+
const shortLink = data?.shortLink || null;
|
|
747
|
+
this.verboseLog(`Short link received: ${shortLink || 'none'}`);
|
|
748
|
+
|
|
749
|
+
return shortLink;
|
|
244
750
|
} catch (err: any) {
|
|
245
|
-
|
|
751
|
+
this.verboseLog(`Error fetching short link: ${err?.message || err}`);
|
|
246
752
|
return null;
|
|
247
753
|
}
|
|
248
754
|
}
|