@explorins/pers-sdk-react-native 2.3.0-alpha.3 → 2.3.2

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/dist/index.js CHANGED
@@ -2916,6 +2916,18 @@ exports.BalanceMigrationType = void 0;
2916
2916
  BalanceMigrationType["MERGE"] = "merge";
2917
2917
  })(exports.BalanceMigrationType || (exports.BalanceMigrationType = {}));
2918
2918
 
2919
+ /**
2920
+ * Entity types that support tags for aggregation
2921
+ */
2922
+ exports.TaggableEntityType = void 0;
2923
+ (function (TaggableEntityType) {
2924
+ TaggableEntityType["CAMPAIGN"] = "campaign";
2925
+ TaggableEntityType["REDEMPTION"] = "redemption";
2926
+ TaggableEntityType["BUSINESS"] = "business";
2927
+ TaggableEntityType["TOKEN_METADATA"] = "token_metadata";
2928
+ TaggableEntityType["USER_STATUS_TYPE"] = "user_status_type";
2929
+ })(exports.TaggableEntityType || (exports.TaggableEntityType = {}));
2930
+
2919
2931
  exports.PurchaseStatus = void 0;
2920
2932
  (function (PurchaseStatus) {
2921
2933
  // after creation of payment and before payment is done by user
@@ -3764,6 +3776,24 @@ const customFieldEntityTypes = [...customFieldEntityTypesArray];
3764
3776
  const selectOptionsSourceEntityArray = [...customFieldEntityTypesArray, 'country'];
3765
3777
  /** All supported select options source entities for iteration/validation */
3766
3778
  const selectOptionsSourceEntities = [...selectOptionsSourceEntityArray];
3779
+ /**
3780
+ * Default label fields for each entity type.
3781
+ * Used when labelField is not explicitly specified.
3782
+ */
3783
+ const SELECT_OPTIONS_LABEL_DEFAULTS = {
3784
+ business: 'displayName',
3785
+ campaign: 'name',
3786
+ redemption: 'name',
3787
+ user: 'email', // User has no single 'name' field
3788
+ country: 'label', // Static source - uses 'label' from { value, label }
3789
+ };
3790
+ /**
3791
+ * Get the default label field for a select options entity.
3792
+ * Falls back to 'name' for unknown entities.
3793
+ */
3794
+ function getSelectOptionsLabelDefault(entity) {
3795
+ return SELECT_OPTIONS_LABEL_DEFAULTS[entity] ?? 'name';
3796
+ }
3767
3797
 
3768
3798
  /**
3769
3799
  * Core Entity Types
@@ -3794,6 +3824,8 @@ const EntityTypes = {
3794
3824
  // Redemption entities
3795
3825
  REDEMPTION: 'redemption',
3796
3826
  REDEMPTION_TYPE: 'redemption_type',
3827
+ // Booking entities
3828
+ BOOKING: 'booking',
3797
3829
  // Infrastructure entities
3798
3830
  CONTRACT: 'contract',
3799
3831
  SIGNING_ACCOUNT: 'signing_account',
@@ -4189,6 +4221,34 @@ const LOCATION_TYPES = [
4189
4221
  'experience', // Experience package (multi-activity, bundle)
4190
4222
  'destination', // Destination/location-based (city pass, area access)
4191
4223
  ];
4224
+ /**
4225
+ * Check if a booking status satisfies a requirement type.
4226
+ *
4227
+ * @param status - Current booking status ('past', 'active', 'future')
4228
+ * @param requirement - Required booking status for eligibility
4229
+ * @returns true if the status satisfies the requirement
4230
+ *
4231
+ * @example
4232
+ * isBookingStatusValid('active', 'active_future'); // true
4233
+ * isBookingStatusValid('past', 'active_future'); // false
4234
+ * isBookingStatusValid('future', 'any'); // true
4235
+ */
4236
+ function isBookingStatusValid(status, requirement) {
4237
+ switch (requirement) {
4238
+ case 'active':
4239
+ return status === 'active';
4240
+ case 'future':
4241
+ return status === 'future';
4242
+ case 'past':
4243
+ return status === 'past';
4244
+ case 'active_future':
4245
+ return status === 'active' || status === 'future';
4246
+ case 'any':
4247
+ return true;
4248
+ default:
4249
+ return false;
4250
+ }
4251
+ }
4192
4252
 
4193
4253
  /**
4194
4254
  * Unified data source tracking for analytics across all entities.
@@ -4246,6 +4306,7 @@ const DATA_SOURCE_CHANNELS = [
4246
4306
  'webhook', // External webhook callback
4247
4307
  'sync', // Synchronization from external system
4248
4308
  'migration', // Data migration
4309
+ 'unknown', // Source couldn't be determined (fallback)
4249
4310
  ];
4250
4311
  /**
4251
4312
  * Type guard to check if a value is a valid DataSourceChannel.
@@ -4254,11 +4315,189 @@ function isDataSourceChannel(value) {
4254
4315
  return typeof value === 'string' && DATA_SOURCE_CHANNELS.includes(value);
4255
4316
  }
4256
4317
  /**
4257
- * Default data source for manual/admin operations.
4318
+ * Default data source when source cannot be determined.
4319
+ * Used as fallback when CLS context is unavailable.
4320
+ *
4321
+ * NOTE: 'unknown' indicates a gap in tracking - should be rare.
4322
+ * If you see many 'unknown' sources, check that DataSourceInterceptor is active.
4258
4323
  */
4259
4324
  const DEFAULT_DATA_SOURCE = {
4260
- channel: 'admin',
4325
+ channel: 'unknown',
4261
4326
  };
4327
+ /**
4328
+ * Known domain mappings for auto-detecting source from referrer.
4329
+ * Maps hostnames to normalized source names.
4330
+ */
4331
+ const KNOWN_SOURCE_DOMAINS = {
4332
+ // Google
4333
+ 'google.com': 'google',
4334
+ 'google.co.uk': 'google',
4335
+ 'google.de': 'google',
4336
+ 'google.fr': 'google',
4337
+ 'google.es': 'google',
4338
+ 'google.it': 'google',
4339
+ 'google.nl': 'google',
4340
+ 'google.pt': 'google',
4341
+ 'google.pl': 'google',
4342
+ 'google.ru': 'google',
4343
+ 'google.com.br': 'google',
4344
+ 'google.ca': 'google',
4345
+ 'google.com.au': 'google',
4346
+ 'google.co.jp': 'google',
4347
+ 'google.co.in': 'google',
4348
+ // Facebook
4349
+ 'facebook.com': 'facebook',
4350
+ 'fb.com': 'facebook',
4351
+ 'm.facebook.com': 'facebook',
4352
+ 'l.facebook.com': 'facebook',
4353
+ 'lm.facebook.com': 'facebook',
4354
+ // Instagram
4355
+ 'instagram.com': 'instagram',
4356
+ 'l.instagram.com': 'instagram',
4357
+ // Twitter/X
4358
+ 'twitter.com': 'twitter',
4359
+ 'x.com': 'twitter',
4360
+ 't.co': 'twitter',
4361
+ 'mobile.twitter.com': 'twitter',
4362
+ // LinkedIn
4363
+ 'linkedin.com': 'linkedin',
4364
+ 'lnkd.in': 'linkedin',
4365
+ // YouTube
4366
+ 'youtube.com': 'youtube',
4367
+ 'youtu.be': 'youtube',
4368
+ 'm.youtube.com': 'youtube',
4369
+ // TikTok
4370
+ 'tiktok.com': 'tiktok',
4371
+ 'vm.tiktok.com': 'tiktok',
4372
+ // Other social
4373
+ 'pinterest.com': 'pinterest',
4374
+ 'reddit.com': 'reddit',
4375
+ 'tumblr.com': 'tumblr',
4376
+ 'snapchat.com': 'snapchat',
4377
+ // Search engines
4378
+ 'bing.com': 'bing',
4379
+ 'yahoo.com': 'yahoo',
4380
+ 'duckduckgo.com': 'duckduckgo',
4381
+ 'baidu.com': 'baidu',
4382
+ 'yandex.com': 'yandex',
4383
+ 'yandex.ru': 'yandex',
4384
+ // Email providers (for tracking email clicks)
4385
+ 'mail.google.com': 'gmail',
4386
+ 'outlook.live.com': 'outlook',
4387
+ 'outlook.office.com': 'outlook',
4388
+ };
4389
+ /**
4390
+ * Auto-detect source from referrer URL domain.
4391
+ * Maps common domains to friendly source names.
4392
+ *
4393
+ * @param referrer - Full referrer URL
4394
+ * @returns Normalized source name or undefined
4395
+ *
4396
+ * @example
4397
+ * detectSourceFromReferrer('https://www.google.com/search?q=test') // 'google'
4398
+ * detectSourceFromReferrer('https://l.facebook.com/...') // 'facebook'
4399
+ * detectSourceFromReferrer('https://partner.example.com') // 'example'
4400
+ */
4401
+ function detectSourceFromReferrer(referrer) {
4402
+ if (!referrer)
4403
+ return undefined;
4404
+ try {
4405
+ const url = new URL(referrer);
4406
+ const hostname = url.hostname.toLowerCase().replace(/^www\./, '');
4407
+ // Check exact match first
4408
+ if (KNOWN_SOURCE_DOMAINS[hostname]) {
4409
+ return KNOWN_SOURCE_DOMAINS[hostname];
4410
+ }
4411
+ // Check if hostname ends with known domain (for subdomains)
4412
+ for (const [domain, source] of Object.entries(KNOWN_SOURCE_DOMAINS)) {
4413
+ if (hostname.endsWith('.' + domain)) {
4414
+ return source;
4415
+ }
4416
+ }
4417
+ // Fallback: extract main domain name (e.g., 'example' from 'mail.example.com')
4418
+ const parts = hostname.split('.');
4419
+ if (parts.length >= 2) {
4420
+ return parts[parts.length - 2]; // Returns 'example' from 'sub.example.com'
4421
+ }
4422
+ return hostname;
4423
+ }
4424
+ catch {
4425
+ // Invalid URL, return undefined
4426
+ return undefined;
4427
+ }
4428
+ }
4429
+ /**
4430
+ * Auto-detect channel from User-Agent string.
4431
+ *
4432
+ * @param userAgent - User-Agent header value
4433
+ * @param path - Optional request path for webhook detection
4434
+ * @returns Detected channel
4435
+ *
4436
+ * @example
4437
+ * detectChannelFromUserAgent('Mozilla/5.0 (iPhone; ...)') // 'mobile'
4438
+ * detectChannelFromUserAgent('Mozilla/5.0 (Windows NT...)') // 'web'
4439
+ * detectChannelFromUserAgent('', '/webhook/stripe') // 'webhook'
4440
+ */
4441
+ function detectChannelFromUserAgent(userAgent, path) {
4442
+ // Check for webhook paths
4443
+ if (path?.includes('/webhook')) {
4444
+ return 'webhook';
4445
+ }
4446
+ const ua = (userAgent || '').toLowerCase();
4447
+ // Check for mobile user-agents
4448
+ if (/mobile|android|iphone|ipad|ipod|blackberry|windows phone/i.test(ua)) {
4449
+ return 'mobile';
4450
+ }
4451
+ // Check for bot/crawler patterns (system)
4452
+ if (/bot|crawler|spider|scraper|curl|wget|python|node-fetch|axios/i.test(ua)) {
4453
+ return 'api';
4454
+ }
4455
+ // Default to web for browser-like user agents
4456
+ return 'web';
4457
+ }
4458
+ /**
4459
+ * Parse UTM parameters from a URL (deep link, referrer, etc.).
4460
+ * Supports standard Google Analytics UTM parameters.
4461
+ *
4462
+ * @param url - Full URL containing UTM query parameters
4463
+ * @returns Parsed UTM parameters object
4464
+ *
4465
+ * @example
4466
+ * // Deep link from email campaign
4467
+ * parseUtmParams('myapp://screen?utm_source=mailchimp&utm_medium=email&utm_campaign=summer_2026')
4468
+ * // Returns: { source: 'mailchimp', medium: 'email', campaign: 'summer_2026' }
4469
+ *
4470
+ * @example
4471
+ * // Web URL with full UTM tracking
4472
+ * parseUtmParams('https://app.example.com?utm_source=google&utm_medium=cpc&utm_campaign=brand&utm_term=loyalty+app')
4473
+ * // Returns: { source: 'google', medium: 'cpc', campaign: 'brand', term: 'loyalty+app' }
4474
+ */
4475
+ function parseUtmParams(url) {
4476
+ const params = {};
4477
+ try {
4478
+ const urlObj = new URL(url);
4479
+ const searchParams = urlObj.searchParams;
4480
+ const source = searchParams.get('utm_source');
4481
+ const medium = searchParams.get('utm_medium');
4482
+ const campaign = searchParams.get('utm_campaign');
4483
+ const term = searchParams.get('utm_term');
4484
+ const content = searchParams.get('utm_content');
4485
+ if (source)
4486
+ params.source = source;
4487
+ if (medium)
4488
+ params.medium = medium;
4489
+ if (campaign)
4490
+ params.campaign = campaign;
4491
+ if (term)
4492
+ params.term = term;
4493
+ if (content)
4494
+ params.content = content;
4495
+ }
4496
+ catch {
4497
+ // Invalid URL, return empty params
4498
+ }
4499
+ return params;
4500
+ }
4262
4501
 
4263
4502
  /**
4264
4503
  * Analytics Constants
@@ -4513,6 +4752,8 @@ const CommonErrorCodes = {
4513
4752
  REDEMPTION_ALREADY_PROCESSING: 'REDEMPTION_ALREADY_PROCESSING',
4514
4753
  MAX_REDEMPTION: 'MAX_REDEMPTION',
4515
4754
  USER_LIMIT: 'USER_LIMIT',
4755
+ REQUIRED_FIELDS_MISSING: 'REQUIRED_FIELDS_MISSING',
4756
+ BOOKING_REQUIREMENT_NOT_MET: 'BOOKING_REQUIREMENT_NOT_MET',
4516
4757
  // Authentication errors
4517
4758
  INVALID_TOKEN: 'INVALID_TOKEN',
4518
4759
  TOKEN_EXPIRED: 'TOKEN_EXPIRED',
@@ -5488,6 +5729,40 @@ class UserApi {
5488
5729
  async restoreUser(identifier) {
5489
5730
  return this.apiClient.post(`${this.basePath}/${identifier}/restore`);
5490
5731
  }
5732
+ // ==========================================
5733
+ // CSV EXPORT
5734
+ // ==========================================
5735
+ /**
5736
+ * ADMIN: Export users to CSV
5737
+ *
5738
+ * Returns a CSV file with user data for the tenant.
5739
+ * Supports date range filtering.
5740
+ *
5741
+ * @param options - Export options (dateFrom, dateTo in ISO format)
5742
+ * @returns CSV blob
5743
+ *
5744
+ * @example
5745
+ * ```typescript
5746
+ * // Export all users
5747
+ * const blob = await sdk.users.exportCSV();
5748
+ *
5749
+ * // Export users created in date range
5750
+ * const blob = await sdk.users.exportCSV({
5751
+ * dateFrom: '2026-01-01',
5752
+ * dateTo: '2026-05-31'
5753
+ * });
5754
+ * ```
5755
+ */
5756
+ async exportCSV(options) {
5757
+ const params = new URLSearchParams();
5758
+ if (options?.dateFrom)
5759
+ params.append('dateFrom', options.dateFrom);
5760
+ if (options?.dateTo)
5761
+ params.append('dateTo', options.dateTo);
5762
+ const queryString = params.toString();
5763
+ const url = `${this.basePath}/export/csv${queryString ? `?${queryString}` : ''}`;
5764
+ return this.apiClient.get(url, 'blob');
5765
+ }
5491
5766
  }
5492
5767
 
5493
5768
  /**
@@ -7395,6 +7670,47 @@ class CampaignApi {
7395
7670
  async removeTriggerSourceFromCampaign(campaignId, triggerSourceId) {
7396
7671
  return this.apiClient.delete(`/campaigns/${campaignId}/trigger-sources/${triggerSourceId}`);
7397
7672
  }
7673
+ // ==========================================
7674
+ // CSV EXPORT
7675
+ // ==========================================
7676
+ /**
7677
+ * ADMIN: Export campaigns to CSV
7678
+ *
7679
+ * Returns a CSV file with campaign data for the tenant.
7680
+ * Supports date range filtering.
7681
+ *
7682
+ * @param options - Export options (dateFrom, dateTo)
7683
+ * @returns CSV blob
7684
+ */
7685
+ async exportCSV(options) {
7686
+ const params = new URLSearchParams();
7687
+ if (options?.dateFrom)
7688
+ params.append('dateFrom', options.dateFrom);
7689
+ if (options?.dateTo)
7690
+ params.append('dateTo', options.dateTo);
7691
+ const queryString = params.toString();
7692
+ const url = `/campaigns/export/csv${queryString ? `?${queryString}` : ''}`;
7693
+ return this.apiClient.get(url, 'blob');
7694
+ }
7695
+ /**
7696
+ * ADMIN: Export campaign claims to CSV
7697
+ *
7698
+ * Returns a CSV file with all campaign claim data.
7699
+ * Supports date range filtering.
7700
+ *
7701
+ * @param options - Export options (dateFrom, dateTo)
7702
+ * @returns CSV blob
7703
+ */
7704
+ async exportClaimsCSV(options) {
7705
+ const params = new URLSearchParams();
7706
+ if (options?.dateFrom)
7707
+ params.append('dateFrom', options.dateFrom);
7708
+ if (options?.dateTo)
7709
+ params.append('dateTo', options.dateTo);
7710
+ const queryString = params.toString();
7711
+ const url = `/campaigns/claims/export/csv${queryString ? `?${queryString}` : ''}`;
7712
+ return this.apiClient.get(url, 'blob');
7713
+ }
7398
7714
  }
