@formo/analytics-react-native 0.1.3 → 0.1.6
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/lib/commonjs/FormoAnalytics.js +91 -16
- package/lib/commonjs/FormoAnalytics.js.map +1 -1
- package/lib/commonjs/constants/events.js +1 -1
- package/lib/commonjs/constants/storage.js +4 -1
- package/lib/commonjs/constants/storage.js.map +1 -1
- package/lib/commonjs/lib/event/EventFactory.js +6 -13
- package/lib/commonjs/lib/event/EventFactory.js.map +1 -1
- package/lib/commonjs/lib/installReferrer/index.js +227 -0
- package/lib/commonjs/lib/installReferrer/index.js.map +1 -0
- package/lib/commonjs/lib/wagmi/WagmiEventHandler.js +1 -6
- package/lib/commonjs/lib/wagmi/WagmiEventHandler.js.map +1 -1
- package/lib/commonjs/solana/address.js +110 -0
- package/lib/commonjs/solana/address.js.map +1 -0
- package/lib/commonjs/solana/index.js +28 -0
- package/lib/commonjs/solana/index.js.map +1 -0
- package/lib/commonjs/solana/types.js +47 -0
- package/lib/commonjs/solana/types.js.map +1 -0
- package/lib/commonjs/types/events.js.map +1 -1
- package/lib/commonjs/utils/address.js +60 -6
- package/lib/commonjs/utils/address.js.map +1 -1
- package/lib/commonjs/utils/trafficSource.js +61 -0
- package/lib/commonjs/utils/trafficSource.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/FormoAnalytics.js +93 -18
- package/lib/module/FormoAnalytics.js.map +1 -1
- package/lib/module/constants/events.js +1 -1
- package/lib/module/constants/storage.js +3 -0
- package/lib/module/constants/storage.js.map +1 -1
- package/lib/module/lib/event/EventFactory.js +7 -14
- package/lib/module/lib/event/EventFactory.js.map +1 -1
- package/lib/module/lib/installReferrer/index.js +221 -0
- package/lib/module/lib/installReferrer/index.js.map +1 -0
- package/lib/module/lib/wagmi/WagmiEventHandler.js +1 -6
- package/lib/module/lib/wagmi/WagmiEventHandler.js.map +1 -1
- package/lib/module/solana/address.js +100 -0
- package/lib/module/solana/address.js.map +1 -0
- package/lib/module/solana/index.js +3 -0
- package/lib/module/solana/index.js.map +1 -0
- package/lib/module/solana/types.js +40 -0
- package/lib/module/solana/types.js.map +1 -0
- package/lib/module/types/events.js.map +1 -1
- package/lib/module/utils/address.js +58 -6
- package/lib/module/utils/address.js.map +1 -1
- package/lib/module/utils/trafficSource.js +59 -0
- package/lib/module/utils/trafficSource.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/FormoAnalytics.d.ts +23 -4
- package/lib/typescript/FormoAnalytics.d.ts.map +1 -1
- package/lib/typescript/constants/events.d.ts +1 -1
- package/lib/typescript/constants/storage.d.ts +1 -0
- package/lib/typescript/constants/storage.d.ts.map +1 -1
- package/lib/typescript/lib/event/EventFactory.d.ts +1 -1
- package/lib/typescript/lib/event/EventFactory.d.ts.map +1 -1
- package/lib/typescript/lib/event/types.d.ts +1 -1
- package/lib/typescript/lib/event/types.d.ts.map +1 -1
- package/lib/typescript/lib/installReferrer/index.d.ts +36 -0
- package/lib/typescript/lib/installReferrer/index.d.ts.map +1 -0
- package/lib/typescript/lib/wagmi/WagmiEventHandler.d.ts +0 -1
- package/lib/typescript/lib/wagmi/WagmiEventHandler.d.ts.map +1 -1
- package/lib/typescript/solana/address.d.ts +42 -0
- package/lib/typescript/solana/address.d.ts.map +1 -0
- package/lib/typescript/solana/index.d.ts +3 -0
- package/lib/typescript/solana/index.d.ts.map +1 -0
- package/lib/typescript/solana/types.d.ts +34 -0
- package/lib/typescript/solana/types.d.ts.map +1 -0
- package/lib/typescript/types/base.d.ts +38 -1
- package/lib/typescript/types/base.d.ts.map +1 -1
- package/lib/typescript/types/events.d.ts +0 -1
- package/lib/typescript/types/events.d.ts.map +1 -1
- package/lib/typescript/utils/address.d.ts +24 -4
- package/lib/typescript/utils/address.d.ts.map +1 -1
- package/lib/typescript/utils/trafficSource.d.ts +16 -0
- package/lib/typescript/utils/trafficSource.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +1 -1
- package/package.json +11 -8
- package/src/FormoAnalytics.ts +112 -16
- package/src/constants/events.ts +1 -1
- package/src/constants/storage.ts +3 -0
- package/src/lib/event/EventFactory.ts +7 -15
- package/src/lib/event/types.ts +0 -1
- package/src/lib/installReferrer/index.ts +262 -0
- package/src/lib/wagmi/WagmiEventHandler.ts +0 -4
- package/src/solana/address.ts +122 -0
- package/src/solana/index.ts +2 -0
- package/src/solana/types.ts +46 -0
- package/src/types/base.ts +40 -1
- package/src/types/events.ts +0 -1
- package/src/utils/address.ts +72 -6
- package/src/utils/trafficSource.ts +73 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solana address validation utilities
|
|
3
|
+
*
|
|
4
|
+
* Solana uses Base58 encoded 32-byte public keys as addresses.
|
|
5
|
+
* Format: FDKJvWcJNe6wecbgDYDFPCfgs14aJnVsUfWQRYWLn4Tn (32-44 characters)
|
|
6
|
+
*
|
|
7
|
+
* @see https://solana.com/developers/courses/intro-to-solana/interact-with-wallets
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { SolanaPublicKey } from "./types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base58 alphabet used by Solana (Bitcoin alphabet)
|
|
14
|
+
*/
|
|
15
|
+
const BASE58_ALPHABET =
|
|
16
|
+
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
17
|
+
|
|
18
|
+
const BASE58_CHAR_SET = new Set(BASE58_ALPHABET);
|
|
19
|
+
|
|
20
|
+
const MIN_SOLANA_ADDRESS_LENGTH = 32;
|
|
21
|
+
const MAX_SOLANA_ADDRESS_LENGTH = 44;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* System program addresses and other special Solana addresses
|
|
25
|
+
* These are valid addresses but may not represent user wallets
|
|
26
|
+
*/
|
|
27
|
+
export const SOLANA_SYSTEM_ADDRESSES = {
|
|
28
|
+
SYSTEM_PROGRAM: "11111111111111111111111111111111",
|
|
29
|
+
TOKEN_PROGRAM: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
|
30
|
+
TOKEN_2022_PROGRAM: "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
|
|
31
|
+
ASSOCIATED_TOKEN_PROGRAM: "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
|
|
32
|
+
RENT_SYSVAR: "SysvarRent111111111111111111111111111111111",
|
|
33
|
+
CLOCK_SYSVAR: "SysvarC1ock11111111111111111111111111111111",
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
function isValidBase58String(str: string): boolean {
|
|
37
|
+
for (const ch of str) {
|
|
38
|
+
if (!BASE58_CHAR_SET.has(ch)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if a string is a valid Solana address format
|
|
47
|
+
*
|
|
48
|
+
* Performs format validation only (length and character set). Does not
|
|
49
|
+
* verify that the address is a valid point on the Ed25519 curve.
|
|
50
|
+
*/
|
|
51
|
+
export function isSolanaAddress(value: unknown): value is string {
|
|
52
|
+
if (typeof value !== "string") {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const trimmed = value.trim();
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
trimmed.length < MIN_SOLANA_ADDRESS_LENGTH ||
|
|
60
|
+
trimmed.length > MAX_SOLANA_ADDRESS_LENGTH
|
|
61
|
+
) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return isValidBase58String(trimmed);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get a valid Solana address from a string or PublicKey
|
|
70
|
+
*/
|
|
71
|
+
export function getValidSolanaAddress(
|
|
72
|
+
address: string | SolanaPublicKey | null | undefined
|
|
73
|
+
): string | null {
|
|
74
|
+
if (!address) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof address === "object" && "toBase58" in address) {
|
|
79
|
+
try {
|
|
80
|
+
const base58 = address.toBase58();
|
|
81
|
+
return isSolanaAddress(base58) ? base58 : null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof address === "string") {
|
|
88
|
+
const trimmed = address.trim();
|
|
89
|
+
return isSolanaAddress(trimmed) ? trimmed : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a Solana address is a system program or well-known program address
|
|
97
|
+
*/
|
|
98
|
+
export function isSolanaSystemAddress(address: string): boolean {
|
|
99
|
+
const validAddress = getValidSolanaAddress(address);
|
|
100
|
+
if (!validAddress) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Object.values(SOLANA_SYSTEM_ADDRESSES).includes(
|
|
105
|
+
validAddress as (typeof SOLANA_SYSTEM_ADDRESSES)[keyof typeof SOLANA_SYSTEM_ADDRESSES]
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if a Solana address is blocked (should not emit events).
|
|
111
|
+
* Blocks system program addresses since they don't represent user wallets.
|
|
112
|
+
*/
|
|
113
|
+
export function isBlockedSolanaAddress(
|
|
114
|
+
address: string | SolanaPublicKey | null | undefined
|
|
115
|
+
): boolean {
|
|
116
|
+
const validAddress = getValidSolanaAddress(address);
|
|
117
|
+
if (!validAddress) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return isSolanaSystemAddress(validAddress);
|
|
122
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solana-specific type definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Solana cluster/network types
|
|
7
|
+
* Solana doesn't use chainId like EVM, instead it uses cluster names
|
|
8
|
+
*/
|
|
9
|
+
export type SolanaCluster = "mainnet-beta" | "testnet" | "devnet" | "localnet";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Mapping of Solana clusters to numeric chain IDs for consistency with EVM events
|
|
13
|
+
* These IDs are non-standard but provide a way to identify Solana networks in our analytics
|
|
14
|
+
*
|
|
15
|
+
* Using high numbers (900000+) to avoid collision with EVM chain IDs
|
|
16
|
+
*/
|
|
17
|
+
export const SOLANA_CHAIN_IDS: Record<SolanaCluster, number> = {
|
|
18
|
+
"mainnet-beta": 900001,
|
|
19
|
+
testnet: 900002,
|
|
20
|
+
devnet: 900003,
|
|
21
|
+
localnet: 900004,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default Solana chain ID (mainnet-beta)
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_SOLANA_CHAIN_ID = SOLANA_CHAIN_IDS["mainnet-beta"];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a chain ID belongs to a Solana network.
|
|
31
|
+
*/
|
|
32
|
+
export function isSolanaChainId(chainId: number | undefined | null): boolean {
|
|
33
|
+
if (chainId === undefined || chainId === null) return false;
|
|
34
|
+
return Object.values(SOLANA_CHAIN_IDS).includes(chainId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Solana PublicKey interface
|
|
39
|
+
* Used by address validation utilities.
|
|
40
|
+
*/
|
|
41
|
+
export interface SolanaPublicKey {
|
|
42
|
+
toBase58(): string;
|
|
43
|
+
toString(): string;
|
|
44
|
+
toBytes(): Uint8Array;
|
|
45
|
+
equals(other: SolanaPublicKey): boolean;
|
|
46
|
+
}
|
package/src/types/base.ts
CHANGED
|
@@ -56,7 +56,6 @@ export interface IFormoAnalytics {
|
|
|
56
56
|
chainId?: ChainID;
|
|
57
57
|
address: Address;
|
|
58
58
|
message: string;
|
|
59
|
-
signatureHash?: string;
|
|
60
59
|
},
|
|
61
60
|
properties?: IFormoEventProperties,
|
|
62
61
|
context?: IFormoEventContext,
|
|
@@ -161,6 +160,36 @@ export interface AutocaptureOptions {
|
|
|
161
160
|
lifecycle?: boolean;
|
|
162
161
|
}
|
|
163
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Configuration options for attribution capture.
|
|
165
|
+
*
|
|
166
|
+
* Attribution is not an event type — it's context enrichment that decorates
|
|
167
|
+
* every tracked event with `utm_*`, `ref`, and `referrer` fields. These
|
|
168
|
+
* options control the SDK's automatic attribution data sources.
|
|
169
|
+
*/
|
|
170
|
+
export interface AttributionOptions {
|
|
171
|
+
/**
|
|
172
|
+
* Capture traffic source from deep links via React Native's Linking API.
|
|
173
|
+
* When enabled, the SDK calls Linking.getInitialURL() on init and subscribes
|
|
174
|
+
* to the `url` event, parsing UTM parameters and referral codes into the
|
|
175
|
+
* event context.
|
|
176
|
+
* @default true
|
|
177
|
+
*/
|
|
178
|
+
deeplinks?: boolean;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Capture install-time attribution from the platform on first launch:
|
|
182
|
+
* - Android: Google Play Install Referrer API (requires react-native-play-install-referrer)
|
|
183
|
+
* - iOS: AdServices attribution token (requires react-native-ad-services-attribution)
|
|
184
|
+
*
|
|
185
|
+
* Resolved once on first successful fetch and cached; subsequent launches
|
|
186
|
+
* skip the native call. Silently no-ops when the optional native module
|
|
187
|
+
* is not installed.
|
|
188
|
+
* @default true
|
|
189
|
+
*/
|
|
190
|
+
installReferrer?: boolean;
|
|
191
|
+
}
|
|
192
|
+
|
|
164
193
|
/**
|
|
165
194
|
* Configuration options for Wagmi integration
|
|
166
195
|
* Allows the SDK to hook into Wagmi v2 wallet events
|
|
@@ -230,6 +259,16 @@ export interface Options {
|
|
|
230
259
|
* @default true
|
|
231
260
|
*/
|
|
232
261
|
autocapture?: boolean | AutocaptureOptions;
|
|
262
|
+
/**
|
|
263
|
+
* Control attribution context capture (deep links and install referrer).
|
|
264
|
+
* Attribution decorates every tracked event with `utm_*`, `ref`, and
|
|
265
|
+
* `referrer` fields — it is not itself an event type.
|
|
266
|
+
* - `false`: Disable all attribution capture
|
|
267
|
+
* - `true`: Enable all attribution sources (default)
|
|
268
|
+
* - `AttributionOptions`: Granular control over specific sources
|
|
269
|
+
* @default true
|
|
270
|
+
*/
|
|
271
|
+
attribution?: boolean | AttributionOptions;
|
|
233
272
|
/**
|
|
234
273
|
* Wagmi integration configuration
|
|
235
274
|
* When provided, the SDK will hook into Wagmi's event system
|
package/src/types/events.ts
CHANGED
package/src/utils/address.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Address validation and checksum utilities
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Supports both EVM and Solana addresses.
|
|
5
|
+
*
|
|
6
|
+
* Uses ethereum-cryptography for proper EIP-55 checksum computation.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { keccak256 } from "ethereum-cryptography/keccak.js";
|
|
8
10
|
import { utf8ToBytes } from "ethereum-cryptography/utils.js";
|
|
11
|
+
import {
|
|
12
|
+
isSolanaAddress,
|
|
13
|
+
getValidSolanaAddress,
|
|
14
|
+
isBlockedSolanaAddress,
|
|
15
|
+
} from "../solana/address";
|
|
16
|
+
import { isSolanaChainId } from "../solana/types";
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Convert Uint8Array to hex string
|
|
@@ -17,7 +25,7 @@ function toHex(bytes: Uint8Array): string {
|
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
/**
|
|
20
|
-
* Check if a string is a valid Ethereum address
|
|
28
|
+
* Check if a string is a valid Ethereum (EVM) address
|
|
21
29
|
*/
|
|
22
30
|
export function isValidAddress(address: string): boolean {
|
|
23
31
|
if (!address) return false;
|
|
@@ -56,7 +64,7 @@ export function toChecksumAddress(address: string): string {
|
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
/**
|
|
59
|
-
* Get valid address or null
|
|
67
|
+
* Get a valid (trimmed) EVM address, or null if invalid.
|
|
60
68
|
*/
|
|
61
69
|
export function getValidAddress(
|
|
62
70
|
address: string | undefined | null
|
|
@@ -68,7 +76,52 @@ export function getValidAddress(
|
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
/**
|
|
71
|
-
*
|
|
79
|
+
* Validates an EVM address and returns it in checksummed format.
|
|
80
|
+
*/
|
|
81
|
+
export function validateAndChecksumAddress(
|
|
82
|
+
address: string
|
|
83
|
+
): string | undefined {
|
|
84
|
+
const validAddress = getValidAddress(address);
|
|
85
|
+
return validAddress ? toChecksumAddress(validAddress) : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validates an address for both EVM and Solana chains.
|
|
90
|
+
*
|
|
91
|
+
* For EVM addresses, returns checksummed format.
|
|
92
|
+
* For Solana addresses, returns the Base58 address as-is.
|
|
93
|
+
*
|
|
94
|
+
* When chainId is explicitly provided, validation is strict:
|
|
95
|
+
* - Solana chainId → only Solana validation
|
|
96
|
+
* - Non-Solana chainId → only EVM validation
|
|
97
|
+
*
|
|
98
|
+
* When chainId is omitted, EVM is tried first with Solana fallback.
|
|
99
|
+
*/
|
|
100
|
+
export function validateAddress(
|
|
101
|
+
address: string,
|
|
102
|
+
chainId?: number | null
|
|
103
|
+
): string | undefined {
|
|
104
|
+
// Explicit Solana chainId → validate ONLY as Solana
|
|
105
|
+
if (chainId !== undefined && chainId !== null && isSolanaChainId(chainId)) {
|
|
106
|
+
return getValidSolanaAddress(address) || undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Explicit non-Solana chainId → validate ONLY as EVM
|
|
110
|
+
if (chainId !== undefined && chainId !== null) {
|
|
111
|
+
return validateAndChecksumAddress(address);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// No chainId → try EVM first, then Solana fallback
|
|
115
|
+
const validEvmAddress = validateAndChecksumAddress(address);
|
|
116
|
+
if (validEvmAddress) {
|
|
117
|
+
return validEvmAddress;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return getValidSolanaAddress(address) || undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Blocked EVM addresses that should not emit events
|
|
72
125
|
* (zero address, dead address)
|
|
73
126
|
*/
|
|
74
127
|
const BLOCKED_ADDRESSES = new Set<string>([
|
|
@@ -77,8 +130,21 @@ const BLOCKED_ADDRESSES = new Set<string>([
|
|
|
77
130
|
]);
|
|
78
131
|
|
|
79
132
|
/**
|
|
80
|
-
* Check if address is in blocked list
|
|
133
|
+
* Check if an address is in a blocked list.
|
|
134
|
+
* Handles both EVM (zero/dead addresses) and Solana (system program) blocks.
|
|
81
135
|
*/
|
|
82
136
|
export function isBlockedAddress(address: string): boolean {
|
|
83
|
-
|
|
137
|
+
if (!address || typeof address !== "string") return false;
|
|
138
|
+
|
|
139
|
+
const trimmed = address.trim();
|
|
140
|
+
|
|
141
|
+
if (isValidAddress(trimmed)) {
|
|
142
|
+
return BLOCKED_ADDRESSES.has(trimmed.toLowerCase());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (isSolanaAddress(trimmed)) {
|
|
146
|
+
return isBlockedSolanaAddress(trimmed);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return false;
|
|
84
150
|
}
|
|
@@ -151,3 +151,76 @@ export function mergeWithStoredTrafficSource(
|
|
|
151
151
|
...(context || {}),
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
/** All traffic-source keys used for merge operations. */
|
|
156
|
+
const TRAFFIC_SOURCE_KEYS: (keyof ITrafficSource)[] = [
|
|
157
|
+
"utm_source",
|
|
158
|
+
"utm_medium",
|
|
159
|
+
"utm_campaign",
|
|
160
|
+
"utm_term",
|
|
161
|
+
"utm_content",
|
|
162
|
+
"ref",
|
|
163
|
+
"referrer",
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update stored traffic source with incoming values. Per-field last-touch with
|
|
168
|
+
* fallback: non-empty incoming fields win, empty incoming fields preserve the
|
|
169
|
+
* previously stored value. This matches the Formo web SDK's behavior and
|
|
170
|
+
* prevents a non-marketing deep link (e.g. "myapp://home" with only a referrer
|
|
171
|
+
* and no UTM/ref) from destroying existing attribution data written by the
|
|
172
|
+
* Install Referrer flow or an earlier marketing deep link.
|
|
173
|
+
*/
|
|
174
|
+
export function updateStoredTrafficSource(
|
|
175
|
+
incoming: Partial<ITrafficSource>
|
|
176
|
+
): void {
|
|
177
|
+
try {
|
|
178
|
+
const existing = getStoredTrafficSource() || {};
|
|
179
|
+
const merged: Partial<ITrafficSource> = {};
|
|
180
|
+
|
|
181
|
+
for (const key of TRAFFIC_SOURCE_KEYS) {
|
|
182
|
+
const incomingVal = incoming[key];
|
|
183
|
+
const existingVal = existing[key];
|
|
184
|
+
if (incomingVal) {
|
|
185
|
+
merged[key] = incomingVal;
|
|
186
|
+
} else if (existingVal) {
|
|
187
|
+
merged[key] = existingVal;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
storeTrafficSource(merged);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.error("Error updating traffic source:", error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Merge a partial traffic source into the stored one, only filling in fields
|
|
199
|
+
* that are currently empty. Used by the Install Referrer flow so campaign data
|
|
200
|
+
* from the Play Store / Apple AdServices cannot clobber a deep-link attribution
|
|
201
|
+
* that arrived earlier in the same cold start.
|
|
202
|
+
*/
|
|
203
|
+
export function mergeTrafficSourceFill(
|
|
204
|
+
incoming: Partial<ITrafficSource>
|
|
205
|
+
): void {
|
|
206
|
+
try {
|
|
207
|
+
const existing = getStoredTrafficSource() || {};
|
|
208
|
+
const merged: Partial<ITrafficSource> = { ...existing };
|
|
209
|
+
let changed = false;
|
|
210
|
+
|
|
211
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
212
|
+
if (!value) continue; // skip empty/undefined incoming values
|
|
213
|
+
const existing = merged[key as keyof ITrafficSource];
|
|
214
|
+
if (existing === undefined || existing === null || existing === "") {
|
|
215
|
+
merged[key as keyof ITrafficSource] = String(value);
|
|
216
|
+
changed = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (changed) {
|
|
221
|
+
storeTrafficSource(merged);
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
logger.error("Error merging traffic source:", error);
|
|
225
|
+
}
|
|
226
|
+
}
|
package/src/version.ts
CHANGED