@formo/analytics-react-native 1.1.0 → 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.
Files changed (65) hide show
  1. package/lib/commonjs/FormoAnalytics.js +75 -19
  2. package/lib/commonjs/FormoAnalytics.js.map +1 -1
  3. package/lib/commonjs/lib/event/EventFactory.js +9 -4
  4. package/lib/commonjs/lib/event/EventFactory.js.map +1 -1
  5. package/lib/commonjs/lib/event/EventManager.js +12 -4
  6. package/lib/commonjs/lib/event/EventManager.js.map +1 -1
  7. package/lib/commonjs/lib/event/EventQueue.js +5 -2
  8. package/lib/commonjs/lib/event/EventQueue.js.map +1 -1
  9. package/lib/commonjs/lib/installReferrer/index.js +16 -9
  10. package/lib/commonjs/lib/installReferrer/index.js.map +1 -1
  11. package/lib/commonjs/lib/wagmi/WagmiEventHandler.js +20 -0
  12. package/lib/commonjs/lib/wagmi/WagmiEventHandler.js.map +1 -1
  13. package/lib/commonjs/version.js +1 -1
  14. package/lib/module/FormoAnalytics.js +76 -20
  15. package/lib/module/FormoAnalytics.js.map +1 -1
  16. package/lib/module/lib/event/EventFactory.js +8 -3
  17. package/lib/module/lib/event/EventFactory.js.map +1 -1
  18. package/lib/module/lib/event/EventManager.js +13 -5
  19. package/lib/module/lib/event/EventManager.js.map +1 -1
  20. package/lib/module/lib/event/EventQueue.js +5 -2
  21. package/lib/module/lib/event/EventQueue.js.map +1 -1
  22. package/lib/module/lib/installReferrer/index.js +17 -9
  23. package/lib/module/lib/installReferrer/index.js.map +1 -1
  24. package/lib/module/lib/wagmi/WagmiEventHandler.js +20 -0
  25. package/lib/module/lib/wagmi/WagmiEventHandler.js.map +1 -1
  26. package/lib/module/version.js +1 -1
  27. package/lib/typescript/commonjs/FormoAnalytics.d.ts +4 -3
  28. package/lib/typescript/commonjs/FormoAnalytics.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/lib/event/EventFactory.d.ts +3 -1
  30. package/lib/typescript/commonjs/lib/event/EventFactory.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/lib/event/EventManager.d.ts +1 -1
  32. package/lib/typescript/commonjs/lib/event/EventManager.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/lib/event/EventQueue.d.ts +2 -1
  34. package/lib/typescript/commonjs/lib/event/EventQueue.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/lib/event/types.d.ts +2 -1
  36. package/lib/typescript/commonjs/lib/event/types.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/lib/installReferrer/index.d.ts +1 -0
  38. package/lib/typescript/commonjs/lib/installReferrer/index.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/lib/wagmi/WagmiEventHandler.d.ts +2 -0
  40. package/lib/typescript/commonjs/lib/wagmi/WagmiEventHandler.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/version.d.ts +1 -1
  42. package/lib/typescript/module/FormoAnalytics.d.ts +4 -3
  43. package/lib/typescript/module/FormoAnalytics.d.ts.map +1 -1
  44. package/lib/typescript/module/lib/event/EventFactory.d.ts +3 -1
  45. package/lib/typescript/module/lib/event/EventFactory.d.ts.map +1 -1
  46. package/lib/typescript/module/lib/event/EventManager.d.ts +1 -1
  47. package/lib/typescript/module/lib/event/EventManager.d.ts.map +1 -1
  48. package/lib/typescript/module/lib/event/EventQueue.d.ts +2 -1
  49. package/lib/typescript/module/lib/event/EventQueue.d.ts.map +1 -1
  50. package/lib/typescript/module/lib/event/types.d.ts +2 -1
  51. package/lib/typescript/module/lib/event/types.d.ts.map +1 -1
  52. package/lib/typescript/module/lib/installReferrer/index.d.ts +1 -0
  53. package/lib/typescript/module/lib/installReferrer/index.d.ts.map +1 -1
  54. package/lib/typescript/module/lib/wagmi/WagmiEventHandler.d.ts +2 -0
  55. package/lib/typescript/module/lib/wagmi/WagmiEventHandler.d.ts.map +1 -1
  56. package/lib/typescript/module/version.d.ts +1 -1
  57. package/package.json +1 -1
  58. package/src/FormoAnalytics.ts +99 -20
  59. package/src/lib/event/EventFactory.ts +10 -3
  60. package/src/lib/event/EventManager.ts +18 -5
  61. package/src/lib/event/EventQueue.ts +8 -3
  62. package/src/lib/event/types.ts +3 -1
  63. package/src/lib/installReferrer/index.ts +20 -10
  64. package/src/lib/wagmi/WagmiEventHandler.ts +24 -0
  65. package/src/version.ts +1 -1
