@tolinku/react-native-sdk 0.1.0 → 0.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.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { StyleSheet, AppState, Dimensions, Modal, Pressable, TouchableOpacity, Text, ScrollView, View, ImageBackground, Image, Linking } from 'react-native';
1
+ import { StyleSheet, AppState, Dimensions, Platform, PixelRatio, Modal, Pressable, TouchableOpacity, Text, ScrollView, View, ImageBackground, Image, Linking } from 'react-native';
2
2
  import AsyncStorage from '@react-native-async-storage/async-storage';
3
3
  import { useState, useRef, useMemo, useEffect, useCallback } from 'react';
4
4
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
@@ -347,6 +347,215 @@ var Analytics = class {
347
347
  }
348
348
  }
349
349
  };
350
+ var BATCH_SIZE2 = 10;
351
+ var FLUSH_INTERVAL_MS2 = 5e3;
352
+ var MAX_QUEUE_SIZE2 = 500;
353
+ var CART_ID_STORAGE_KEY = "tolinku_ecom_cart_id";
354
+ var Ecommerce = class {
355
+ constructor(client, getUserId) {
356
+ this.queue = [];
357
+ this.flushTimer = null;
358
+ this.appStateSubscription = null;
359
+ this.isFlushing = false;
360
+ this.memoryCartId = null;
361
+ this.handleAppStateChange = (state) => {
362
+ if (state === "background" || state === "inactive") {
363
+ this.flush().catch(() => {
364
+ });
365
+ }
366
+ };
367
+ this.client = client;
368
+ this.getUserId = getUserId;
369
+ this.appStateSubscription = AppState.addEventListener(
370
+ "change",
371
+ this.handleAppStateChange
372
+ );
373
+ }
374
+ // ─── Public methods (13 event types) ────────────────────
375
+ async viewItem(params) {
376
+ await this.enqueue({ event_type: "view_item", items: params.items });
377
+ }
378
+ async addToCart(params) {
379
+ const cartId = params.cart_id || await this.getOrCreateCartId();
380
+ await this.enqueue({ event_type: "add_to_cart", items: params.items, cart_id: cartId });
381
+ }
382
+ async removeFromCart(params) {
383
+ const cartId = params.cart_id || await this.getCartId();
384
+ await this.enqueue({ event_type: "remove_from_cart", items: params.items, cart_id: cartId });
385
+ }
386
+ async addToWishlist(params) {
387
+ await this.enqueue({ event_type: "add_to_wishlist", items: params.items });
388
+ }
389
+ async viewCart() {
390
+ const cartId = await this.getCartId();
391
+ await this.enqueue({ event_type: "view_cart", cart_id: cartId });
392
+ }
393
+ async addPaymentInfo(params) {
394
+ const cartId = params?.cart_id || await this.getCartId();
395
+ await this.enqueue({ event_type: "add_payment_info", cart_id: cartId });
396
+ }
397
+ async beginCheckout(params) {
398
+ const cartId = params.cart_id || await this.getCartId();
399
+ await this.enqueue({
400
+ event_type: "begin_checkout",
401
+ revenue: params.revenue,
402
+ currency: params.currency,
403
+ cart_id: cartId,
404
+ items: params.items
405
+ });
406
+ }
407
+ async purchase(params) {
408
+ const cartId = params.cart_id || await this.getCartId();
409
+ await this.enqueue({
410
+ event_type: "purchase",
411
+ transaction_id: params.transaction_id,
412
+ revenue: params.revenue,
413
+ currency: params.currency,
414
+ cart_id: cartId,
415
+ coupon_code: params.coupon_code,
416
+ discount: params.discount,
417
+ shipping: params.shipping,
418
+ tax: params.tax,
419
+ items: params.items
420
+ });
421
+ await this.clearCartId();
422
+ }
423
+ async refund(params) {
424
+ await this.enqueue({
425
+ event_type: "refund",
426
+ transaction_id: params.transaction_id,
427
+ revenue: params.revenue,
428
+ currency: params.currency,
429
+ items: params.items
430
+ });
431
+ }
432
+ async search(params) {
433
+ await this.enqueue({ event_type: "search", properties: { search_term: params.search_term } });
434
+ }
435
+ async share(params) {
436
+ const props = {};
437
+ if (params.item_id) props.item_id = params.item_id;
438
+ if (params.url) props.url = params.url;
439
+ if (params.method) props.method = params.method;
440
+ await this.enqueue({ event_type: "share", properties: props });
441
+ }
442
+ async rate(params) {
443
+ await this.enqueue({
444
+ event_type: "rate",
445
+ properties: {
446
+ item_id: params.item_id,
447
+ rating: String(params.rating),
448
+ ...params.max_rating != null ? { max_rating: String(params.max_rating) } : {}
449
+ }
450
+ });
451
+ }
452
+ async spendCredits(params) {
453
+ await this.enqueue({ event_type: "spend_credits", revenue: params.revenue, currency: params.currency });
454
+ }
455
+ // ─── Flush ─────────────────────────────────────────────
456
+ async flush() {
457
+ if (this.queue.length === 0 || this.isFlushing) return;
458
+ this.isFlushing = true;
459
+ const events = this.queue.splice(0);
460
+ this.cancelFlushTimer();
461
+ try {
462
+ debugLog(`Flushing ${events.length} ecommerce event(s)`);
463
+ const result = await this.client.post(
464
+ "/v1/api/analytics/ecommerce/batch",
465
+ { events }
466
+ );
467
+ if (result.errors && result.errors.length > 0) {
468
+ debugWarn(`Ecommerce batch partial failure: ${result.errors.join(", ")}`);
469
+ }
470
+ } catch (err) {
471
+ debugWarn(`Failed to flush ecommerce events: ${err.message}`);
472
+ const spaceLeft = MAX_QUEUE_SIZE2 - this.queue.length;
473
+ if (spaceLeft > 0) {
474
+ this.queue.unshift(...events.slice(0, spaceLeft));
475
+ }
476
+ } finally {
477
+ this.isFlushing = false;
478
+ }
479
+ }
480
+ async destroy() {
481
+ this.cancelFlushTimer();
482
+ this.appStateSubscription?.remove();
483
+ this.appStateSubscription = null;
484
+ try {
485
+ await this.flush();
486
+ } catch {
487
+ }
488
+ }
489
+ // ─── Private ───────────────────────────────────────────
490
+ async enqueue(event) {
491
+ const userId = this.getUserId();
492
+ if (userId) event.user_id = userId;
493
+ if (this.queue.length >= MAX_QUEUE_SIZE2) {
494
+ debugWarn(`Ecommerce queue full (${MAX_QUEUE_SIZE2}). Dropping oldest event.`);
495
+ this.queue.shift();
496
+ }
497
+ this.queue.push(event);
498
+ if (this.queue.length >= BATCH_SIZE2) {
499
+ await this.flush();
500
+ } else if (this.queue.length === 1) {
501
+ this.startFlushTimer();
502
+ }
503
+ }
504
+ startFlushTimer() {
505
+ this.cancelFlushTimer();
506
+ this.flushTimer = setTimeout(() => {
507
+ this.flush().catch((err) => {
508
+ debugWarn(`Ecommerce timer flush failed: ${err.message}`);
509
+ });
510
+ }, FLUSH_INTERVAL_MS2);
511
+ }
512
+ cancelFlushTimer() {
513
+ if (this.flushTimer !== null) {
514
+ clearTimeout(this.flushTimer);
515
+ this.flushTimer = null;
516
+ }
517
+ }
518
+ // ─── Cart ID lifecycle (AsyncStorage + memory fallback) ─
519
+ async getOrCreateCartId() {
520
+ const existing = await this.getCartId();
521
+ if (existing) return existing;
522
+ const cartId = this.generateId();
523
+ await this.setCartId(cartId);
524
+ return cartId;
525
+ }
526
+ async getCartId() {
527
+ try {
528
+ const stored = await AsyncStorage.getItem(CART_ID_STORAGE_KEY);
529
+ if (stored) return stored;
530
+ } catch {
531
+ }
532
+ return this.memoryCartId || void 0;
533
+ }
534
+ async setCartId(cartId) {
535
+ this.memoryCartId = cartId;
536
+ try {
537
+ await AsyncStorage.setItem(CART_ID_STORAGE_KEY, cartId);
538
+ } catch {
539
+ }
540
+ }
541
+ async clearCartId() {
542
+ this.memoryCartId = null;
543
+ try {
544
+ await AsyncStorage.removeItem(CART_ID_STORAGE_KEY);
545
+ } catch {
546
+ }
547
+ }
548
+ generateId() {
549
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
550
+ return crypto.randomUUID();
551
+ }
552
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
553
+ const r = Math.random() * 16 | 0;
554
+ const v = c === "x" ? r : r & 3 | 8;
555
+ return v.toString(16);
556
+ });
557
+ }
558
+ };
350
559
 
