import { EIP6963ProviderDetail } from "mipd"; import { Address, ChainID, Config, EIP1193Provider, IFormoAnalytics, IFormoEventContext, IFormoEventProperties, EventCallback, Options, SignatureStatus, TransactionStatus } from "./types"; import { AutocaptureEventType } from "./tracking/TrackingPolicy"; import { WalletRestore } from "./wallet/WalletStateStore"; import { SolanaManager } from "./solana/SolanaManager"; import type { PrivyUser } from "./privy"; export declare class FormoAnalytics implements IFormoAnalytics { readonly writeKey: string; options: Options; /** Wallet identity, chain state and the active-wallet cookie. */ private wallet; private get _provider(); private set _provider(value); private get _evmAddress(); private get _evmChainId(); private session; private eventManager; /** Which EVM wallets exist, and what is known about each. */ private evm; /** Which providers to watch, and what their events mean. */ private evmEvents; /** Autocapture for signatures and transactions. */ private evmRequests; /** Every "may we track this?" rule. See src/tracking/TrackingPolicy.ts. */ private trackingPolicy; /** * Wagmi event handler for tracking wallet events via Wagmi v2 * Only initialized when options.wagmi is provided */ private wagmiHandler?; /** * Solana integration manager for tracking Solana wallet events. * Only initialized when options.solana is provided or via formo.solana. */ private solanaManager?; /** * Flag indicating if Wagmi mode is enabled * When true, EIP-1193 provider wrapping is skipped */ private isWagmiMode; /** * Flag indicating if EVM provider tracking is disabled. * When true, all EIP-1193/EIP-6963 detection and wrapping is skipped. */ private isEvmDisabled; /** Instance-level flag so multiple SDK instances don't interfere. */ private crossSubdomainCookies; /** In-memory URL used to deduplicate SPA pageview events. */ private _currentUrl; /** Page-hit hooks installed in trackPageHits() so cleanup() can undo them. */ private _onPopStateListener?; private _onLocationChangeListener?; private _pageGeneration; /** Measures Core Web Vitals for this page load; stopped by cleanup(). */ private webVitals?; config: Config; /** * The wallet later events are attributed to, derived from whichever * namespace last claimed the slot. Read by the wagmi and Privy * integrations; the store is the only writer. */ get currentAddress(): Address | undefined; get currentChainId(): ChainID | undefined; /** * These stay writable. * * Both were public mutable fields, so a consumer assigning one is using a * documented public API. Getter-only replacements would break that * silently: TypeScript rejects the assignment, and plain JavaScript throws * on an accessor with no setter. The writes forward into the store, which * keeps a single owner without turning a refactor into a breaking change. */ set currentAddress(value: Address | undefined); set currentChainId(value: ChainID | undefined); currentUserId?: string; private constructor(); static init(writeKey: string, options?: Options): Promise; /** * Emits a page visit event with the current URL information, fire on page change. * @param {string} category - The category of the page * @param {string} name - The name of the page * @param {Record} properties - Additional properties to include * @param {Record} context - Additional context to include * @param {(...args: unknown[]) => void} callback - Optional callback function * @returns {Promise} */ page(category?: string, name?: string, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: EventCallback): Promise; /** * Reset user and wallet state while preserving the browser's anonymous id. * The detect marker is scoped to that id, so it survives; the identify * marker is cleared so a login after a logout identifies again. Use * `optOutTracking()` to clear the id, the markers and the attribution. * @returns {void} */ reset(): void; /** * Clean up resources and event listeners * Call this when destroying the analytics instance * @returns {void} */ /** Set by cleanup(); a torn-down instance refuses new registrations. */ private isCleanedUp; /** True once cleanup() has run. formofy() uses it to avoid reusing a dead instance. */ get disposed(): boolean; cleanup(): void; /** * Emits a connect wallet event. * @param {ChainID} params.chainId * @param {Address} params.address * @param {IFormoEventProperties} properties * @param {IFormoEventContext} context * @param {(...args: unknown[]) => void} callback * @returns {Promise} */ connect({ chainId, address, }: { chainId: ChainID; address: Address; }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: EventCallback): Promise; /** * Emits a disconnect wallet event. * @param {ChainID} [params.chainId] * @param {Address} [params.address] * @param {IFormoEventProperties} properties * @param {IFormoEventContext} context * @param {(...args: unknown[]) => void} callback * @returns {Promise} */ disconnect(params?: { chainId?: ChainID; address?: Address; }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: EventCallback): Promise; /** @see WalletStateStore.set */ private setChainState; /** @see WalletStateStore.clear */ private clearChainState; /** * Record validated wallet/chain state WITHOUT emitting an event. * * Integrations must call this on every connect / chain change / disconnect, * even when the matching autocapture event is disabled, or the exclusion * gate (which keys off the central chain, not the event payload) can be * bypassed. Stays on the class because integrations bind to it. * @see WalletStateStore.syncWalletState */ syncWalletState(params: { chainId?: ChainID; address?: Address; }): void; /** @see WalletStateStore.restore */ restoreWalletState(params: { chainId: ChainID; address: Address; }): void; /** @see WalletStateStore.deferRestore */ deferWalletRestore(chainId: ChainID): WalletRestore; /** The Solana wallet held centrally, whichever namespace is active. */ get solanaAddress(): Address | undefined; private retryWalletDetection; /** @see WalletStateStore.backfill */ private backfillActiveWallet; /** @see WalletStateStore.clearStaleEvmWalletOnSwitchWhileSuppressed */ private clearStaleEvmWalletOnSwitchWhileSuppressed; /** @see EvmProviderRegistry.infoFor */ private getProviderInfo; /** @see EvmProviderRegistry.resolveChainId */ private resolveChainIdForProvider; /** @see EvmProviderRegistry.rememberChain */ private rememberProviderChain; /** @see EvmProviderRegistry.addressOf */ private getAddress; /** * Emits a chain network change event. * @param {ChainID} params.chainId * @param {Address} params.address * @param {IFormoEventProperties} properties * @param {IFormoEventContext} context * @param {(...args: unknown[]) => void} callback * @returns {Promise} */ chain({ chainId, address, }: { chainId: ChainID; address?: Address; }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: EventCallback): Promise; /** * Emits a signature event. * @param {SignatureStatus} params.status - requested, confirmed, rejected * @param {ChainID} params.chainId * @param {Address} params.address * @param {string} params.message * @param {IFormoEventProperties} properties * @param {IFormoEventContext} context * @param {(...args: unknown[]) => void} callback * @returns {Promise} */ signature({ status, chainId, address, message, }: { status: SignatureStatus; chainId?: ChainID; address: Address; message: string; }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: EventCallback): Promise; /** * Emits a transaction event. * @param {TransactionStatus} params.status - started, broadcasted, rejected * @param {ChainID} params.chainId * @param {Address} params.address * @param {string} params.data * @param {string} params.to * @param {string} params.value * @param {string} params.transactionHash - only provided if status is broadcasted * @param {IFormoEventProperties} properties * @param {IFormoEventContext} context * @param {(...args: unknown[]) => void} callback * @returns {Promise} */ transaction({ status, chainId, address, data, to, value, transactionHash, function_name, function_args, }: { status: TransactionStatus; chainId: ChainID; address: Address; data?: string; to?: string; value?: string; transactionHash?: string; function_name?: string; function_args?: Record; }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: EventCallback): Promise; /** * Emits an identify event with current wallet address and provider info. * * @param {string} params.address - Wallet address * @param {string} params.userId - External user ID * @param {string} params.rdns - Provider reverse domain name * @param {string} params.providerName - Provider display name * @param {IFormoEventProperties} properties - Additional properties to include with the identify event * @param {IFormoEventContext} context * @param {(...args: unknown[]) => void} callback * @returns {Promise} * * @example * ```ts * // Basic identify * formo.identify({ address: '0x...', userId: 'user123' }); * * // Privy: pass the usePrivy() user to identify every linked wallet under * // the user's DID in one call. Attribution stays on the already-connected * // wallet when there is one, else Privy's primary (user.wallet); pass * // `activeAddress` to pin a specific wallet. * const { user } = usePrivy(); * if (user) formo.identify(user); * ``` */ identify(user: PrivyUser, options?: { activeAddress?: string; properties?: IFormoEventProperties; }): Promise; identify(params?: { address: Address; providerName?: string; userId?: string; rdns?: string; }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: EventCallback): Promise; /** * Reconcile currentChainId with a newly-activated Privy wallet's chain * namespace. identify() sets currentAddress but never touches the chain id * (that comes from connect()/chain()/wagmi), so activating e.g. a Solana * wallet while an EVM chain id is current would leave the address paired with * a mismatched chain in events, excludeChains, and the active-wallet cookie. * * We can't infer the wallet's specific chain id from Privy's chainType, so on * a namespace mismatch we clear the chain id rather than assert a wrong one; a * real wallet connect will set the correct chain. Same-namespace activations * (and wallets whose namespace can't be determined) leave the chain id alone. * * Privy doesn't always supply `chainType`: a `smart_wallet` entry is * `{ type, address, smartWalletType }`, and `cross_app` wallets are bare * `{ address }`. A `0x`-prefixed 20-byte address is unambiguously EVM though, * so fall back to the address shape - otherwise activating an EVM smart * wallet while a Solana chain id is current would leave the address paired * with the wrong chain, and an `excludeChains` gate could drop the identify * after it was already dedup-marked. * * @internal Not part of the public IFormoAnalytics contract - invoked by * `identifyPrivyUser` (via a structural cast) before it emits, so both the * `identify(user,{privy:true})` and direct `identifyPrivyUser()` paths * reconcile the chain. */ syncPrivyActiveChain(chainType?: string, address?: string): void; /** * Emits a detect wallet event with current wallet provider info. * @param {string} params.providerName * @param {string} params.rdns * @param {IFormoEventProperties} properties * @param {IFormoEventContext} context * @param {(...args: unknown[]) => void} callback * @returns {Promise} */ detect({ providerName, rdns, }: { providerName: string; rdns: string; }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: EventCallback): Promise; /** * Emits a custom user event with custom properties. * * `properties.idempotency_key` (string or safe integer) names one * action, e.g. an order id. Calls that reuse it for the same event name * share one message id and collapse at ingestion. The key is hashed and * not sent. null or undefined means no key; any other value drops the * call with a warning. * @param {string} event The name of the tracked event * @param {IFormoEventProperties} properties * @param {IFormoEventContext} context * @param {EventCallback} callback * @returns {Promise} */ track(event: string, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: EventCallback): Promise; /** * Whether an event carrying this chain would currently be sent. * * Exposed for integrations that keep their own "already reported" state. * `syncWalletState()` can accept a wallet that `trackEvent()` then drops - * `tracking: false`, or a chain in `excludeChains` - and an integration that * marked it as reported would stay silent about that wallet even after the * configuration changed to allow it. */ willTrackEvent(chainId?: ChainID): boolean; /** Stop tracking this visitor, and forget the identity already collected. */ optOutTracking(): void; /** * Opt back into tracking after previously opting out. This will re-enable analytics tracking * and switch back to persistent storage. * @returns {void} */ optInTracking(): void; /** * Check if the user has opted out of tracking. * @returns {boolean} True if the user has opted out */ hasOptedOutTracking(): boolean; private onLocationChange; private trackPageHits; /** * Wrap history.pushState / replaceState exactly once per `history` object, * regardless of how many SDK instances are constructed. Uses a Symbol * marker so we recognize our own wrapper across module reloads in HMR. */ private static installHistoryHooksOnce; private trackPageHit; /** Send the web vitals of this page load. Called once, as the page is hidden. */ private trackWebVitals; private trackEvent; /** * Visitor-level tracking suppression. * * True when the SDK must not persist identity/session/chain state or send * events for this visitor. Public entry points that write state before * reaching the `shouldTrack()` gate (identify / connect / detect) check this * first, so a suppressed visitor leaves no cookies or session state. * * The rule lives in `TrackingPolicy`; this stays on the class because it is * part of the surface integrations bind to. * @internal Also read by `identifyPrivyUser` (via a structural cast) so the * Privy sync skips chain reconciliation and emission for suppressed visitors. */ isTrackingSuppressed(): boolean; /** @see TrackingPolicy.shouldTrack */ private shouldTrack; /** * Check if a specific wallet event type is enabled for autocapture * @param eventType The wallet event type to check * @returns {boolean} True if the event type should be autocaptured */ isAutocaptureEnabled(eventType: AutocaptureEventType): boolean; get providers(): readonly EIP6963ProviderDetail[]; get provider(): EIP1193Provider | undefined; /** * Access the Solana integration manager. * Lazily creates one if not already initialized. * * @example * ```tsx * formo.solana.setStore(client.store); * formo.solana.setCluster("devnet"); * // For signatures, use formo.signature() directly * ``` */ get solana(): SolanaManager; /** * INTERNAL. Install the request wrapper on a wagmi connector's provider. * * Wagmi mode watches the store and caches, which see hook-driven calls * only; imperative viem calls (walletClient.sendTransaction, * .signMessage, .writeContract, raw request) create no mutation and were * silently lost. Every viem client in a wagmi app is built on the * connector's EIP-1193 provider, so wrapping that provider closes the * gap. Lifecycle (connect/chain/disconnect) stays store-driven: only the * request wrapper installs here. Double counting is prevented in the * wrapper via `shouldSkipRequestCapture`. * * `attribution` resolves, live, the name and rdns of the connector this * provider was wrapped for. Request-derived events are named from the * registry, which for an unannounced provider falls back to flag * sniffing; recording the connector's resolver here keeps them in * agreement with the hook-driven events for that connector. */ _wrapWagmiProvider(provider: EIP1193Provider, chainId?: number, attribution?: () => { name: string; rdns?: string; } | undefined): boolean; /** INTERNAL. Chain updates for the fallback-wrapped provider. */ _rememberWagmiProviderChain(provider: EIP1193Provider, chainId: number | undefined): void; /** * Track an EIP-1193 provider the page constructed itself. * * Discovery covers EIP-6963 announcements and `window.ethereum`, which is * every injected wallet and nothing else. WalletConnect and Ledger * providers are built by the app (`EthereumProvider.init(...)`) and * announce nothing, so their sessions were invisible: connects, * signatures, and transactions all silently missing. Hand the provider * here once it exists and it takes the exact pipeline a discovered * provider takes - detect event, lifecycle listeners, request wrapper - * and a session that is already live is adopted from the provider's * synchronous state. * * Metadata resolution order: the caller's `info` overrides win; then a * WalletConnect session's peer metadata, which names the REAL wallet on * the far side of the transport (for example "Ledger Live"); then flag * sniffing; then a generic fallback. One deliberate exception: a caller * name of exactly "WalletConnect" is the generic transport name, so the * live peer still replaces it on events - name the provider anything * else to pin it verbatim. * * No-op outside the EIP-1193 path: in wagmi mode the connector system * already tracks these sessions, and wrapping the same provider twice * would double-report every event. * * With several live SDK instances (multi write-key pages) registering * the SAME provider, request-derived events (signatures, transactions) * go to the most recently CREATED live instance, regardless of the * order registrations happen to land in - the same single-observer * semantics discovery has always had for the request wrapper. Lifecycle events (connect, chain, disconnect) reach every * instance. Fanning request observations out to all instances is a * separate feature. * * @returns true when the provider is (now) tracked, false when it was * refused (wagmi mode, EVM disabled, or not a valid EIP-1193 provider). * * @example * ```typescript * const wcProvider = await EthereumProvider.init({ projectId, chains }); * formo.registerProvider(wcProvider); * ``` */ registerProvider(provider: EIP1193Provider, info?: { name?: string; rdns?: string; icon?: `data:image/${string}`; }): boolean; /** Fallback uuid suffix for platforms without crypto.randomUUID. */ private static registeredProviderSeq; getTrackedProvidersCount(): number; /** * Get current provider state for debugging * @returns Object containing current provider state information */ getProviderState(): { totalProviders: number; trackedProviders: number; seenProviders: number; activeProvider: boolean; }; } //# sourceMappingURL=FormoAnalytics.d.ts.map