@@ -40,7 +40,7 @@ import {
40
40
  TransactionStatus,
41
41
  } from "./types";
42
42
  import { validateAddress } from "./utils";
43
- import { parseTrafficSource, updateStoredTrafficSource } from "./utils/trafficSource";
43
+ import { clearTrafficSource, parseTrafficSource, updateStoredTrafficSource } from "./utils/trafficSource";
44
44
  import { captureInstallReferrer } from "./lib/installReferrer";
45
45
  import { Linking, EmitterSubscription } from "react-native";
46
46
 
@@ -60,6 +60,9 @@ export class FormoAnalytics implements IFormoAnalytics {
60
60
  private crashReporter?: CrashReporter;
61
61
  private initialDeepLinkUrl?: string;
62
62
  private linkingSubscription?: EmitterSubscription;
63
+ private walletGeneration = 0;
64
+ private chainGeneration = 0;
65
+ private consentGeneration = 0;
63
66
 
64
67
  config: Config;
65
68
  currentChainId?: ChainID;
@@ -86,7 +89,18 @@ export class FormoAnalytics implements IFormoAnalytics {
86
89
  this.detect = this.detect.bind(this);
87
90
  this.track = this.track.bind(this);
88
91
  this.screen = this.screen.bind(this);
92
+ this.reset = this.reset.bind(this);
93
+ this.cleanup = this.cleanup.bind(this);
94
+ this.flush = this.flush.bind(this);
95
+ this.setTrafficSourceFromUrl = this.setTrafficSourceFromUrl.bind(this);
96
+ this.optOutTracking = this.optOutTracking.bind(this);
97
+ this.optInTracking = this.optInTracking.bind(this);
98
+ this.hasOptedOutTracking = this.hasOptedOutTracking.bind(this);
99
+ this.pushNotificationReceived = this.pushNotificationReceived.bind(this);
100
+ this.pushNotificationTapped = this.pushNotificationTapped.bind(this);
101
+ this.pushNotificationBounced = this.pushNotificationBounced.bind(this);
89
102
  this.isAutocaptureEnabled = this.isAutocaptureEnabled.bind(this);
103
+ this.isAttributionEnabled = this.isAttributionEnabled.bind(this);
90
104
 
91
105
  // Initialize logger
92
106
  Logger.init({
@@ -104,10 +118,15 @@ export class FormoAnalytics implements IFormoAnalytics {
104
118
  });
105
119
 
106
120
  // Initialize event manager
107
- this.eventManager = new EventManager(this.eventQueue, options);
121
+ this.eventManager = new EventManager(
122
+ this.eventQueue,
123
+ options,
124
+ () => !this.hasOptedOutTracking()
125
+ );
108
126
 
109
127
  // Check consent status
110
128
  if (this.hasOptedOutTracking()) {
129
+ clearTrafficSource();
111
130
  logger.info("User has previously opted out of tracking");
112
131
  }
113
132
 
@@ -172,9 +191,13 @@ export class FormoAnalytics implements IFormoAnalytics {
172
191
 
173
192
  if (analytics.isAttributionEnabled("installReferrer")) {
174
193
  try {
194
+ const consentGeneration = analytics.consentGeneration;
175
195
  await captureInstallReferrer({
176
196
  customRefParams: analytics.options.referral?.queryParams,
177
197
  pathPattern: analytics.options.referral?.pathPattern,
198
+ canCapture: () =>
199
+ analytics.consentGeneration === consentGeneration &&
200
+ !analytics.hasOptedOutTracking(),
178
201
  });
179
202
  } catch (error) {
180
203
  logger.debug("FormoAnalytics: install referrer capture failed", error);
@@ -226,9 +249,14 @@ export class FormoAnalytics implements IFormoAnalytics {
226
249
  * so attribution is in storage before the first lifecycle event fires.
227
250
  */
228
251
  private async startDeepLinkCapture(): Promise<void> {
252
+ const consentGeneration = this.consentGeneration;
229
253
  try {
230
254
  const url = await Linking.getInitialURL();
231
- if (url) {
255
+ if (
256
+ url &&
257
+ this.consentGeneration === consentGeneration &&
258
+ !this.hasOptedOutTracking()
259
+ ) {
232
260
  if (this.isAttributionEnabled("deeplinks")) {
233
261
  this.setTrafficSourceFromUrl(url);
234
262
  }
@@ -243,7 +271,7 @@ export class FormoAnalytics implements IFormoAnalytics {
243
271
 
244
272
  // Runtime deep links (foreground opens, universal links).
245
273
  this.linkingSubscription = Linking.addEventListener("url", (event) => {
246
- if (!event?.url) return;
274
+ if (!event?.url || this.hasOptedOutTracking()) return;
247
275
  // Each behaviour checks its own flag: the hook may exist because only one
248
276
  // of them is enabled.
249
277
  if (this.isAttributionEnabled("deeplinks")) {
@@ -367,6 +395,7 @@ export class FormoAnalytics implements IFormoAnalytics {
367
395
  * ```
368
396
  */
369
397
  public setTrafficSourceFromUrl(url: string): void {
398
+ if (this.hasOptedOutTracking()) return;
370
399
  const trafficSource = parseTrafficSource(
371
400
  url,
372
401
  this.options.referral?.queryParams,
@@ -379,12 +408,14 @@ export class FormoAnalytics implements IFormoAnalytics {
379
408
  logger.debug("Traffic source set from URL:", trafficSource);
380
409
  }
381
410
 
382
- /**
383
- * Reset the current user session
384
- */
411
+ /** Reset user and wallet state, preserving device identity and attribution. */
385
412
  public reset(): void {
413
+ this.walletGeneration++;
414
+ this.chainGeneration++;
386
415
  this.currentUserId = undefined;
387
- storage().remove(LOCAL_ANONYMOUS_ID_KEY);
416
+ this.currentAddress = undefined;
417
+ this.currentChainId = undefined;
418
+ this.wagmiHandler?.clearIdentity();
388
419
  storage().remove(LOCAL_SESSION_ID_KEY);
389
420
  storage().remove(LOCAL_SESSION_LAST_ACTIVITY_KEY);
390
421
  storage().remove(SESSION_USER_ID_KEY);
@@ -447,6 +478,9 @@ export class FormoAnalytics implements IFormoAnalytics {
447
478
  logger.warn(`Connect: Invalid address provided ("${address}")`);
448
479
  return;
449
480
  }
481
+ if (this.hasOptedOutTracking()) return;
482
+ const walletGeneration = ++this.walletGeneration;
483
+ const chainGeneration = ++this.chainGeneration;
450
484
 
451
485
  // Track event before updating state so connect events TO excluded chains are tracked
452
486
  await this.trackEvent(
@@ -457,8 +491,14 @@ export class FormoAnalytics implements IFormoAnalytics {
457
491
  callback
458
492
  );
459
493
 
460
- this.currentChainId = chainId;
461
- this.currentAddress = validatedAddress;
494
+ if (!this.hasOptedOutTracking()) {
495
+ if (walletGeneration === this.walletGeneration) {
496
+ this.currentAddress = validatedAddress;
497
+ }
498
+ if (chainGeneration === this.chainGeneration) {
499
+ this.currentChainId = chainId;
500
+ }
501
+ }
462
502
  }
463
503
 
464
504
  /**
@@ -470,6 +510,8 @@ export class FormoAnalytics implements IFormoAnalytics {
470
510
  context?: IFormoEventContext,
471
511
  callback?: (...args: unknown[]) => void
472
512
  ): Promise<void> {
513
+ const walletGeneration = ++this.walletGeneration;
514
+ const chainGeneration = ++this.chainGeneration;
473
515
  const chainId = params?.chainId || this.currentChainId;
474
516
  const address = params?.address || this.currentAddress;
475
517
 
@@ -489,8 +531,8 @@ export class FormoAnalytics implements IFormoAnalytics {
489
531
  callback
490
532
  );
491
533
 
492
- this.currentAddress = undefined;
493
- this.currentChainId = undefined;
534
+ if (walletGeneration === this.walletGeneration) this.currentAddress = undefined;
535
+ if (chainGeneration === this.chainGeneration) this.currentChainId = undefined;
494
536
  }
495
537
 
496
538
  /**
@@ -510,22 +552,37 @@ export class FormoAnalytics implements IFormoAnalytics {
510
552
  logger.warn("FormoAnalytics::chain: chainId must be a valid number");
511
553
  return;
512
554
  }
513
- if (!address && !this.currentAddress) {
514
- logger.warn("FormoAnalytics::chain: address was empty and no previous address recorded");
555
+ const validAddress = address
556
+ ? this.validateAndChecksumAddress(address, chainId)
557
+ : this.currentAddress;
558
+ if (!validAddress) {
559
+ logger.warn("FormoAnalytics::chain: address is invalid or unavailable");
515
560
  return;
516
561
  }
562
+ if (this.hasOptedOutTracking()) return;
563
+ const walletGeneration = address
564
+ ? ++this.walletGeneration
565
+ : this.walletGeneration;
566
+ const chainGeneration = ++this.chainGeneration;
517
567
 
518
568
  // Track event before updating currentChainId so shouldTrack uses the previous chain
519
569
  // This ensures chain change events TO excluded chains are still tracked
520
570
  await this.trackEvent(
521
571
  EventType.CHAIN,
522
- { chainId, address: address || this.currentAddress },
572
+ { chainId, address: validAddress },
523
573
  properties,
524
574
  context,
525
575
  callback
526
576
  );
527
577
 
528
- this.currentChainId = chainId;
578
+ if (!this.hasOptedOutTracking()) {
579
+ if (chainGeneration === this.chainGeneration) {
580
+ this.currentChainId = chainId;
581
+ }
582
+ if (address && walletGeneration === this.walletGeneration) {
583
+ this.currentAddress = validAddress;
584
+ }
585
+ }
529
586
  }
530
587
 
531
588
  /**
@@ -638,6 +695,7 @@ export class FormoAnalytics implements IFormoAnalytics {
638
695
  try {
639
696
  const { userId, address, providerName, rdns } = params;
640
697
  logger.info("Identify", address, userId, providerName, rdns);
698
+ if (this.hasOptedOutTracking()) return;
641
699
 
642
700
  let validAddress: Address | undefined = undefined;
643
701
  if (address) {
@@ -646,10 +704,12 @@ export class FormoAnalytics implements IFormoAnalytics {
646
704
  logger.warn(`Identify: Invalid address provided ("${address}")`);
647
705
  return;
648
706
  }
707
+ this.walletGeneration++;
649
708
  this.currentAddress = validAddress;
650
709
  // Note: validateAddress returns Solana addresses unchanged (Base58, case-sensitive)
651
710
  // and EVM addresses checksummed.
652
711
  } else {
712
+ this.walletGeneration++;
653
713
  this.currentAddress = undefined;
654
714
  }
655
715
 
@@ -696,6 +756,7 @@ export class FormoAnalytics implements IFormoAnalytics {
696
756
  context?: IFormoEventContext,
697
757
  callback?: (...args: unknown[]) => void
698
758
  ): Promise<void> {
759
+ if (this.hasOptedOutTracking()) return;
699
760
  if (this.session.isWalletDetected(rdns)) {
700
761
  logger.warn(`Detect: Wallet ${providerName} already detected in this session`);
701
762
  return;
@@ -734,9 +795,14 @@ export class FormoAnalytics implements IFormoAnalytics {
734
795
  */
735
796
  public optOutTracking(): void {
736
797
  logger.info("Opting out of tracking");
798
+ this.consentGeneration++;
799
+ this.initialDeepLinkUrl = undefined;
737
800
  setConsentFlag(this.writeKey, CONSENT_OPT_OUT_KEY, "true");
738
801
  this.eventQueue.clear();
739
802
  this.reset();
803
+ // Consent withdrawal clears device identity and attribution too.
804
+ storage().remove(LOCAL_ANONYMOUS_ID_KEY);
805
+ clearTrafficSource();
740
806
  logger.info("Successfully opted out of tracking");
741
807
  }
742
808
 
@@ -842,7 +908,18 @@ export class FormoAnalytics implements IFormoAnalytics {
842
908
  callback?: (...args: unknown[]) => void
843
909
  ): Promise<void> {
844
910
  try {
845
- if (!this.shouldTrack()) {
911
+ const payloadChainId = payload?.chainId as ChainID | undefined;
912
+ const validPayloadChainId =
913
+ typeof payloadChainId === "number" &&
914
+ Number.isFinite(payloadChainId) &&
915
+ payloadChainId > 0
916
+ ? payloadChainId
917
+ : undefined;
918
+ const eventChainId =
919
+ type === EventType.CONNECT || type === EventType.CHAIN
920
+ ? this.currentChainId
921
+ : validPayloadChainId;
922
+ if (!this.shouldTrack(eventChainId)) {
846
923
  logger.info(`Skipping ${type} event due to tracking configuration`);
847
924
  return;
848
925
  }
@@ -873,7 +950,7 @@ export class FormoAnalytics implements IFormoAnalytics {
873
950
  /**
874
951
  * Check if tracking should be enabled
875
952
  */
876
- private shouldTrack(): boolean {
953
+ private shouldTrack(eventChainId?: ChainID): boolean {
877
954
  // Check consent
878
955
  if (this.hasOptedOutTracking()) {
879
956
  return false;
@@ -891,11 +968,13 @@ export class FormoAnalytics implements IFormoAnalytics {
891
968
  !Array.isArray(this.options.tracking)
892
969
  ) {
893
970
  const { excludeChains = [] } = this.options.tracking as TrackingOptions;
971
+ const chainId = eventChainId ?? this.currentChainId;
894
972
 
895
973
  if (
896
974
  excludeChains.length > 0 &&
897
- this.currentChainId &&
898
- excludeChains.includes(this.currentChainId)
975
+ chainId !== undefined &&
976
+ chainId !== null &&
977
+ excludeChains.includes(chainId)
899
978
  ) {
900
979
  return false;
901
980
  }
@@ -210,11 +210,13 @@ export function synthesizeUserAgent(info: {
210
210
  */
211
211
  class EventFactory implements IEventFactory {
212
212
  private options?: Options;
213
+ private canCreate: () => boolean;
213
214
  /** Memoised device/app identity — see getDeviceInfo(). */
214
215
  private deviceInfoPromise?: Promise<DeviceInfoResult>;
215
216
 
216
- constructor(options?: Options) {
217
+ constructor(options?: Options, canCreate: () => boolean = () => true) {
217
218
  this.options = options;
219
+ this.canCreate = canCreate;
218
220
  }
219
221
 
220
222
  /**
@@ -508,6 +510,10 @@ class EventFactory implements IEventFactory {
508
510
  formoEvent: Partial<IFormoEvent>,
509
511
  context?: IFormoEventContext
510
512
  ): Promise<IFormoEvent> {
513
+ if (!this.canCreate()) throw EVENT_CREATION_CANCELLED;
514
+
515
+ const anonymousId = generateAnonymousId(LOCAL_ANONYMOUS_ID_KEY);
516
+ const sessionId = getSessionId();
511
517
  const commonEventData: Partial<IFormoEvent> = {
512
518
  context: await this.generateContext(context),
513
519
  original_timestamp: getCurrentTimeFormatted(),
@@ -517,8 +523,8 @@ class EventFactory implements IEventFactory {
517
523
  version: VERSION,
518
524
  };
519
525
 
520
- commonEventData.anonymous_id = generateAnonymousId(LOCAL_ANONYMOUS_ID_KEY);
521
- commonEventData.session_id = getSessionId();
526
+ commonEventData.anonymous_id = anonymousId;
527
+ commonEventData.session_id = sessionId;
522
528
 
523
529
  // Handle address - convert undefined to null for consistency
524
530
  // Try EVM first, then Solana fallback (chainId is not always present here).
@@ -886,3 +892,4 @@ class EventFactory implements IEventFactory {
886
892
  }
887
893
 
888
894
  export { EventFactory };
895
+ export const EVENT_CREATION_CANCELLED = Symbol("event creation cancelled");
@@ -1,6 +1,6 @@
1
1
  import { Address, APIEvent, Options } from "../../types";
2
2
  import { logger } from "../logger";
3
- import { EventFactory } from "./EventFactory";
3
+ import { EVENT_CREATION_CANCELLED, EventFactory } from "./EventFactory";
4
4
  import { IEventFactory, IEventManager, IEventQueue } from "./types";
5
5
  import { isBlockedAddress } from "../../utils/address";
6
6
 
@@ -12,9 +12,13 @@ class EventManager implements IEventManager {
12
12
  eventQueue: IEventQueue;
13
13
  eventFactory: IEventFactory;
14
14
 
15
- constructor(eventQueue: IEventQueue, options?: Options) {
15
+ constructor(
16
+ eventQueue: IEventQueue,
17
+ options?: Options,
18
+ canCreate: () => boolean = () => true
19
+ ) {
16
20
  this.eventQueue = eventQueue;
17
- this.eventFactory = new EventFactory(options);
21
+ this.eventFactory = new EventFactory(options, canCreate);
18
22
  }
19
23
 
20
24
  /**
@@ -26,7 +30,16 @@ class EventManager implements IEventManager {
26
30
  userId?: string
27
31
  ): Promise<void> {
28
32
  const { callback, ..._event } = event;
29
- const formoEvent = await this.eventFactory.create(_event, address, userId);
33
+ const generation = this.eventQueue.getGeneration();
34
+ let formoEvent;
35
+ try {
36
+ formoEvent = await this.eventFactory.create(_event, address, userId);
37
+ } catch (error) {
38
+ if (error === EVENT_CREATION_CANCELLED) return;
39
+ throw error;
40
+ }
41
+
42
+ if (this.eventQueue.getGeneration() !== generation) return;
30
43
 
31
44
  // Check if the final event has a blocked address
32
45
  if (formoEvent.address && isBlockedAddress(formoEvent.address)) {
@@ -43,7 +56,7 @@ class EventManager implements IEventManager {
43
56
  logger.info(`Events sent successfully: ${(data as unknown[])?.length ?? 0} events`);
44
57
  }
45
58
  callback?.(err, _, data);
46
- });
59
+ }, generation);
47
60
  }
48
61
  }
49
62
 
@@ -210,16 +210,17 @@ export class EventQueue implements IEventQueue {
210
210
  */
211
211
  async enqueue(
212
212
  event: IFormoEvent,
213
- callback?: (...args: unknown[]) => void
213
+ callback?: (...args: unknown[]) => void,
214
+ generation = this.generation
214
215
  ): Promise<void> {
215
216
  if (this.closed) {
216
217
  logger.debug("EventQueue: Ignoring event enqueued after cleanup");
217
218
  return;
218
219
  }
219
220
 
220
- callback = callback || noop;
221
+ if (this.generation !== generation) return;
221
222
 
222
- const generation = this.generation;
223
+ callback = callback || noop;
223
224
  const message_id = await this.generateMessageId(event);
224
225
 
225
226
  // Hashing is async, so cleanup() can complete while this call is suspended
@@ -558,6 +559,10 @@ export class EventQueue implements IEventQueue {
558
559
  logger.debug("EventQueue: Cleared all pending events");
559
560
  }
560
561
 
562
+ public getGeneration(): number {
563
+ return this.generation;
564
+ }
565
+
561
566
  /**
562
567
  * Give up on the events still queued at teardown, and make sure a send that
563
568
  * is still holding a batch cannot put them back.
@@ -96,9 +96,11 @@ export interface IEventManager {
96
96
  }
97
97
 
98
98
  export interface IEventQueue {
99
+ getGeneration(): number;
99
100
  enqueue(
100
101
  event: IFormoEvent,
101
- callback?: (...args: unknown[]) => void
102
+ callback?: (...args: unknown[]) => void,
103
+ generation?: number
102
104
  ): Promise<void>;
103
105
  flush(callback?: (...args: unknown[]) => void): Promise<void>;
104
106
  clear(): void;
@@ -39,17 +39,23 @@ import {
39
39
  import type { ITrafficSource } from "../../types";
40
40
 
41
41
  // Lazy-load the optional native module. Absence is fine — attribution is best-effort.
42
- let PlayInstallReferrer: {
42
+ type PlayInstallReferrerModule = {
43
43
  getInstallReferrerInfo: (
44
44
  cb: (info: { installReferrer?: string } | null, error?: unknown) => void
45
45
  ) => void;
46
- } | null = null;
46
+ };
47
47
 
48
- try {
49
- PlayInstallReferrer = require("react-native-play-install-referrer")
50
- .PlayInstallReferrer;
51
- } catch {
52
- // Not installed — Android install referrer capture will no-op.
48
+ let playInstallReferrer: PlayInstallReferrerModule | null | undefined;
49
+
50
+ function getPlayInstallReferrer(): PlayInstallReferrerModule | null {
51
+ if (playInstallReferrer !== undefined) return playInstallReferrer;
52
+ try {
53
+ playInstallReferrer = require("react-native-play-install-referrer")
54
+ .PlayInstallReferrer ?? null;
55
+ } catch {
56
+ playInstallReferrer = null;
57
+ }
58
+ return playInstallReferrer ?? null;
53
59
  }
54
60
 
55
61
  /**
@@ -71,6 +77,7 @@ const INSTALL_REFERRER_TIMEOUT_MS = 1500;
71
77
  export interface CaptureOptions {
72
78
  customRefParams?: string[];
73
79
  pathPattern?: string;
80
+ canCapture?: () => boolean;
74
81
  }
75
82
 
76
83
  /**
@@ -81,6 +88,7 @@ export async function captureInstallReferrer(
81
88
  options: CaptureOptions = {}
82
89
  ): Promise<void> {
83
90
  try {
91
+ if (options.canCapture && !options.canCapture()) return;
84
92
  // The one-shot flag is only useful if it can persist across launches.
85
93
  // Without AsyncStorage (MemoryStorage fallback) the flag is lost every
86
94
  // restart, so we'd re-hit the native API every cold start. Mirror the
@@ -114,7 +122,7 @@ export async function captureInstallReferrer(
114
122
  return;
115
123
  }
116
124
 
117
- if (didResolve) {
125
+ if (didResolve && (!options.canCapture || options.canCapture())) {
118
126
  await storage().setAsync(LOCAL_INSTALL_REFERRER_RESOLVED_KEY, "true");
119
127
  }
120
128
  } catch (error) {
@@ -130,7 +138,8 @@ export async function captureInstallReferrer(
130
138
  async function captureAndroidReferrer(
131
139
  options: CaptureOptions
132
140
  ): Promise<boolean> {
133
- if (!PlayInstallReferrer) {
141
+ const referrer = getPlayInstallReferrer();
142
+ if (!referrer) {
134
143
  // Warn (not debug) on Android: attribution silently does nothing here, and
135
144
  // marking the peer optional suppresses the missing-peer install warning, so
136
145
  // this is the only actionable signal the integrator gets.
@@ -174,7 +183,7 @@ async function captureAndroidReferrer(
174
183
  }, INSTALL_REFERRER_TIMEOUT_MS);
175
184
 
176
185
  try {
177
- PlayInstallReferrer!.getInstallReferrerInfo((info, error) => {
186
+ referrer.getInstallReferrerInfo((info, error) => {
178
187
  if (error) {
179
188
  logger.debug("InstallReferrer: Play API error", error);
180
189
  finish({ ok: false, info: null });
@@ -189,6 +198,7 @@ async function captureAndroidReferrer(
189
198
  });
190
199
 
191
200
  if (!result.ok) return false; // errored — retry next launch
201
+ if (options.canCapture && !options.canCapture()) return false;
192
202
 
193
203
  const referrerQuery = result.info?.installReferrer;
194
204
  if (!referrerQuery) {
@@ -46,6 +46,7 @@ interface IFormoAnalyticsInstance {
46
46
  isAutocaptureEnabled(
47
47
  eventType: "connect" | "disconnect" | "signature" | "transaction" | "chain"
48
48
  ): boolean;
49
+ hasOptedOutTracking(): boolean;
49
50
  }
50
51
 
51
52
  export class WagmiEventHandler {
@@ -121,6 +122,12 @@ export class WagmiEventHandler {
121
122
  status: WagmiState["status"],
122
123
  prevStatus: WagmiState["status"]
123
124
  ): Promise<void> {
125
+ if (this.formo.hasOptedOutTracking()) {
126
+ this.clearIdentity();
127
+ this.trackingState.lastStatus = status;
128
+ return;
129
+ }
130
+
124
131
  if (this.trackingState.isProcessing) {
125
132
  // Limit queue size to prevent unbounded growth during rapid status changes
126
133
  if (this.pendingStatusChanges.length >= WagmiEventHandler.MAX_PENDING_STATUS_CHANGES) {
@@ -163,6 +170,12 @@ export class WagmiEventHandler {
163
170
  const address = this.getConnectedAddress(state);
164
171
  const chainId = state.chainId;
165
172
 
173
+ if (this.formo.hasOptedOutTracking()) {
174
+ this.clearIdentity();
175
+ this.trackingState.lastStatus = status;
176
+ return;
177
+ }
178
+
166
179
  logger.info("WagmiEventHandler: Status changed", {
167
180
  status,
168
181
  prevStatus,
@@ -216,6 +229,11 @@ export class WagmiEventHandler {
216
229
  chainId: number | undefined,
217
230
  prevChainId: number | undefined
218
231
  ): Promise<void> {
232
+ if (this.formo.hasOptedOutTracking()) {
233
+ this.clearIdentity();
234
+ return;
235
+ }
236
+
219
237
  // Skip if no change, chainId is undefined, or this is initial connection (prevChainId undefined)
220
238
  if (chainId === prevChainId || chainId === undefined || prevChainId === undefined) {
221
239
  return;
@@ -548,6 +566,12 @@ export class WagmiEventHandler {
548
566
  return connection?.connector.id;
549
567
  }
550
568
 
569
+ public clearIdentity(): void {
570
+ this.trackingState.lastAddress = undefined;
571
+ this.trackingState.lastChainId = undefined;
572
+ this.pendingStatusChanges = [];
573
+ }
574
+
551
575
  /**
552
576
  * Clean up subscriptions
553
577
  */
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // This file is auto-generated by scripts/update-version.js during npm version
2
2
  // Do not edit manually - it will be overwritten
3
- export const version = '1.1.0';
3
+ export const version = '1.2.0';