diff --git a/install.mdx b/install.mdx index 4e13c2b..b16e896 100644 --- a/install.mdx +++ b/install.mdx @@ -637,6 +637,143 @@ Make sure you use the same `` for both your website and your app. ``` + + + + Working example: [with-angular](https://github.com/getformo/examples/tree/main/with-angular). Also see the [Angular section](/sdks/web#angular) of the Web SDK page. + + + Angular has no first-class Formo binding — `FormoAnalyticsProvider` and `useFormo()` are React-only. Angular apps install Formo on the **non-wagmi, non-React path**: the framework-agnostic `FormoAnalytics.init()` core wrapped in an injectable service, with wallets connected over the bare EIP-1193 provider (`window.ethereum`). + + #### 1. Install the Formo SDK + + Install the SDK along with the `buffer` polyfill (Angular's esbuild build doesn't auto-polyfill Node globals, but the SDK uses `Buffer` to decode signed-message payloads) and viem: + + ```bash + pnpm add @formo/analytics buffer viem + pnpm add -D @ngx-env/builder + ``` + + Wire the polyfill in `src/polyfills.ts`: + + ```ts + // src/polyfills.ts + import { Buffer } from 'buffer'; + (globalThis as unknown as { Buffer?: typeof Buffer }).Buffer ??= Buffer; + ``` + + Reference it from `angular.json` and silence the SDK's React-re-export warnings: + + ```jsonc + // angular.json (build > options) + { + "polyfills": ["src/polyfills.ts"], + "allowedCommonJsDependencies": ["react", "react/jsx-runtime", "react-dom", "viem"] + } + ``` + + #### 2. Wrap `FormoAnalytics.init()` in an injectable service + + ```ts + // src/app/services/formo-analytics.service.ts + + import { Injectable } from '@angular/core'; + import { FormoAnalytics } from '@formo/analytics'; + import type { IFormoAnalytics, IFormoEventProperties } from '@formo/analytics'; + + @Injectable({ providedIn: 'root' }) + export class FormoAnalyticsService { + private analytics: IFormoAnalytics | null = null; + + async init(): Promise { + if (typeof window === 'undefined') return; + const writeKey = import.meta.env.NG_APP_FORMO_WRITE_KEY; + if (!writeKey) return; + + this.analytics = await FormoAnalytics.init(writeKey, { + tracking: true, + autocapture: { connect: true, disconnect: true, chain: true, signature: true, transaction: true }, + }); + } + + identify(address: string): void { + void this.analytics?.identify({ address }); + } + + track(event: string, properties?: IFormoEventProperties): void { + void this.analytics?.track(event, properties); + } + } + ``` + + Replace `NG_APP_FORMO_WRITE_KEY` with your write key in `.env`. The `NG_APP_*` prefix is exposed to the client by [`@ngx-env/builder`](https://github.com/chihab/ngx-env). + + #### 3. Initialize before bootstrap + + Use `provideAppInitializer` so the SDK's autocapture wraps `window.ethereum` **before** any wallet interaction can happen: + + ```ts + // src/app/app.config.ts + + import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core'; + import { provideRouter } from '@angular/router'; + + import { routes } from './app.routes'; + import { FormoAnalyticsService } from './services/formo-analytics.service'; + + export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes), + provideAppInitializer(() => inject(FormoAnalyticsService).init()), + ], + }; + ``` + + + Don't initialize from `ngOnInit` — it runs after first render, leaving a race window where early wallet interactions are not captured. + + + #### 4. Identify users + + Call `identify()` once a wallet address is known. Angular Router's `pushState` is already wrapped by the SDK, so `page` events are autocaptured on every route change — do not add a `NavigationEnd` subscription that calls `formo.page()` or you'll double-count. + + ```ts + import { Injectable, inject, signal } from '@angular/core'; + import { FormoAnalyticsService } from './services/formo-analytics.service'; + import type { Address } from 'viem'; + + @Injectable({ providedIn: 'root' }) + export class WalletService { + private readonly formo = inject(FormoAnalyticsService); + readonly address = signal
(null); + + async connect(): Promise { + const [account] = await window.ethereum!.request({ method: 'eth_requestAccounts' }); + this.address.set(account); + this.formo.identify(account); + } + } + ``` + + #### 5. Track custom events + + Formo autocaptures page views, wallet connect/disconnect, chain switches, signatures, and transactions. Use `track()` for app-specific actions: + + ```ts + import { Component, inject } from '@angular/core'; + import { FormoAnalyticsService } from './services/formo-analytics.service'; + + @Component({ /* ... */ }) + export class Home { + private readonly formo = inject(FormoAnalyticsService); + + onSwapCompleted(): void { + this.formo.track('Swap Completed', { points: 100 }); + } + } + ``` + + ## Code Examples diff --git a/sdks/web.mdx b/sdks/web.mdx index 502327f..3342f2a 100644 --- a/sdks/web.mdx +++ b/sdks/web.mdx @@ -21,6 +21,7 @@ There are several ways to install the Formo SDK: - [Solana](#solana-integration) for Solana apps using framework-kit - [HTML Snippet](#html-snippet) is recommended for static websites - [React & Next.js (without Wagmi)](#react-&-next-js-without-wagmi) +- [Angular](#angular) for Angular apps using the bare EIP-1193 provider We recommend installing Formo on **both your website (example.com) and your app (app.example.com)** on the same project with the same SDK write key. @@ -143,6 +144,83 @@ const HomePage = () => { export default HomePage; ``` +### Angular + + +`FormoAnalyticsProvider` and `useFormo()` are React-only. Angular apps use the framework-agnostic `FormoAnalytics.init()` core, wrapped in an injectable service, with wallets connected over the bare EIP-1193 provider (`window.ethereum`). Full working example: [with-angular](https://github.com/getformo/examples/tree/main/with-angular). + + +Install the Web SDK along with the `buffer` polyfill. Angular's esbuild build doesn't auto-polyfill Node globals, but the SDK uses `Buffer` to decode signed-message payloads — without it, signing throws `ReferenceError: Buffer is not defined`. + +```bash +npm install @formo/analytics buffer viem --save +``` + +```ts +// src/polyfills.ts +import { Buffer } from 'buffer'; +(globalThis as unknown as { Buffer?: typeof Buffer }).Buffer ??= Buffer; +``` + +```jsonc +// angular.json (build > options) +{ + "polyfills": ["src/polyfills.ts"], + "allowedCommonJsDependencies": ["react", "react/jsx-runtime", "react-dom", "viem"] +} +``` + +Wrap `FormoAnalytics.init()` in an injectable service: + +```ts +// src/app/services/formo-analytics.service.ts +import { Injectable } from '@angular/core'; +import { FormoAnalytics } from '@formo/analytics'; +import type { IFormoAnalytics, IFormoEventProperties } from '@formo/analytics'; + +@Injectable({ providedIn: 'root' }) +export class FormoAnalyticsService { + private analytics: IFormoAnalytics | null = null; + + async init(): Promise { + if (typeof window === 'undefined') return; + this.analytics = await FormoAnalytics.init('', { + autocapture: { connect: true, disconnect: true, chain: true, signature: true, transaction: true }, + }); + } + + identify(address: string): void { + void this.analytics?.identify({ address }); + } + + track(event: string, properties?: IFormoEventProperties): void { + void this.analytics?.track(event, properties); + } +} +``` + +Initialize it with `provideAppInitializer` so the SDK's autocapture wraps `window.ethereum` **before** any wallet interaction: + +```ts +// src/app/app.config.ts +import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { routes } from './app.routes'; +import { FormoAnalyticsService } from './services/formo-analytics.service'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes), + provideAppInitializer(() => inject(FormoAnalyticsService).init()), + ], +}; +``` + + +Angular Router navigates with `history.pushState`, which the SDK hooks on init — page changes are autocaptured. Don't add a `NavigationEnd` subscription that calls `formo.page()` or you'll double-count. + + ## Identify users Call [`identify()`](/data/events/identify) after a user connects their wallet or signs in on your website or app: @@ -223,6 +301,29 @@ If no parameters are specified, the Formo SDK will attempt to auto-identify the > ``` + + Inject the analytics service and call `identify()` from the same place you discover the wallet address (for example, a `WalletService` that drives `eth_requestAccounts`): + + ```ts + import { Injectable, inject, signal } from '@angular/core'; + import { FormoAnalyticsService } from './services/formo-analytics.service'; + import type { Address } from 'viem'; + + @Injectable({ providedIn: 'root' }) + export class WalletService { + private readonly formo = inject(FormoAnalyticsService); + readonly address = signal
(null); + + async connect(): Promise { + const [account] = await window.ethereum!.request({ method: 'eth_requestAccounts' }); + this.address.set(account); + this.formo.identify(account); + } + } + ``` + + See the [with-angular example](https://github.com/getformo/examples/tree/main/with-angular) for the full wallet service, including the hydrate-on-load and `wallet_revokePermissions` patterns. + ## Track events @@ -335,6 +436,27 @@ You can [track volume, revenue, and points](/data/events/track#tracking-volume,- > examples/with-porto + + Next.js + Crossmint embedded smart wallets. Manual Formo instrumentation (no EIP-1193 / wagmi). + + + Vite + React + Openfort with Shield encryption sessions (Express backend) and an Aave supply/withdraw demo. + + + Angular 21 + bare EIP-1193 + viem. Uses the framework-agnostic SDK core. +