7399
7715
 
7400
7716
  /**
@@ -7902,6 +8218,47 @@ class RedemptionApi {
7902
8218
  const result = await this.getRedemptions({ active: true });
7903
8219
  return [...result.data];
7904
8220
  }
8221
+ // ==========================================
8222
+ // CSV EXPORT
8223
+ // ==========================================
8224
+ /**
8225
+ * ADMIN: Export redemptions to CSV
8226
+ *
8227
+ * Returns a CSV file with redemption data for the tenant.
8228
+ * Supports date range filtering.
8229
+ *
8230
+ * @param options - Export options (dateFrom, dateTo)
8231
+ * @returns CSV blob
8232
+ */
8233
+ async exportCSV(options) {
8234
+ const params = new URLSearchParams();
8235
+ if (options?.dateFrom)
8236
+ params.append('dateFrom', options.dateFrom);
8237
+ if (options?.dateTo)
8238
+ params.append('dateTo', options.dateTo);
8239
+ const queryString = params.toString();
8240
+ const url = `${this.basePath}/export/csv${queryString ? `?${queryString}` : ''}`;
8241
+ return this.apiClient.get(url, 'blob');
8242
+ }
8243
+ /**
8244
+ * ADMIN: Export redemption redeems to CSV
8245
+ *
8246
+ * Returns a CSV file with all redemption redeem data.
8247
+ * Supports date range filtering.
8248
+ *
8249
+ * @param options - Export options (dateFrom, dateTo)
8250
+ * @returns CSV blob
8251
+ */
8252
+ async exportRedeemsCSV(options) {
8253
+ const params = new URLSearchParams();
8254
+ if (options?.dateFrom)
8255
+ params.append('dateFrom', options.dateFrom);
8256
+ if (options?.dateTo)
8257
+ params.append('dateTo', options.dateTo);
8258
+ const queryString = params.toString();
8259
+ const url = `${this.basePath}/redeems/export/csv${queryString ? `?${queryString}` : ''}`;
8260
+ return this.apiClient.get(url, 'blob');
8261
+ }
7905
8262
  }
