insert-affiliate-js-sdk 1.1.0 → 1.3.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.
@@ -0,0 +1,237 @@
1
+ # Stripe Integration Guide
2
+
3
+ This guide covers all Stripe-based payment integrations for affiliate tracking with Insert Affiliate.
4
+
5
+ ## Prerequisites
6
+
7
+ - Insert Affiliate SDK initialized in your web application
8
+ - Stripe account connected to Insert Affiliate (see [Connect Stripe Account](#1-connect-your-stripe-account) below)
9
+
10
+ ## 1. Connect Your Stripe Account
11
+
12
+ Before integrating the SDK code, you must connect your Stripe account to Insert Affiliate:
13
+
14
+ 1. Go to your [Insert Affiliate dashboard settings](https://app.insertaffiliate.com/settings)
15
+ 2. Navigate to the payment verification settings
16
+ 3. Select **Stripe** as your verification method
17
+ 4. Click **Connect with Stripe** to authorize the connection via Stripe Connect
18
+ 5. Once connected, Insert Affiliate will automatically receive your Stripe events
19
+
20
+ ## 2. Direct Stripe Checkout Integration
21
+
22
+ For web-based subscriptions and payments using Stripe Checkout directly.
23
+
24
+ ### Frontend Code
25
+
26
+ Retrieve the affiliate identifier and company ID before creating a checkout session:
27
+
28
+ ```javascript
29
+ import { InsertAffiliate } from 'insert-affiliate-js-sdk';
30
+
31
+ const affiliateId = await InsertAffiliate.returnInsertAffiliateIdentifier();
32
+ const companyId = await InsertAffiliate.returnCompanyId();
33
+
34
+ const response = await fetch('https://your-backend.com/create-checkout-session', {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ },
39
+ body: JSON.stringify({
40
+ priceId: 'price_xxxxx',
41
+ insertAffiliate: affiliateId,
42
+ insertAffiliateCompanyId: companyId,
43
+ successUrl: window.location.origin + '/success',
44
+ cancelUrl: window.location.origin + '/canceled',
45
+ }),
46
+ });
47
+
48
+ const { sessionId } = await response.json();
49
+ // Redirect to Stripe Checkout
50
+ ```
51
+
52
+ ### Backend Code (Node.js)
53
+
54
+ Store both the affiliate identifier and company ID in Stripe metadata:
55
+
56
+ ```javascript
57
+ const stripe = require('stripe')('sk_test_xxxxx');
58
+
59
+ app.post('/create-checkout-session', async (req, res) => {
60
+ const { priceId, insertAffiliate, insertAffiliateCompanyId, successUrl, cancelUrl } = req.body;
61
+
62
+ const session = await stripe.checkout.sessions.create({
63
+ mode: 'subscription',
64
+ line_items: [{
65
+ price: priceId,
66
+ quantity: 1,
67
+ }],
68
+ metadata: {
69
+ insertAffiliate: insertAffiliate || '',
70
+ insertAffiliateCompanyId: insertAffiliateCompanyId || '',
71
+ },
72
+ subscription_data: {
73
+ metadata: {
74
+ insertAffiliate: insertAffiliate || '',
75
+ insertAffiliateCompanyId: insertAffiliateCompanyId || '',
76
+ },
77
+ },
78
+ success_url: successUrl,
79
+ cancel_url: cancelUrl,
80
+ });
81
+
82
+ res.json({ sessionId: session.id });
83
+ });
84
+ ```
85
+
86
+ **Required Metadata Fields:**
87
+ - `insertAffiliate`: The affiliate's short code
88
+ - `insertAffiliateCompanyId`: Your Insert Affiliate company ID
89
+
90
+ Both fields are required for proper affiliate attribution and commission tracking.
91
+
92
+ ## 3. Stripe Billing with RevenueCat
93
+
94
+ If you're using RevenueCat's [Stripe Billing](https://www.revenuecat.com/docs/web/integrations/stripe), you can track affiliate conversions through Insert Affiliate while using RevenueCat for subscription management.
95
+
96
+ **Prerequisites:**
97
+ - You must host your own web checkout page where Stripe Checkout is embedded
98
+ - Follow RevenueCat's [Stripe integration guide](https://www.revenuecat.com/docs/web/integrations/stripe)
99
+
100
+ **Integration:**
101
+
102
+ Follow the same steps as [Direct Stripe Checkout Integration](#2-direct-stripe-checkout-integration) above. Since you've connected your Stripe account via Stripe Connect, Insert Affiliate will automatically receive all Stripe events. RevenueCat handles subscription management, while Insert Affiliate handles affiliate attribution through the Stripe metadata.
103
+
104
+ ## 4. RevenueCat Web Billing Integration
105
+
106
+ If you're using [RevenueCat's Web SDK with Web Billing](https://www.revenuecat.com/docs/web/web-billing/overview), pass UTM parameters as purchase metadata.
107
+
108
+ **Prerequisites:**
109
+ - RevenueCat Web SDK installed and configured
110
+ - RevenueCat Web Billing set up with Stripe
111
+ - Insert Affiliate SDK initialized on your page
112
+ - Stripe account connected via Stripe Connect
113
+
114
+ ### Implementation
115
+
116
+ ```javascript
117
+ import { InsertAffiliate } from 'insert-affiliate-js-sdk';
118
+ import { Purchases } from '@revenuecat/purchases-js';
119
+
120
+ // Initialize both SDKs
121
+ await InsertAffiliate.initialize('your_company_code');
122
+ const purchases = Purchases.configure('your_revenuecat_web_api_key');
123
+
124
+ // Get affiliate information
125
+ const affiliateId = await InsertAffiliate.returnInsertAffiliateIdentifier(true);
126
+ const companyId = await InsertAffiliate.returnCompanyId();
127
+
128
+ // Prepare metadata with UTM parameters
129
+ const metadata: Record<string, string> = {};
130
+
131
+ if (affiliateId && affiliateId !== 'none') {
132
+ metadata.utm_source = 'insertAffiliate';
133
+ metadata.utm_medium = companyId || 'none';
134
+ metadata.utm_campaign = affiliateId;
135
+ }
136
+
137
+ // Get offerings and make purchase
138
+ const offerings = await purchases.getOfferings();
139
+ const selectedPackage = offerings.current?.availablePackages[0];
140
+
141
+ if (selectedPackage) {
142
+ const { customerInfo } = await purchases.purchase({
143
+ rcPackage: selectedPackage,
144
+ metadata: metadata,
145
+ });
146
+ console.log('Purchase successful!');
147
+ }
148
+ ```
149
+
150
+ **UTM Parameter Mapping:**
151
+ | Parameter | Value | Purpose |
152
+ |-----------|-------|---------|
153
+ | `utm_source` | `'insertAffiliate'` | Identifies Insert Affiliate conversions |
154
+ | `utm_medium` | Your company ID | Links to your Insert Affiliate account |
155
+ | `utm_campaign` | Affiliate identifier | Credits the specific affiliate |
156
+
157
+ ## 5. RevenueCat Web Purchase Links
158
+
159
+ If you're using [RevenueCat Web Purchase Links](https://www.revenuecat.com/docs/web/web-billing/web-purchase-links) for online campaigns, append UTM parameters to the links.
160
+
161
+ ### URL Format
162
+
163
+ Base URL:
164
+ ```
165
+ https://pay.rev.cat/sandbox/viqxbcoudyfaeaae/
166
+ ```
167
+
168
+ With affiliate parameters:
169
+ ```
170
+ https://pay.rev.cat/sandbox/viqxbcoudyfaeaae/?utm_source=insertAffiliate&utm_medium={companyId}&utm_campaign={affiliateShortCode}
171
+ ```
172
+
173
+ ### Full Example
174
+
175
+ ```
176
+ https://pay.rev.cat/sandbox/viqxbcoudyfaeaxa/?utm_source=insertAffiliate&utm_medium=12345&utm_campaign=AFF123
177
+ ```
178
+
179
+ Where:
180
+ - `utm_source=insertAffiliate` - Identifies this as an Insert Affiliate conversion
181
+ - `utm_medium=12345` - Your Insert Affiliate company ID
182
+ - `utm_campaign=AFF123` - The affiliate's short code
183
+
184
+ The Insert Affiliate SDK automatically processes the UTM parameters when users open these URLs and attributes the resulting purchase to the correct affiliate.
185
+
186
+ ## Using the Callback for Automatic Integration
187
+
188
+ The SDK provides a callback that fires whenever the affiliate identifier changes, making it easy to automatically update your checkout flow:
189
+
190
+ ```javascript
191
+ import { InsertAffiliate } from 'insert-affiliate-js-sdk';
192
+
193
+ let currentAffiliateId = null;
194
+
195
+ InsertAffiliate.setInsertAffiliateIdentifierChangeCallback((identifier) => {
196
+ if (identifier) {
197
+ console.log('Affiliate identifier changed:', identifier);
198
+ currentAffiliateId = identifier;
199
+ }
200
+ });
201
+
202
+ // Later, when creating a Stripe checkout session
203
+ const companyId = await InsertAffiliate.returnCompanyId();
204
+
205
+ const response = await fetch('/create-checkout-session', {
206
+ method: 'POST',
207
+ headers: { 'Content-Type': 'application/json' },
208
+ body: JSON.stringify({
209
+ priceId: 'price_xxxxx',
210
+ insertAffiliate: currentAffiliateId,
211
+ insertAffiliateCompanyId: companyId,
212
+ successUrl: window.location.origin + '/success',
213
+ cancelUrl: window.location.origin + '/canceled',
214
+ }),
215
+ });
216
+ ```
217
+
218
+ ## Troubleshooting
219
+
220
+ **Problem:** Affiliate not credited for purchase
221
+ - **Solution:** Verify both `insertAffiliate` and `insertAffiliateCompanyId` are included in Stripe metadata
222
+ - Check that your Stripe account is connected via Stripe Connect in the Insert Affiliate dashboard
223
+
224
+ **Problem:** Metadata not appearing in Stripe
225
+ - **Solution:** Ensure metadata is passed to both `metadata` and `subscription_data.metadata` in the checkout session
226
+
227
+ **Problem:** UTM parameters not being captured
228
+ - **Solution:** Initialize the Insert Affiliate SDK before users interact with purchase flows
229
+ - Verify the SDK is processing URL parameters on page load
230
+
231
+ ## Next Steps
232
+
233
+ - Test with a test affiliate link to verify attribution
234
+ - Make a test purchase to confirm tracking works end-to-end
235
+ - Monitor conversions in your Insert Affiliate dashboard
236
+
237
+ [Back to Main README](../README.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insert-affiliate-js-sdk",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,7 +25,7 @@ export interface AffiliateDetails {
25
25
  deeplinkUrl: string;
26
26
  }
27
27
 
28
- export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null) => void;
28
+ export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null, offerCode: string | null) => void;
29
29
 
30
30
  export class InsertAffiliate {
31
31
  private static isInitialized: boolean = false;
@@ -33,6 +33,7 @@ export class InsertAffiliate {
33
33
  private static verboseLogging: boolean = false;
34
34
  private static insertAffiliateIdentifierChangeCallback: InsertAffiliateIdentifierChangeCallback | null = null;
35
35
  private static affiliateAttributionActiveTime: number | null = null; // in milliseconds
36
+ private static preventAffiliateTransfer: boolean = false;
36
37
  private static offerCode: string | null = null;
37
38
 
38
39
  private static verboseLog(message: string): void {
@@ -41,13 +42,20 @@ export class InsertAffiliate {
41
42
  }
42
43
  }
43
44
 
44
- static async initialize(code: string | null, verboseLogging: boolean = false, affiliateAttributionActiveTime?: number): Promise<void> {
45
+ static async initialize(
46
+ code: string | null,
47
+ verboseLogging: boolean = false,
48
+ affiliateAttributionActiveTime?: number,
49
+ preventAffiliateTransfer: boolean = false
50
+ ): Promise<void> {
45
51
  this.verboseLogging = verboseLogging;
52
+ this.preventAffiliateTransfer = preventAffiliateTransfer;
46
53
 
47
54
  if (verboseLogging) {
48
55
  this.verboseLog('Starting SDK initialization...');
49
56
  this.verboseLog(`Company code provided: ${code ? 'Yes' : 'No'}`);
50
57
  this.verboseLog('Verbose logging enabled');
58
+ this.verboseLog(`Prevent affiliate transfer: ${preventAffiliateTransfer}`);
51
59
  }
52
60
 
53
61
  if (this.isInitialized) {
@@ -154,6 +162,13 @@ export class InsertAffiliate {
154
162
  return identifier;
155
163
  }
156
164
 
165
+ // Check if transfer is blocked
166
+ if (this.preventAffiliateTransfer && existingShortCode && existingShortCode !== shortCode) {
167
+ this.verboseLog(`Transfer blocked: existing affiliate "${existingShortCode}" protected from being replaced by "${shortCode}"`);
168
+ const identifier = `${existingShortCode}-${userId}`;
169
+ return identifier;
170
+ }
171
+
157
172
  this.verboseLog(`Saving short code to storage: ${shortCode}`);
158
173
  await saveValue('referrerLink', shortCode);
159
174
 
@@ -166,12 +181,12 @@ export class InsertAffiliate {
166
181
  this.verboseLog(`Returning identifier: ${identifier}`);
167
182
 
168
183
  // Auto-fetch and store offer code (use just the short code, not the full identifier)
169
- await this.fetchAndStoreOfferCode(shortCode);
184
+ const offerCode = await this.fetchAndStoreOfferCode(shortCode);
170
185
 
171
186
  // Trigger callback if one is registered
172
187
  if (this.insertAffiliateIdentifierChangeCallback) {
173
- this.verboseLog(`Triggering callback with identifier: ${identifier}`);
174
- this.insertAffiliateIdentifierChangeCallback(identifier);
188
+ this.verboseLog(`Triggering callback with identifier: ${identifier}, offerCode: ${offerCode || 'none'}`);
189
+ this.insertAffiliateIdentifierChangeCallback(identifier, offerCode);
175
190
  }
176
191
 
177
192
  return identifier;
@@ -195,7 +210,7 @@ export class InsertAffiliate {
195
210
  }
196
211
 
197
212
  // Validate that the short code exists in the system
198
- const affiliateDetails = await this.getAffiliateDetails(shortCode);
213
+ const affiliateDetails = await this.getAffiliateDetails(shortCode, { trackUsage: true });
199
214
  if (!affiliateDetails) {
200
215
  this.verboseLog(`Short code '${shortCode}' does not exist or validation failed`);
201
216
  console.error(`[Insert Affiliate] Error: Short code '${shortCode}' does not exist or validation failed.`);
@@ -255,13 +270,46 @@ export class InsertAffiliate {
255
270
  return storedDate;
256
271
  }
257
272
 
273
+ /**
274
+ * Get the Unix timestamp (in milliseconds) when the affiliate attribution will expire
275
+ * @returns The expiry timestamp in milliseconds, or null if no attribution or no timeout configured
276
+ */
277
+ static async getAffiliateExpiryTimestamp(): Promise<number | null> {
278
+ this.verboseLog('Getting affiliate expiry timestamp...');
279
+
280
+ const storedDateStr = await getValue('affiliateStoredDate');
281
+ if (!storedDateStr) {
282
+ this.verboseLog('No stored date found, returning null');
283
+ return null;
284
+ }
285
+
286
+ // Get timeout value from storage or class property
287
+ let timeoutMs = this.affiliateAttributionActiveTime;
288
+ if (timeoutMs === null) {
289
+ const storedTimeout = await getValue('affiliateAttributionActiveTime');
290
+ timeoutMs = storedTimeout ? parseInt(storedTimeout, 10) : null;
291
+ }
292
+
293
+ // If no timeout is set, return null (attribution never expires)
294
+ if (timeoutMs === null) {
295
+ this.verboseLog('No attribution timeout configured, returning null');
296
+ return null;
297
+ }
298
+
299
+ const storedDate = new Date(storedDateStr);
300
+ const expiryTimestamp = storedDate.getTime() + timeoutMs;
301
+ this.verboseLog(`Expiry timestamp: ${expiryTimestamp} (stored: ${storedDateStr}, timeout: ${timeoutMs}ms)`);
302
+
303
+ return expiryTimestamp;
304
+ }
305
+
258
306
  /**
259
307
  * Retrieve detailed information about an affiliate by their short code or deep link
260
308
  * This method queries the API and does not store or set the affiliate identifier
261
309
  * @param affiliateCode The short code or deep link to look up
262
310
  * @returns AffiliateDetails if found, null otherwise
263
311
  */
264
- static async getAffiliateDetails(affiliateCode: string): Promise<AffiliateDetails | null> {
312
+ static async getAffiliateDetails(affiliateCode: string, options?: { trackUsage?: boolean }): Promise<AffiliateDetails | null> {
265
313
  this.verboseLog(`Getting affiliate details for: ${affiliateCode}`);
266
314
 
267
315
  const companyCode = this.companyCode || await getValue('companyCode');
@@ -276,11 +324,15 @@ export class InsertAffiliate {
276
324
 
277
325
  try {
278
326
  const url = 'https://api.insertaffiliate.com/V1/checkAffiliateExists';
279
- const payload = {
327
+ const payload: Record<string, any> = {
280
328
  companyId: companyCode,
281
329
  affiliateCode: cleanCode,
282
330
  };
283
331
 
332
+ if (options?.trackUsage) {
333
+ payload.trackUsage = true;
334
+ }
335
+
284
336
  this.verboseLog(`Making API call to: ${url}`);
285
337
  this.verboseLog(`Payload: ${JSON.stringify(payload)}`);
286
338
 
@@ -326,28 +378,112 @@ export class InsertAffiliate {
326
378
  return companyCode;
327
379
  }
328
380
 
329
- static async getOfferCode(): Promise<string | null> {
330
- this.verboseLog('Getting offer code...');
381
+ /**
382
+ * Get the offer code for the current affiliate
383
+ * @param platformType Optional platform type: 'stripe' (default for web), 'ios', or 'android'
384
+ * @returns The offer code for the specified platform, or null if not found
385
+ */
386
+ static async getOfferCode(platformType: 'ios' | 'android' | 'stripe' = 'stripe'): Promise<string | null> {
387
+ this.verboseLog(`Getting offer code for platform: ${platformType}...`);
388
+
389
+ const storageKey = platformType === 'stripe' ? 'offerCode' : `offerCode_${platformType}`;
331
390
 
332
- // Return cached offer code if available
333
- if (this.offerCode) {
391
+ // Return cached offer code if available (only for default Stripe)
392
+ if (platformType === 'stripe' && this.offerCode) {
334
393
  this.verboseLog(`Returning cached offer code: ${this.offerCode}`);
335
394
  return this.offerCode;
336
395
  }
337
396
 
338
397
  // Try to get from storage
339
- const storedOfferCode = await getValue('offerCode');
398
+ const storedOfferCode = await getValue(storageKey);
340
399
  if (storedOfferCode) {
341
400
  this.verboseLog(`Returning stored offer code: ${storedOfferCode}`);
342
- this.offerCode = storedOfferCode;
401
+ if (platformType === 'stripe') {
402
+ this.offerCode = storedOfferCode;
403
+ }
343
404
  return storedOfferCode;
344
405
  }
345
406
 
407
+ // If not in storage, try to fetch it
408
+ const shortCode = await getValue('referrerLink');
409
+ if (shortCode) {
410
+ this.verboseLog(`No stored offer code, fetching for short code: ${shortCode}`);
411
+ const fetchedCode = await this.fetchOfferCodeForPlatform(shortCode, platformType);
412
+ return fetchedCode;
413
+ }
414
+
346
415
  this.verboseLog('No offer code found');
347
416
  return null;
348
417
  }
349
418
 
350
- private static async fetchAndStoreOfferCode(shortCode: string): Promise<void> {
419
+ /**
420
+ * Get the Stripe coupon/promo code for the current affiliate
421
+ * Convenience method that calls getOfferCode with platformType='stripe'
422
+ * @returns The Stripe coupon/promo code, or null if not found
423
+ */
424
+ static async getStripeCouponCode(): Promise<string | null> {
425
+ return this.getOfferCode('stripe');
426
+ }
427
+
428
+ /**
429
+ * Fetch offer code for a specific platform
430
+ * @param shortCode The affiliate short code
431
+ * @param platformType The platform type: 'ios', 'android', or 'stripe'
432
+ * @returns The offer code for the specified platform, or null if not found
433
+ */
434
+ private static async fetchOfferCodeForPlatform(shortCode: string, platformType: 'ios' | 'android' | 'stripe'): Promise<string | null> {
435
+ this.verboseLog(`Fetching offer code for platform: ${platformType}, short code: ${shortCode}`);
436
+
437
+ try {
438
+ const companyCode = this.companyCode || await getValue('companyCode');
439
+ if (!companyCode) {
440
+ this.verboseLog('Cannot fetch offer code: no company code available');
441
+ return null;
442
+ }
443
+
444
+ const encoded = encodeURIComponent(shortCode);
445
+ const url = `https://api.insertaffiliate.com/v1/affiliateReturnOfferCode/${companyCode}/${encoded}?platformType=${platformType}`;
446
+ this.verboseLog(`Making API call to: ${url}`);
447
+
448
+ const response = await fetch(url);
449
+ this.verboseLog(`API response status: ${response.status}`);
450
+
451
+ if (!response.ok) {
452
+ this.verboseLog(`Failed to fetch offer code, status: ${response.status}`);
453
+ return null;
454
+ }
455
+
456
+ const offerCode = (await response.text()).replace(/[^a-zA-Z0-9]/g, '');
457
+ this.verboseLog(`Received offer code: ${offerCode}`);
458
+
459
+ const errorCodes = [
460
+ 'errorofferCodeNotFound',
461
+ 'errorAffiliateoffercodenotfoundinanycompany',
462
+ 'errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas',
463
+ 'Routenotfound'
464
+ ];
465
+
466
+ if (errorCodes.includes(offerCode)) {
467
+ this.verboseLog('Offer code not found or invalid');
468
+ return null;
469
+ }
470
+
471
+ // Store offer code with platform-specific key
472
+ const storageKey = platformType === 'stripe' ? 'offerCode' : `offerCode_${platformType}`;
473
+ await saveValue(storageKey, offerCode);
474
+ if (platformType === 'stripe') {
475
+ this.offerCode = offerCode;
476
+ }
477
+ this.verboseLog(`Offer code stored successfully with key ${storageKey}: ${offerCode}`);
478
+
479
+ return offerCode;
480
+ } catch (error) {
481
+ this.verboseLog(`Error fetching offer code: ${error}`);
482
+ return null;
483
+ }
484
+ }
485
+
486
+ private static async fetchAndStoreOfferCode(shortCode: string): Promise<string | null> {
351
487
  this.verboseLog(`Fetching offer code for short code: ${shortCode}`);
352
488
 
353
489
  try {
@@ -355,7 +491,7 @@ export class InsertAffiliate {
355
491
  const companyCode = this.companyCode || await getValue('companyCode');
356
492
  if (!companyCode) {
357
493
  this.verboseLog('Cannot fetch offer code: no company code available');
358
- return;
494
+ return null;
359
495
  }
360
496
 
361
497
  // Use the more efficient endpoint with company code and just the short code
@@ -368,7 +504,7 @@ export class InsertAffiliate {
368
504
 
369
505
  if (!response.ok) {
370
506
  this.verboseLog(`Failed to fetch offer code, status: ${response.status}`);
371
- return;
507
+ return null;
372
508
  }
373
509
 
374
510
  const offerCode = (await response.text()).replace(/[^a-zA-Z0-9]/g, '');
@@ -384,15 +520,17 @@ export class InsertAffiliate {
384
520
 
385
521
  if (errorCodes.includes(offerCode)) {
386
522
  this.verboseLog('Offer code not found or invalid');
387
- return;
523
+ return null;
388
524
  }
389
525
 
390
526
  // Store offer code
391
527
  this.offerCode = offerCode;
392
528
  await saveValue('offerCode', offerCode);
393
529
  this.verboseLog(`Offer code stored successfully: ${offerCode}`);
530
+ return offerCode;
394
531
  } catch (error) {
395
532
  this.verboseLog(`Error fetching offer code: ${error}`);
533
+ return null;
396
534
  }
397
535
  }
398
536