351
560
  // src/referrals.ts
352
561
  var Referrals = class {
@@ -439,15 +648,29 @@ var Deferred = class {
439
648
  try {
440
649
  const { width, height } = Dimensions.get("screen");
441
650
  const resolvedTimezone = options.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
442
- const resolvedLanguage = options.language || "en";
651
+ const resolvedLanguage = options.language || (typeof Intl !== "undefined" && typeof Intl.DateTimeFormat === "function" ? Intl.DateTimeFormat().resolvedOptions().locale : void 0) || "en";
443
652
  return await this.client.postPublic("/v1/api/deferred/claim-by-signals", {
444
653
  appspace_id: options.appspaceId,
445
654
  timezone: resolvedTimezone,
446
655
  language: resolvedLanguage,
447
656
  screen_width: options.screenWidth || width,
448
- screen_height: options.screenHeight || height
657
+ screen_height: options.screenHeight || height,
658
+ // Separates devices reporting identical dp dimensions.
659
+ device_pixel_ratio: options.devicePixelRatio || PixelRatio.get(),
660
+ os_version: options.osVersion || String(Platform.Version)
449
661
  });
450
662
  } catch (err) {
663
+ const status = err?.statusCode ?? err?.status;
664
+ if (status === 404) {
665
+ debugWarn("Deferred claimBySignals: no match for this device.");
666
+ return null;
667
+ }
668
+ if (status === 403) {
669
+ console.warn(
670
+ `[Tolinku] claimBySignals failed with HTTP 403. Check that appspaceId is your Appspace ID (copy it from the dashboard under Settings), not your subdomain or slug. ${err.message}`
671
+ );
672
+ return null;
673
+ }
451
674
  debugWarn(`Deferred claimBySignals failed: ${err.message}`);
452
675
  return null;
453
676
  }
@@ -562,6 +785,7 @@ var _Tolinku = class _Tolinku {
562
785
  setStorageNamespace(config.apiKey);
563
786
  _Tolinku.client = new HttpClient(resolvedConfig);
564
787
  _Tolinku.analyticsInstance = new Analytics(_Tolinku.client);
788
+ _Tolinku.ecommerceInstance = new Ecommerce(_Tolinku.client, () => _Tolinku._userId);
565
789
  _Tolinku.referralsInstance = new Referrals(_Tolinku.client);
566
790
  _Tolinku.deferredInstance = new Deferred(_Tolinku.client);
567
791
  _Tolinku._initialized = true;
@@ -602,13 +826,23 @@ var _Tolinku = class _Tolinku {
602
826
  return _Tolinku.analyticsInstance.track(eventType, mergedProps);
603
827
  }
604
828
  /**
605
- * Immediately flush all queued analytics events to the server.
829
+ * Immediately flush all queued analytics and ecommerce events to the server.
606
830
  */
607
831
  static async flush() {
608
832
  if (!_Tolinku.analyticsInstance) {
609
833
  throw new Error("Tolinku: SDK not initialized. Call Tolinku.init() first.");
610
834
  }
611
- return _Tolinku.analyticsInstance.flush();
835
+ await Promise.all([
836
+ _Tolinku.analyticsInstance.flush(),
837
+ _Tolinku.ecommerceInstance?.flush()
838
+ ]);
839
+ }
840
+ /** Ecommerce: track purchases, carts, products, revenue */
841
+ static get ecommerce() {
842
+ if (!_Tolinku.ecommerceInstance) {
843
+ throw new Error("Tolinku: SDK not initialized. Call Tolinku.init() first.");
844
+ }
845
+ return _Tolinku.ecommerceInstance;
612
846
  }
613
847
  /** Referrals: create, complete, milestone, leaderboard, claimReward */
614
848
  static get referrals() {
@@ -635,12 +869,16 @@ var _Tolinku = class _Tolinku {
635
869
  if (_Tolinku.analyticsInstance) {
636
870
  await _Tolinku.analyticsInstance.destroy();
637
871
  }
872
+ if (_Tolinku.ecommerceInstance) {
873
+ await _Tolinku.ecommerceInstance.destroy();
874
+ }
638
875
  if (_Tolinku.client) {
639
876
  _Tolinku.client.abort();
640
877
  }
641
878
  resetStorageNamespace();
642
879
  _Tolinku.client = null;
643
880
  _Tolinku.analyticsInstance = null;
881
+ _Tolinku.ecommerceInstance = null;
644
882
  _Tolinku.referralsInstance = null;
645
883
  _Tolinku.deferredInstance = null;
646
884
  _Tolinku._initialized = false;
@@ -651,6 +889,7 @@ var _Tolinku = class _Tolinku {
651
889
  _Tolinku.VERSION = SDK_VERSION;
652
890
  _Tolinku.client = null;
653
891
  _Tolinku.analyticsInstance = null;
892
+ _Tolinku.ecommerceInstance = null;
654
893
  _Tolinku.referralsInstance = null;
655
894
  _Tolinku.deferredInstance = null;
656
895
  _Tolinku._initialized = false;