7906
8263
 
7907
8264
  /**
@@ -8106,7 +8463,7 @@ class TransactionApi {
8106
8463
  // AUTHENTICATED USER OPERATIONS
8107
8464
  // ==========================================
8108
8465
  /**
8109
- * Helper to build query params from TransactionQueryOptions
8466
+ * Helper to build query params from TransactionQueryParams
8110
8467
  * Handles all filter options and include parameter
8111
8468
  */
8112
8469
  buildTransactionQueryParams(options) {
@@ -8163,6 +8520,11 @@ class TransactionApi {
8163
8520
  params.append('type', options.type);
8164
8521
  }
8165
8522
  }
8523
+ // Date range filters
8524
+ if (options.dateFrom)
8525
+ params.append('dateFrom', options.dateFrom);
8526
+ if (options.dateTo)
8527
+ params.append('dateTo', options.dateTo);
8166
8528
  // Include relations for entity enrichment
8167
8529
  if (options.include && options.include.length > 0) {
8168
8530
  options.include.forEach(relation => params.append('include', relation));
@@ -8272,10 +8634,27 @@ class TransactionApi {
8272
8634
  /**
8273
8635
  * ADMIN: Export transactions to CSV
8274
8636
  *
8275
- * UPDATED: /transaction/admin/export/csv → /transactions/export/csv
8637
+ * Returns a CSV file with transaction data for the tenant.
8638
+ * Supports date range filtering.
8639
+ *
8640
+ * @param options - Export options (dateFrom, dateTo)
8641
+ * @returns CSV blob
8642
+ */
8643
+ async exportCSV(options) {
8644
+ const params = new URLSearchParams();
8645
+ if (options?.dateFrom)
8646
+ params.append('dateFrom', options.dateFrom);
8647
+ if (options?.dateTo)
8648
+ params.append('dateTo', options.dateTo);
8649
+ const queryString = params.toString();
8650
+ const url = `${this.basePath}/export/csv${queryString ? `?${queryString}` : ''}`;
8651
+ return this.apiClient.get(url, 'blob');
8652
+ }
8653
+ /**
8654
+ * @deprecated Use exportCSV() instead
8276
8655
  */
8277
8656
  async exportTransactionsCSV() {
8278
- return this.apiClient.get(`${this.basePath}/export/csv`, 'blob');
8657
+ return this.exportCSV();
8279
8658
  }
8280
8659
  // ==========================================
8281
8660
  // NEW ADMIN QUERY METHODS
@@ -8446,8 +8825,8 @@ class TransactionService {
8446
8825
  /**
8447
8826
  * ADMIN: Export transactions to CSV
8448
8827
  */
8449
- async exportTransactionsCSV() {
8450
- return this.transactionApi.exportTransactionsCSV();
8828
+ async exportTransactionsCSV(options) {
8829
+ return this.transactionApi.exportCSV(options);
8451
8830
  }
8452
8831
  // ==========================================
8453
8832
  // QUERY & ANALYTICS OPERATIONS
@@ -11696,10 +12075,139 @@ class DefaultAuthProvider {
11696
12075
  /** SDK package name */
11697
12076
  const SDK_NAME = "@explorins/pers-sdk";
11698
12077
  /** SDK version - injected from package.json at build time */
11699
- const SDK_VERSION = "2.2.0-alpha.2";
12078
+ const SDK_VERSION = "2.3.3";
11700
12079
  /** Full SDK identifier for headers */
11701
12080
  const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
11702
12081
 
12082
+ /**
12083
+ * Tracking Manager - Handles data source and platform tracking
12084
+ *
12085
+ * Manages attribution data for analytics across all user actions.
12086
+ *
12087
+ * @module @explorins/pers-sdk/core/tracking
12088
+ */
12089
+ /**
12090
+ * Manages data source and platform tracking state
12091
+ */
12092
+ class TrackingManager {
12093
+ constructor(options) {
12094
+ this.configDataSource = options?.dataSource;
12095
+ this.configPlatform = options?.platform;
12096
+ }
12097
+ /**
12098
+ * Set data source tracking info (replaces existing)
12099
+ *
12100
+ * @example
12101
+ * ```typescript
12102
+ * tracking.setDataSource({
12103
+ * channel: 'mobile',
12104
+ * medium: 'push',
12105
+ * campaign: 'summer_2026',
12106
+ * });
12107
+ * ```
12108
+ */
12109
+ setDataSource(dataSource) {
12110
+ this._dataSource = dataSource;
12111
+ }
12112
+ /**
12113
+ * Update data source tracking info (merges with existing)
12114
+ *
12115
+ * @example
12116
+ * ```typescript
12117
+ * tracking.updateDataSource({
12118
+ * campaign: 'new_campaign',
12119
+ * });
12120
+ * ```
12121
+ */
12122
+ updateDataSource(dataSource) {
12123
+ const existing = this._dataSource ?? this.configDataSource;
12124
+ this._dataSource = { ...existing, ...dataSource };
12125
+ }
12126
+ /**
12127
+ * Set platform info for analytics
12128
+ *
12129
+ * @example
12130
+ * ```typescript
12131
+ * tracking.setPlatform({
12132
+ * os: 'iOS',
12133
+ * osVersion: '17.4',
12134
+ * app: 'MyApp',
12135
+ * appVersion: '2.3.1',
12136
+ * deviceType: 'phone',
12137
+ * });
12138
+ * ```
12139
+ */
12140
+ setPlatform(platform) {
12141
+ this._platform = platform;
12142
+ }
12143
+ /**
12144
+ * Get current data source config
12145
+ */
12146
+ getDataSource() {
12147
+ return this._dataSource ?? this.configDataSource;
12148
+ }
12149
+ /**
12150
+ * Get current platform info
12151
+ */
12152
+ getPlatform() {
12153
+ return this._platform ?? this.configPlatform;
12154
+ }
12155
+ /**
12156
+ * Check if tracking is properly configured
12157
+ *
12158
+ * Returns warnings for missing recommended tracking data.
12159
+ * Useful for development/debugging to ensure proper attribution.
12160
+ */
12161
+ validate() {
12162
+ const warnings = [];
12163
+ const dataSource = this.getDataSource();
12164
+ const platform = this.getPlatform();
12165
+ // Check channel for non-web contexts
12166
+ if (!dataSource?.channel) {
12167
+ warnings.push('No channel set. Native apps should set channel: "mobile"');
12168
+ }
12169
+ // Check platform info
12170
+ if (!platform || Object.keys(platform).length === 0) {
12171
+ warnings.push('No platform info set. Consider calling setPlatform() for better analytics');
12172
+ }
12173
+ return {
12174
+ isValid: warnings.length === 0,
12175
+ warnings,
12176
+ };
12177
+ }
12178
+ /**
12179
+ * Build HTTP headers for data source tracking
12180
+ *
12181
+ * @internal - Used by PersApiClient
12182
+ */
12183
+ buildHeaders() {
12184
+ const headers = {};
12185
+ const dataSource = this.getDataSource();
12186
+ const platform = this.getPlatform();
12187
+ if (dataSource) {
12188
+ if (dataSource.channel) {
12189
+ headers['x-source-channel'] = dataSource.channel;
12190
+ }
12191
+ if (dataSource.medium) {
12192
+ headers['x-source-medium'] = dataSource.medium;
12193
+ }
12194
+ if (dataSource.campaign) {
12195
+ headers['x-source-campaign'] = dataSource.campaign;
12196
+ }
12197
+ if (dataSource.source) {
12198
+ headers['x-source'] = dataSource.source;
12199
+ }
12200
+ if (dataSource.referrer) {
12201
+ headers['Referer'] = dataSource.referrer;
12202
+ }
12203
+ }
12204
+ if (platform && Object.keys(platform).length > 0) {
12205
+ headers['x-platform'] = JSON.stringify(platform);
12206
+ }
12207
+ return headers;
12208
+ }
12209
+ }
12210
+
11703
12211
  // packages/pers-sdk/src/core/pers-api-client.ts
11704
12212
  /**
11705
12213
  * PERS API Client - Platform-agnostic HTTP client with authentication
@@ -11748,6 +12256,11 @@ class PersApiClient {
11748
12256
  }
11749
12257
  this.authService = new AuthService(this.authApi, this.mergedConfig.authProvider);
11750
12258
  this.refreshManager = new TokenRefreshManager(this.authService, this.mergedConfig.authProvider);
12259
+ // Initialize tracking manager
12260
+ this.tracking = new TrackingManager({
12261
+ dataSource: this.mergedConfig.dataSource,
12262
+ platform: this.mergedConfig.platform,
12263
+ });
11751
12264
  if (this.mergedConfig.authProvider) {
11752
12265
  this.initializationPromise = this.initialize();
11753
12266
  }
@@ -12032,24 +12545,8 @@ class PersApiClient {
12032
12545
  else if (this.mergedConfig.apiProjectKey) {
12033
12546
  headers['x-project-key'] = this.mergedConfig.apiProjectKey;
12034
12547
  }
12035
- // Add data source tracking headers
12036
- const dataSource = this.mergedConfig.dataSource;
12037
- if (dataSource) {
12038
- headers['x-source-channel'] = dataSource.channel;
12039
- if (dataSource.medium) {
12040
- headers['x-source-medium'] = dataSource.medium;
12041
- }
12042
- if (dataSource.campaign) {
12043
- headers['x-source-campaign'] = dataSource.campaign;
12044
- }
12045
- if (dataSource.source) {
12046
- headers['x-source'] = dataSource.source;
12047
- }
12048
- // Note: referrer is typically set by browser, but can be overridden
12049
- if (dataSource.referrer) {
12050
- headers['Referer'] = dataSource.referrer;
12051
- }
12052
- }
12548
+ // Add tracking headers (data source + platform)
12549
+ Object.assign(headers, this.tracking.buildHeaders());
12053
12550
  return headers;
12054
12551
  }
12055
12552
  // ==========================================
@@ -12073,6 +12570,33 @@ class PersApiClient {
12073
12570
  getOriginalConfig() {
12074
12571
  return this.config;
12075
12572
  }
12573
+ // ==========================================
12574
+ // DATA SOURCE TRACKING
12575
+ // ==========================================
12576
+ /** Set data source tracking info (replaces existing) */
12577
+ setDataSource(dataSource) {
12578
+ this.tracking.setDataSource(dataSource);
12579
+ }
12580
+ /** Update data source tracking info (merges with existing) */
12581
+ updateDataSource(dataSource) {
12582
+ this.tracking.updateDataSource(dataSource);
12583
+ }
12584
+ /** Set platform info for analytics */
12585
+ setPlatform(platform) {
12586
+ this.tracking.setPlatform(platform);
12587
+ }
12588
+ /** Get current data source config */
12589
+ getDataSource() {
12590
+ return this.tracking.getDataSource();
12591
+ }
12592
+ /** Get current platform info */
12593
+ getPlatform() {
12594
+ return this.tracking.getPlatform();
12595
+ }
12596
+ /** Validate tracking configuration (for debugging) */
12597
+ validateTracking() {
12598
+ return this.tracking.validate();
12599
+ }
12076
12600
  }
12077
12601
 
12078
12602
  /**
@@ -13291,6 +13815,37 @@ class UserManager {
13291
13815
  });
13292
13816
  return result;
13293
13817
  }
13818
+ /**
13819
+ * Admin: Export users as CSV
13820
+ *
13821
+ * Generates a comprehensive CSV export of all tenant users for
13822
+ * external analysis, reporting, or compliance purposes. This operation
13823
+ * requires administrator privileges and creates a downloadable file.
13824
+ *
13825
+ * @param options - Optional date range filters
13826
+ * @param options.dateFrom - Start date filter (ISO date string)
13827
+ * @param options.dateTo - End date filter (ISO date string)
13828
+ * @returns Promise resolving to CSV blob for download
13829
+ * @throws {PersApiError} When not authenticated as administrator or export fails
13830
+ *
13831
+ * @example Export All Users
13832
+ * ```typescript
13833
+ * const csvBlob = await sdk.users.exportCSV();
13834
+ * const downloadUrl = URL.createObjectURL(csvBlob);
13835
+ * ```
13836
+ *
13837
+ * @example Export Users Within Date Range
13838
+ * ```typescript
13839
+ * const csvBlob = await sdk.users.exportCSV({
13840
+ * dateFrom: '2024-01-01',
13841
+ * dateTo: '2024-12-31'
13842
+ * });
13843
+ * ```
13844
+ */
13845
+ async exportCSV(options) {
13846
+ const userApi = new UserApi(this.apiClient);
13847
+ return userApi.exportCSV(options);
13848
+ }
13294
13849
  /**
13295
13850
  * Get the user service for advanced operations
13296
13851
  *
@@ -14753,7 +15308,13 @@ class CampaignManager {
14753
15308
  * ```
14754
15309
  */
14755
15310
  async getCampaigns(options) {
14756
- return this.campaignService.getCampaigns(options);
15311
+ // Convert Date objects to ISO strings for API compatibility
15312
+ const normalizedOptions = options ? {
15313
+ ...options,
15314
+ startDate: options.startDate instanceof Date ? options.startDate.toISOString() : options.startDate,
15315
+ endDate: options.endDate instanceof Date ? options.endDate.toISOString() : options.endDate,
15316
+ } : undefined;
15317
+ return this.campaignService.getCampaigns(normalizedOptions);
14757
15318
  }
14758
15319
  /**
14759
15320
  * Admin: Create new campaign
@@ -15402,6 +15963,60 @@ class CampaignManager {
15402
15963
  async removeTriggerSource(campaignId, triggerSourceId) {
15403
15964
  return this.campaignService.removeTriggerSourceFromCampaign(campaignId, triggerSourceId);
15404
15965
  }
15966
+ /**
15967
+ * Admin: Export campaigns as CSV
15968
+ *
15969
+ * Generates a comprehensive CSV export of all tenant campaigns for
15970
+ * external analysis, reporting, or compliance purposes. This operation
15971
+ * requires administrator privileges and creates a downloadable file.
15972
+ *
15973
+ * @param options - Optional date range filters
15974
+ * @param options.dateFrom - Start date filter (ISO date string)
15975
+ * @param options.dateTo - End date filter (ISO date string)
15976
+ * @returns Promise resolving to CSV blob for download
15977
+ * @throws {PersApiError} When not authenticated as administrator or export fails
15978
+ *
15979
+ * @example Export All Campaigns
15980
+ * ```typescript
15981
+ * const csvBlob = await sdk.campaigns.exportCSV();
15982
+ * const downloadUrl = URL.createObjectURL(csvBlob);
15983
+ * ```
15984
+ *
15985
+ * @example Export Campaigns Within Date Range
15986
+ * ```typescript
15987
+ * const csvBlob = await sdk.campaigns.exportCSV({
15988
+ * dateFrom: '2024-01-01',
15989
+ * dateTo: '2024-12-31'
15990
+ * });
15991
+ * ```
15992
+ */
15993
+ async exportCSV(options) {
15994
+ const campaignApi = new CampaignApi(this.apiClient);
15995
+ return campaignApi.exportCSV(options);
15996
+ }
15997
+ /**
15998
+ * Admin: Export campaign claims as CSV
15999
+ *
16000
+ * Generates a comprehensive CSV export of all campaign claim records for
16001
+ * external analysis, reporting, or compliance purposes. This operation
16002
+ * requires administrator privileges and creates a downloadable file.
16003
+ *
16004
+ * @param options - Optional date range filters
16005
+ * @param options.dateFrom - Start date filter (ISO date string)
16006
+ * @param options.dateTo - End date filter (ISO date string)
16007
+ * @returns Promise resolving to CSV blob for download
16008
+ * @throws {PersApiError} When not authenticated as administrator or export fails
16009
+ *
16010
+ * @example Export All Claims
16011
+ * ```typescript
16012
+ * const csvBlob = await sdk.campaigns.exportClaimsCSV();
16013
+ * const downloadUrl = URL.createObjectURL(csvBlob);
16014
+ * ```
16015
+ */
16016
+ async exportClaimsCSV(options) {
16017
+ const campaignApi = new CampaignApi(this.apiClient);
16018
+ return campaignApi.exportClaimsCSV(options);
16019
+ }
15405
16020
  /**
15406
16021
  * Get the full campaign service for advanced operations
15407
16022
  *
@@ -16208,6 +16823,60 @@ class RedemptionManager {
16208
16823
  async deleteRedemption(id) {
16209
16824
  return this.redemptionService.deleteRedemption(id);
16210
16825
  }
16826
+ /**
16827
+ * Admin: Export redemptions as CSV
16828
+ *
16829
+ * Generates a comprehensive CSV export of all tenant redemptions for
16830
+ * external analysis, reporting, or compliance purposes. This operation
16831
+ * requires administrator privileges and creates a downloadable file.
16832
+ *
16833
+ * @param options - Optional date range filters
16834
+ * @param options.dateFrom - Start date filter (ISO date string)
16835
+ * @param options.dateTo - End date filter (ISO date string)
16836
+ * @returns Promise resolving to CSV blob for download
16837
+ * @throws {PersApiError} When not authenticated as administrator or export fails
16838
+ *
16839
+ * @example Export All Redemptions
16840
+ * ```typescript
16841
+ * const csvBlob = await sdk.redemptions.exportCSV();
16842
+ * const downloadUrl = URL.createObjectURL(csvBlob);
16843
+ * ```
16844
+ *
16845
+ * @example Export Redemptions Within Date Range
16846
+ * ```typescript
16847
+ * const csvBlob = await sdk.redemptions.exportCSV({
16848
+ * dateFrom: '2024-01-01',
16849
+ * dateTo: '2024-12-31'
16850
+ * });
16851
+ * ```
16852
+ */
16853
+ async exportCSV(options) {
16854
+ const redemptionApi = new RedemptionApi(this.apiClient);
16855
+ return redemptionApi.exportCSV(options);
16856
+ }
16857
+ /**
16858
+ * Admin: Export redemption redeems as CSV
16859
+ *
16860
+ * Generates a comprehensive CSV export of all redemption redeem records for
16861
+ * external analysis, reporting, or compliance purposes. This operation
16862
+ * requires administrator privileges and creates a downloadable file.
16863
+ *
16864
+ * @param options - Optional date range filters
16865
+ * @param options.dateFrom - Start date filter (ISO date string)
16866
+ * @param options.dateTo - End date filter (ISO date string)
16867
+ * @returns Promise resolving to CSV blob for download
16868
+ * @throws {PersApiError} When not authenticated as administrator or export fails
16869
+ *
16870
+ * @example Export All Redeems
16871
+ * ```typescript
16872
+ * const csvBlob = await sdk.redemptions.exportRedeemsCSV();
16873
+ * const downloadUrl = URL.createObjectURL(csvBlob);
16874
+ * ```
16875
+ */
16876
+ async exportRedeemsCSV(options) {
16877
+ const redemptionApi = new RedemptionApi(this.apiClient);
16878
+ return redemptionApi.exportRedeemsCSV(options);
16879
+ }
16211
16880
  /**
16212
16881
  * Get the full redemption service for advanced operations
16213
16882
  *
@@ -16601,54 +17270,36 @@ class TransactionManager {
16601
17270
  * external analysis, reporting, or compliance purposes. This operation
16602
17271
  * requires administrator privileges and creates a downloadable file.
16603
17272
  *
17273
+ * @param options - Optional date range filters
17274
+ * @param options.dateFrom - Start date filter (ISO date string)
17275
+ * @param options.dateTo - End date filter (ISO date string)
16604
17276
  * @returns Promise resolving to CSV blob for download
16605
17277
  * @throws {PersApiError} When not authenticated as administrator or export fails
16606
17278
  *
16607
- * @example
17279
+ * @example Export All Transactions
16608
17280
  * ```typescript
16609
- * // Admin operation - export transaction data
16610
- * try {
16611
- * const csvBlob = await sdk.transactions.exportTransactionsCSV();
16612
- *
16613
- * // Create download link
16614
- * const downloadUrl = URL.createObjectURL(csvBlob);
16615
- * const link = document.createElement('a');
16616
- * link.href = downloadUrl;
16617
- * link.download = `transactions-${new Date().toISOString().split('T')[0]}.csv`;
16618
- *
16619
- * // Trigger download
16620
- * document.body.appendChild(link);
16621
- * link.click();
16622
- * document.body.removeChild(link);
16623
- *
16624
- * // Clean up object URL
16625
- * URL.revokeObjectURL(downloadUrl);
16626
- *
16627
- * console.log('Transaction export downloaded successfully');
16628
- *
16629
- * } catch (error) {
16630
- * console.log('Export failed:', error.message);
16631
- * }
17281
+ * const csvBlob = await sdk.transactions.exportCSV();
17282
+ * const downloadUrl = URL.createObjectURL(csvBlob);
16632
17283
  * ```
16633
17284
  *
16634
- * @example Server-side Export (Node.js)
17285
+ * @example Export Transactions Within Date Range
16635
17286
  * ```typescript
16636
- * // Admin operation - server-side CSV export
16637
- * const csvBlob = await sdk.transactions.exportTransactionsCSV();
16638
- *
16639
- * // Convert blob to buffer for Node.js
16640
- * const buffer = Buffer.from(await csvBlob.arrayBuffer());
16641
- *
16642
- * // Save to file system
16643
- * const fs = require('fs');
16644
- * const filename = `transactions-export-${Date.now()}.csv`;
16645
- * fs.writeFileSync(filename, buffer);
16646
- *
16647
- * console.log(`Transaction data exported to ${filename}`);
17287
+ * const csvBlob = await sdk.transactions.exportCSV({
17288
+ * dateFrom: '2024-01-01',
17289
+ * dateTo: '2024-12-31'
17290
+ * });
16648
17291
  * ```
16649
17292
  */
16650
- async exportTransactionsCSV() {
16651
- return this.transactionService.exportTransactionsCSV();
17293
+ async exportCSV(options) {
17294
+ return this.transactionService.exportTransactionsCSV(options);
17295
+ }
17296
+ /**
17297
+ * Admin: Export transactions as CSV
17298
+ *
17299
+ * @deprecated Use `exportCSV()` instead for consistent naming across all managers
17300
+ */
17301
+ async exportTransactionsCSV(options) {
17302
+ return this.exportCSV(options);
16652
17303
  }
16653
17304
  /**
16654
17305
  * Prepare existing transaction for client-side signing
@@ -18854,10 +19505,10 @@ class WebhookApi {
18854
19505
  params.append('hookId', options.webhookId);
18855
19506
  if (options?.status)
18856
19507
  params.append('status', options.status);
18857
- if (options?.fromDate)
18858
- params.append('dateFrom', options.fromDate);
18859
- if (options?.toDate)
18860
- params.append('dateTo', options.toDate);
19508
+ if (options?.dateFrom)
19509
+ params.append('dateFrom', options.dateFrom);
19510
+ if (options?.dateTo)
19511
+ params.append('dateTo', options.dateTo);
18861
19512
  if (options?.page)
18862
19513
  params.append('page', String(options.page));
18863
19514
  if (options?.limit)
@@ -19346,7 +19997,7 @@ class WebhookManager {
19346
19997
  * // Get executions for specific webhook
19347
19998
  * const executions = await sdk.webhooks.getExecutions({
19348
19999
  * webhookId: 'webhook-123',
19349
- * fromDate: '2024-01-01'
20000
+ * dateFrom: '2024-01-01'
19350
20001
  * });
19351
20002
  * ```
19352
20003
  */
@@ -20729,6 +21380,28 @@ class BookingApi {
20729
21380
  async delete(id) {
20730
21381
  return this.apiClient.delete(`${this.basePath}/${id}`);
20731
21382
  }
21383
+ // ==========================================
21384
+ // CSV EXPORT
21385
+ // ==========================================
21386
+ /**
21387
+ * ADMIN: Export bookings to CSV
21388
+ *
21389
+ * Returns a CSV file with booking data for the tenant.
21390
+ * Supports date range filtering.
21391
+ *
21392
+ * @param options - Export options (dateFrom, dateTo in ISO format)
21393
+ * @returns CSV blob
21394
+ */
21395
+ async exportCSV(options) {
21396
+ const params = new URLSearchParams();
21397
+ if (options?.dateFrom)
21398
+ params.append('dateFrom', options.dateFrom);
21399
+ if (options?.dateTo)
21400
+ params.append('dateTo', options.dateTo);
21401
+ const queryString = params.toString();
21402
+ const url = `${this.basePath}/export/csv${queryString ? `?${queryString}` : ''}`;
21403
+ return this.apiClient.get(url, 'blob');
21404
+ }
20732
21405
  }
20733
21406
 
20734
21407
  /**
@@ -21181,6 +21854,28 @@ class PersSDK {
21181
21854
  this.setupWalletEventsAutoConnect();
21182
21855
  // Auto-restore session from stored tokens (if enabled, default: true)
21183
21856
  this.setupAutoRestoreSession();
21857
+ // Dev-mode tracking validation (helps developers ensure proper attribution)
21858
+ this.validateTrackingInDev();
21859
+ }
21860
+ /**
21861
+ * Validate tracking configuration in development mode
21862
+ * @internal
21863
+ */
21864
+ validateTrackingInDev() {
21865
+ // Only run in development (check common env vars)
21866
+ const isDev = (typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'development') ||
21867
+ (typeof window !== 'undefined' && window.__DEV__);
21868
+ if (!isDev)
21869
+ return;
21870
+ // Delay check to allow time for setDataSource/setPlatform calls
21871
+ setTimeout(() => {
21872
+ const result = this.apiClient.validateTracking();
21873
+ if (!result.isValid) {
21874
+ console.warn('[PERS SDK] Tracking configuration warnings:');
21875
+ result.warnings.forEach(w => console.warn(` ⚠️ ${w}`));
21876
+ console.warn('[PERS SDK] Call sdk.setDataSource() and sdk.setPlatform() for better analytics.');
21877
+ }
21878
+ }, 2000);
21184
21879
  }
21185
21880
  /**
21186
21881
  * Setup automatic session restoration from stored tokens
@@ -21881,6 +22576,74 @@ class PersSDK {
21881
22576
  // Reset manager so it picks up new config
21882
22577
  this._walletEvents = undefined;
21883
22578
  }
22579
+ // ==========================================
22580
+ // DATA SOURCE TRACKING
22581
+ // ==========================================
22582
+ /**
22583
+ * Set data source tracking for analytics attribution
22584
+ *
22585
+ * Use this to track where users and actions originate from.
22586
+ * Web apps typically only need medium/campaign/source (channel is auto-detected).
22587
+ * Native apps must set channel: 'mobile'.
22588
+ *
22589
+ * @param dataSource - Data source configuration
22590
+ *
22591
+ * @example Web app with UTM params
22592
+ * ```typescript
22593
+ * sdk.setDataSource({
22594
+ * medium: 'email',
22595
+ * campaign: 'summer_2026',
22596
+ * source: 'mailchimp',
22597
+ * });
22598
+ * ```
22599
+ *
22600
+ * @example Native app
22601
+ * ```typescript
22602
+ * sdk.setDataSource({
22603
+ * channel: 'mobile',
22604
+ * medium: 'push',
22605
+ * campaign: 'retention_30d',
22606
+ * });
22607
+ * ```
22608
+ */
22609
+ setDataSource(dataSource) {
22610
+ this.apiClient.setDataSource(dataSource);
22611
+ }
22612
+ /**
22613
+ * Update data source (merges with existing values)
22614
+ *
22615
+ * @param dataSource - Partial data source to merge
22616
+ *
22617
+ * @example Update campaign after deep link
22618
+ * ```typescript
22619
+ * sdk.updateDataSource({ campaign: 'new_campaign' });
22620
+ * ```
22621
+ */
22622
+ updateDataSource(dataSource) {
22623
+ this.apiClient.updateDataSource(dataSource);
22624
+ }
22625
+ /**
22626
+ * Set platform info for device analytics
22627
+ *
22628
+ * Native apps should call this on initialization with device info.
22629
+ * Web apps can optionally send browser/OS info.
22630
+ *
22631
+ * @param platform - Platform information
22632
+ *
22633
+ * @example Native app
22634
+ * ```typescript
22635
+ * sdk.setPlatform({
22636
+ * os: 'iOS',
22637
+ * osVersion: '17.4',
22638
+ * app: 'MyApp',
22639
+ * appVersion: '2.3.1',
22640
+ * deviceType: 'phone',
22641
+ * });
22642
+ * ```
22643
+ */
22644
+ setPlatform(platform) {
22645
+ this.apiClient.setPlatform(platform);
22646
+ }
21884
22647
  /**
21885
22648
  * Gets the API client for direct PERS API requests
21886
22649
  *
@@ -21970,6 +22733,108 @@ function warnIfProblematicEnvironment(feature) {
21970
22733
  }
21971
22734
  }
21972
22735
 
22736
+ /**
22737
+ * DataSource Tracking - SDK Extensions
22738
+ *
22739
+ * Re-exports from @explorins/pers-shared with SDK-specific utilities.
22740
+ *
22741
+ * Most types and utilities come from pers-shared:
22742
+ * - DataSource, DataSourceChannel
22743
+ * - Platform, UtmParams, WebDataSourceConfig, NativeDataSourceConfig
22744
+ * - parseUtmParams(), detectSourceFromReferrer(), KNOWN_SOURCE_DOMAINS
22745
+ *
22746
+ * SDK adds:
22747
+ * - detectWebPlatform() - Browser User-Agent detection (requires DOM)
22748
+ *
22749
+ * @module @explorins/pers-sdk/data-source
22750
+ */
22751
+ /**
22752
+ * Detect web platform from User-Agent (browser environments only)
22753
+ *
22754
+ * This uses `navigator.userAgent` which is only available in browsers.
22755
+ * For Node.js or React Native, use platform-specific detection.
22756
+ *
22757
+ * @example
22758
+ * ```typescript
22759
+ * const platform = detectWebPlatform();
22760
+ * sdk.setPlatform(platform);
22761
+ * // { os: 'macOS', browser: 'Chrome', browserVersion: '125.0', deviceType: 'desktop' }
22762
+ * ```
22763
+ */
22764
+ function detectWebPlatform() {
22765
+ if (typeof navigator === 'undefined') {
22766
+ return {};
22767
+ }
22768
+ const ua = navigator.userAgent;
22769
+ const platform = {};
22770
+ if (!ua)
22771
+ return platform;
22772
+ // Device type detection
22773
+ if (/mobile|android|iphone|ipod/i.test(ua)) {
22774
+ platform.deviceType = 'phone';
22775
+ }
22776
+ else if (/ipad|tablet/i.test(ua)) {
22777
+ platform.deviceType = 'tablet';
22778
+ }
22779
+ else {
22780
+ platform.deviceType = 'desktop';
22781
+ }
22782
+ // OS detection
22783
+ if (/windows/i.test(ua)) {
22784
+ platform.os = 'Windows';
22785
+ const match = ua.match(/Windows NT ([\d.]+)/);
22786
+ if (match)
22787
+ platform.osVersion = match[1];
22788
+ }
22789
+ else if (/macintosh|mac os/i.test(ua) && !/iphone|ipad|ipod/i.test(ua)) {
22790
+ platform.os = 'macOS';
22791
+ const match = ua.match(/Mac OS X ([\d_.]+)/);
22792
+ if (match)
22793
+ platform.osVersion = match[1].replace(/_/g, '.');
22794
+ }
22795
+ else if (/linux/i.test(ua) && !/android/i.test(ua)) {
22796
+ platform.os = 'Linux';
22797
+ }
22798
+ else if (/iphone|ipad|ipod/i.test(ua)) {
22799
+ platform.os = 'iOS';
22800
+ const match = ua.match(/OS ([\d_]+)/);
22801
+ if (match)
22802
+ platform.osVersion = match[1].replace(/_/g, '.');
22803
+ }
22804
+ else if (/android/i.test(ua)) {
22805
+ platform.os = 'Android';
22806
+ const match = ua.match(/Android ([\d.]+)/);
22807
+ if (match)
22808
+ platform.osVersion = match[1];
22809
+ }
22810
+ // Browser detection
22811
+ if (/edg\//i.test(ua)) {
22812
+ platform.browser = 'Edge';
22813
+ const match = ua.match(/Edg\/([\d.]+)/);
22814
+ if (match)
22815
+ platform.browserVersion = match[1];
22816
+ }
22817
+ else if (/chrome/i.test(ua) && !/edg/i.test(ua)) {
22818
+ platform.browser = 'Chrome';
22819
+ const match = ua.match(/Chrome\/([\d.]+)/);
22820
+ if (match)
22821
+ platform.browserVersion = match[1];
22822
+ }
22823
+ else if (/safari/i.test(ua) && !/chrome/i.test(ua)) {
22824
+ platform.browser = 'Safari';
22825
+ const match = ua.match(/Version\/([\d.]+)/);
22826
+ if (match)
22827
+ platform.browserVersion = match[1];
22828
+ }
22829
+ else if (/firefox/i.test(ua)) {
22830
+ platform.browser = 'Firefox';
22831
+ const match = ua.match(/Firefox\/([\d.]+)/);
22832
+ if (match)
22833
+ platform.browserVersion = match[1];
22834
+ }
22835
+ return platform;
22836
+ }
22837
+
21973
22838
  /**
21974
22839
  * AsyncStorage Token Storage for React Native Mobile Platforms
21975
22840
  *
@@ -22607,6 +23472,62 @@ class ReactNativeHttpClient {
22607
23472
  }
22608
23473
  }
22609
23474
 
23475
+ /**
23476
+ * React Native Platform Detection Utilities
23477
+ *
23478
+ * Provides automatic platform info detection for React Native apps.
23479
+ *
23480
+ * @module @explorins/pers-sdk-react-native/utils
23481
+ */
23482
+ // DeviceInfo is optional - dynamically loaded if available
23483
+ let DeviceInfo = null;
23484
+ try {
23485
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
23486
+ DeviceInfo = require('react-native-device-info').default;
23487
+ }
23488
+ catch {
23489
+ // react-native-device-info not installed - will use basic detection
23490
+ }
23491
+ /**
23492
+ * Detect platform info for React Native apps
23493
+ *
23494
+ * Automatically gathers OS, version, app info, and device type.
23495
+ * Uses react-native-device-info for enhanced detection if installed.
23496
+ *
23497
+ * @example
23498
+ * ```typescript
23499
+ * import { detectReactNativePlatform } from '@explorins/pers-sdk-react-native';
23500
+ *
23501
+ * // Get platform info
23502
+ * const platform = await detectReactNativePlatform();
23503
+ * // { os: 'iOS', osVersion: '17.4', app: 'MyApp', appVersion: '2.3.1', deviceType: 'phone' }
23504
+ *
23505
+ * // Set in SDK
23506
+ * sdk.setPlatform(platform);
23507
+ * ```
23508
+ */
23509
+ async function detectReactNativePlatform() {
23510
+ const platform = {
23511
+ os: reactNative.Platform.OS === 'ios' ? 'iOS' : reactNative.Platform.OS === 'android' ? 'Android' : reactNative.Platform.OS,
23512
+ osVersion: reactNative.Platform.Version?.toString(),
23513
+ deviceType: 'phone', // Default fallback
23514
+ };
23515
+ if (DeviceInfo) {
23516
+ try {
23517
+ // App info
23518
+ platform.app = await DeviceInfo.getApplicationName();
23519
+ platform.appVersion = DeviceInfo.getVersion();
23520
+ // Device type detection
23521
+ const isTablet = await DeviceInfo.isTablet();
23522
+ platform.deviceType = isTablet ? 'tablet' : 'phone';
23523
+ }
23524
+ catch (error) {
23525
+ console.debug('[Platform] DeviceInfo error, using basic detection');
23526
+ }
23527
+ }
23528
+ return platform;
23529
+ }
23530
+
22610
23531
  // Create the context
22611
23532
  const SDKContext = react.createContext(null);
22612
23533
  /**
@@ -22694,6 +23615,22 @@ const PersSDKProvider = ({ children, config }) => {
22694
23615
  };
22695
23616
  // Initialize PersSDK with platform-aware auth provider
22696
23617
  const sdkInstance = new PersSDK(httpClient, sdkConfig);
23618
+ // Auto-configure data source and platform for React Native
23619
+ // Native apps MUST set channel as backend cannot auto-detect without browser User-Agent
23620
+ if (reactNative.Platform.OS !== 'web') {
23621
+ sdkInstance.setDataSource({
23622
+ channel: 'mobile',
23623
+ source: config.dataSource?.source || 'app',
23624
+ medium: config.dataSource?.medium,
23625
+ campaign: config.dataSource?.campaign,
23626
+ });
23627
+ // Auto-detect platform info asynchronously
23628
+ detectReactNativePlatform().then(platformInfo => {
23629
+ sdkInstance.setPlatform(platformInfo);
23630
+ }).catch(error => {
23631
+ console.debug('[PersSDK] Platform detection failed:', error);
23632
+ });
23633
+ }
22697
23634
  // Subscribe to auth events BEFORE exposing SDK to consumers
22698
23635
  // This ensures no events are missed due to React timing
22699
23636
  eventUnsubscribeRef.current = sdkInstance.events.subscribe((event) => {
@@ -47477,7 +48414,13 @@ const useRedemptions = () => {
47477
48414
  throw new Error('SDK not initialized. Call initialize() first.');
47478
48415
  }
47479
48416
  try {
47480
- const result = await sdk.redemptions.getRedemptions(options);
48417
+ // Convert Date to ISO string for API compatibility
48418
+ const apiOptions = options ? {
48419
+ ...options,
48420
+ startDate: options.startDate instanceof Date ? options.startDate.toISOString() : options.startDate,
48421
+ endDate: options.endDate instanceof Date ? options.endDate.toISOString() : options.endDate,
48422
+ } : undefined;
48423
+ const result = await sdk.redemptions.getRedemptions(apiOptions);
47481
48424
  return result;
47482
48425
  }
47483
48426
  catch (error) {
@@ -49447,6 +50390,7 @@ exports.IMAGE_CDN_MAX_INPUT_SIZE = IMAGE_CDN_MAX_INPUT_SIZE;
49447
50390
  exports.IMAGE_CDN_PRESETS = IMAGE_CDN_PRESETS;
49448
50391
  exports.IMAGE_CDN_SUPPORTED_FORMATS = IMAGE_CDN_SUPPORTED_FORMATS;
49449
50392
  exports.IndexedDBTokenStorage = IndexedDBTokenStorage;
50393
+ exports.KNOWN_SOURCE_DOMAINS = KNOWN_SOURCE_DOMAINS;
49450
50394
  exports.LOCATION_TYPES = LOCATION_TYPES;
49451
50395
  exports.LocalStorageTokenStorage = LocalStorageTokenStorage;
49452
50396
  exports.LogoutRequired = LogoutRequired;
@@ -49476,6 +50420,7 @@ exports.RedemptionService = RedemptionService;
49476
50420
  exports.SDK_NAME = SDK_NAME;
49477
50421
  exports.SDK_USER_AGENT = SDK_USER_AGENT;
49478
50422
  exports.SDK_VERSION = SDK_VERSION;
50423
+ exports.SELECT_OPTIONS_LABEL_DEFAULTS = SELECT_OPTIONS_LABEL_DEFAULTS;
49479
50424
  exports.SOURCE_LOGIC_TYPES = SOURCE_LOGIC_TYPES;
49480
50425
  exports.SOURCE_LOGIC_TYPE_VALUES = SOURCE_LOGIC_TYPE_VALUES;
49481
50426
  exports.SdkErrorCodes = SdkErrorCodes;
@@ -49494,6 +50439,7 @@ exports.TokenRefreshNeeded = TokenRefreshNeeded;
49494
50439
  exports.TokenSDK = TokenSDK;
49495
50440
  exports.TokenService = TokenService;
49496
50441
  exports.TokenValidityTypes = TokenValidityTypes;
50442
+ exports.TrackingManager = TrackingManager;
49497
50443
  exports.TransactionApi = TransactionApi;
49498
50444
  exports.TransactionManager = TransactionManager;
49499
50445
  exports.TransactionService = TransactionService;
@@ -49545,8 +50491,12 @@ exports.createReactNativeAuthProvider = createReactNativeAuthProvider;
49545
50491
  exports.createUserIdentifierObject = createUserIdentifierObject;
49546
50492
  exports.createUserStatusSDK = createUserStatusSDK;
49547
50493
  exports.customFieldEntityTypes = customFieldEntityTypes;
50494
+ exports.detectChannelFromUserAgent = detectChannelFromUserAgent;
49548
50495
  exports.detectEnvironment = detectEnvironment;
49549
50496
  exports.detectIdentifierType = detectIdentifierType;
50497
+ exports.detectReactNativePlatform = detectReactNativePlatform;
50498
+ exports.detectSourceFromReferrer = detectSourceFromReferrer;
50499
+ exports.detectWebPlatform = detectWebPlatform;
49550
50500
  exports.environment = environment;
49551
50501
  exports.extractData = extractData;
49552
50502
  exports.extractDeadlineFromSigningData = extractDeadlineFromSigningData;
@@ -49558,10 +50508,12 @@ exports.getBuiltInFieldDefinition = getBuiltInFieldDefinition;
49558
50508
  exports.getMetadataFromTokenUnitResponse = getMetadataFromTokenUnitResponse;
49559
50509
  exports.getOperatorText = getOperatorText;
49560
50510
  exports.getPresetDimensions = getPresetDimensions;
50511
+ exports.getSelectOptionsLabelDefault = getSelectOptionsLabelDefault;
49561
50512
  exports.getSigningUrl = getSigningUrl;
49562
50513
  exports.globalCacheService = globalCacheService;
49563
50514
  exports.hasMinimumRole = hasMinimumRole;
49564
50515
  exports.initializeReactNativePolyfills = initializeReactNativePolyfills;
50516
+ exports.isBookingStatusValid = isBookingStatusValid;
49565
50517
  exports.isBuiltInUserField = isBuiltInUserField;
49566
50518
  exports.isConnectedMessage = isConnectedMessage;
49567
50519
  exports.isDataSourceChannel = isDataSourceChannel;
@@ -49598,6 +50550,7 @@ exports.needsSubmission = needsSubmission;
49598
50550
  exports.normalizeFilters = normalizeFilters;
49599
50551
  exports.normalizeToPaginated = normalizeToPaginated;
49600
50552
  exports.operatorText = operatorText;
50553
+ exports.parseUtmParams = parseUtmParams;
49601
50554
  exports.registerIdentifierType = registerIdentifierType;
49602
50555
  exports.selectOptionsSourceEntities = selectOptionsSourceEntities;
49603
50556
  exports.testnetPrefix = testnetPrefix;