@formo/analytics-react-native 1.1.1 → 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 +64 -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 +65 -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 +88 -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;
@@ -115,10 +118,15 @@ export class FormoAnalytics implements IFormoAnalytics {
115
118
  });
116
119
 
117
120
  // Initialize event manager
118
- this.eventManager = new EventManager(this.eventQueue, options);
121
+ this.eventManager = new EventManager(
122
+ this.eventQueue,
123
+ options,
124
+ () => !this.hasOptedOutTracking()
125
+ );
119
126
 
120
127
  // Check consent status
121
128
  if (this.hasOptedOutTracking()) {
129
+ clearTrafficSource();
122
130
  logger.info("User has previously opted out of tracking");
123
131
  }
124
132
 
@@ -183,9 +191,13 @@ export class FormoAnalytics implements IFormoAnalytics {
183
191
 
184
192
  if (analytics.isAttributionEnabled("installReferrer")) {
185
193
  try {
194
+ const consentGeneration = analytics.consentGeneration;
186
195
  await captureInstallReferrer({
187
196
  customRefParams: analytics.options.referral?.queryParams,
188
197
  pathPattern: analytics.options.referral?.pathPattern,
198
+ canCapture: () =>
199
+ analytics.consentGeneration === consentGeneration &&
200
+ !analytics.hasOptedOutTracking(),
189
201
  });
190
202
  } catch (error) {
191
203
  logger.debug("FormoAnalytics: install referrer capture failed", error);
@@ -237,9 +249,14 @@ export class FormoAnalytics implements IFormoAnalytics {
237
249
  * so attribution is in storage before the first lifecycle event fires.
238
250
  */
239
251
  private async startDeepLinkCapture(): Promise<void> {
252
+ const consentGeneration = this.consentGeneration;
240
253
  try {
241
254
  const url = await Linking.getInitialURL();
242
- if (url) {
255
+ if (
256
+ url &&
257
+ this.consentGeneration === consentGeneration &&
258
+ !this.hasOptedOutTracking()
259
+ ) {
243
260
  if (this.isAttributionEnabled("deeplinks")) {
244
261
  this.setTrafficSourceFromUrl(url);
245
262
  }
@@ -254,7 +271,7 @@ export class FormoAnalytics implements IFormoAnalytics {
254
271
 
255
272
  // Runtime deep links (foreground opens, universal links).
256
273
  this.linkingSubscription = Linking.addEventListener("url", (event) => {
257
- if (!event?.url) return;
274
+ if (!event?.url || this.hasOptedOutTracking()) return;
258
275
  // Each behaviour checks its own flag: the hook may exist because only one
259
276
  // of them is enabled.
260
277
  if (this.isAttributionEnabled("deeplinks")) {
@@ -378,6 +395,7 @@ export class FormoAnalytics implements IFormoAnalytics {
378
395
  * ```
379
396
  */
380
397
  public setTrafficSourceFromUrl(url: string): void {
398
+ if (this.hasOptedOutTracking()) return;
381
399
  const trafficSource = parseTrafficSource(
382
400
  url,
383
401
  this.options.referral?.queryParams,
@@ -390,12 +408,14 @@ export class FormoAnalytics implements IFormoAnalytics {
390
408
  logger.debug("Traffic source set from URL:", trafficSource);
391
409
  }
392
410
 
393
- /**
394
- * Reset the current user session
395
- */
411
+ /** Reset user and wallet state, preserving device identity and attribution. */
396
412
  public reset(): void {
413
+ this.walletGeneration++;
414
+ this.chainGeneration++;
397
415
  this.currentUserId = undefined;
398
- storage().remove(LOCAL_ANONYMOUS_ID_KEY);
416
+ this.currentAddress = undefined;
417
+ this.currentChainId = undefined;
418
+ this.wagmiHandler?.clearIdentity();
399
419
  storage().remove(LOCAL_SESSION_ID_KEY);
400
420
  storage().remove(LOCAL_SESSION_LAST_ACTIVITY_KEY);
401
421
  storage().remove(SESSION_USER_ID_KEY);
@@ -458,6 +478,9 @@ export class FormoAnalytics implements IFormoAnalytics {
458
478
  logger.warn(`Connect: Invalid address provided ("${address}")`);
459
479
  return;
460
480
  }
481
+ if (this.hasOptedOutTracking()) return;
482
+ const walletGeneration = ++this.walletGeneration;
483
+ const chainGeneration = ++this.chainGeneration;
461
484
 
462
485
  // Track event before updating state so connect events TO excluded chains are tracked
463
486
  await this.trackEvent(
@@ -468,8 +491,14 @@ export class FormoAnalytics implements IFormoAnalytics {
468
491
  callback
469
492
  );
470
493
 
471
- this.currentChainId = chainId;
472
- 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
+ }
473
502
  }
474
503
 
475
504
  /**
@@ -481,6 +510,8 @@ export class FormoAnalytics implements IFormoAnalytics {
481
510
  context?: IFormoEventContext,
482
511
  callback?: (...args: unknown[]) => void
483
512
  ): Promise<void> {
513
+ const walletGeneration = ++this.walletGeneration;
514
+ const chainGeneration = ++this.chainGeneration;
484
515
  const chainId = params?.chainId || this.currentChainId;
485
516
  const address = params?.address || this.currentAddress;
486
517
 
@@ -500,8 +531,8 @@ export class FormoAnalytics implements IFormoAnalytics {
500
531
  callback
501
532
  );
502
533
 
503
- this.currentAddress = undefined;
504
- this.currentChainId = undefined;
534
+ if (walletGeneration === this.walletGeneration) this.currentAddress = undefined;
535
+ if (chainGeneration === this.chainGeneration) this.currentChainId = undefined;
505
536
  }
506
537
 
507
538
  /**
@@ -521,22 +552,37 @@ export class FormoAnalytics implements IFormoAnalytics {
521
552
  logger.warn("FormoAnalytics::chain: chainId must be a valid number");
522
553
  return;
523
554
  }
524
- if (!address && !this.currentAddress) {
525
- 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");
526
560
  return;
527
561
  }
562
+ if (this.hasOptedOutTracking()) return;
563
+ const walletGeneration = address
564
+ ? ++this.walletGeneration
565
+ : this.walletGeneration;
566
+ const chainGeneration = ++this.chainGeneration;
528
567
 
529
568
  // Track event before updating currentChainId so shouldTrack uses the previous chain
530
569
  // This ensures chain change events TO excluded chains are still tracked
531
570
  await this.trackEvent(
532
571
  EventType.CHAIN,
533
- { chainId, address: address || this.currentAddress },
572
+ { chainId, address: validAddress },
534
573
  properties,
535
574
  context,
536
575
  callback
537
576
  );
538
577
 
539
- 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
+ }
540
586
  }
541
587
 
542
588
  /**
@@ -649,6 +695,7 @@ export class FormoAnalytics implements IFormoAnalytics {
649
695
  try {
650
696
  const { userId, address, providerName, rdns } = params;
651
697
  logger.info("Identify", address, userId, providerName, rdns);
698
+ if (this.hasOptedOutTracking()) return;
652
699
 
653
700
  let validAddress: Address | undefined = undefined;
654
701
  if (address) {
@@ -657,10 +704,12 @@ export class FormoAnalytics implements IFormoAnalytics {
657
704
  logger.warn(`Identify: Invalid address provided ("${address}")`);
658
705
  return;
659
706
  }
707
+ this.walletGeneration++;
660
708
  this.currentAddress = validAddress;
661
709
  // Note: validateAddress returns Solana addresses unchanged (Base58, case-sensitive)
662
710
  // and EVM addresses checksummed.
663
711
  } else {
712
+ this.walletGeneration++;
664
713
  this.currentAddress = undefined;
665
714
  }
666
715
 
@@ -707,6 +756,7 @@ export class FormoAnalytics implements IFormoAnalytics {
707
756
  context?: IFormoEventContext,
708
757
  callback?: (...args: unknown[]) => void
709
758
  ): Promise<void> {
759
+ if (this.hasOptedOutTracking()) return;
710
760
  if (this.session.isWalletDetected(rdns)) {
711
761
  logger.warn(`Detect: Wallet ${providerName} already detected in this session`);
712
762
  return;
@@ -745,9 +795,14 @@ export class FormoAnalytics implements IFormoAnalytics {
745
795
  */
746
796
  public optOutTracking(): void {
747
797
  logger.info("Opting out of tracking");
798
+ this.consentGeneration++;
799
+ this.initialDeepLinkUrl = undefined;
748
800
  setConsentFlag(this.writeKey, CONSENT_OPT_OUT_KEY, "true");
749
801
  this.eventQueue.clear();
750
802
  this.reset();
803
+ // Consent withdrawal clears device identity and attribution too.
804
+ storage().remove(LOCAL_ANONYMOUS_ID_KEY);
805
+ clearTrafficSource();
751
806
  logger.info("Successfully opted out of tracking");
752
807
  }
753
808
 
@@ -853,7 +908,18 @@ export class FormoAnalytics implements IFormoAnalytics {
853
908
  callback?: (...args: unknown[]) => void
854
909
  ): Promise<void> {
855
910
  try {
856
- 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)) {
857
923
  logger.info(`Skipping ${type} event due to tracking configuration`);
858
924
  return;
859
925
  }
@@ -884,7 +950,7 @@ export class FormoAnalytics implements IFormoAnalytics {
884
950
  /**
885
951
  * Check if tracking should be enabled
886
952
  */
887
- private shouldTrack(): boolean {
953
+ private shouldTrack(eventChainId?: ChainID): boolean {
888
954
  // Check consent
889
955
  if (this.hasOptedOutTracking()) {
890
956
  return false;
@@ -902,11 +968,13 @@ export class FormoAnalytics implements IFormoAnalytics {
902
968
  !Array.isArray(this.options.tracking)
903
969
  ) {
904
970
  const { excludeChains = [] } = this.options.tracking as TrackingOptions;
971
+ const chainId = eventChainId ?? this.currentChainId;
905
972
 
906
973
  if (
907
974
  excludeChains.length > 0 &&
908
- this.currentChainId &&
909
- excludeChains.includes(this.currentChainId)
975
+ chainId !== undefined &&
976
+ chainId !== null &&
977
+ excludeChains.includes(chainId)
910
978
  ) {
911
979
  return false;
912
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.1';
3
+ export const version = '1.2.0';