Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,143 @@ Make sure you use the same `<SDK_WRITE_KEY>` for both your website and your app.
```

</Tab>
<Tab icon="angular" title="Angular">

<Note>
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.
</Note>

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
Comment on lines +653 to +654

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with the rest of the documentation which primarily uses npm, it's better to use npm commands here as well.

    npm install @formo/analytics buffer viem --save
    npm install @ngx-env/builder --save-dev

```

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<void> {
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()),
],
};
```

<Warning>
Don't initialize from `ngOnInit` — it runs after first render, leaving a race window where early wallet interactions are not captured.
</Warning>

#### 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<Address | null>(null);

async connect(): Promise<void> {
const [account] = await window.ethereum!.request({ method: 'eth_requestAccounts' });
this.address.set(account);
this.formo.identify(account);
}
Comment on lines +750 to +754

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a non-null assertion (!) on window.ethereum can lead to runtime errors if the user does not have a wallet extension installed. It is safer to check for its existence before attempting to use it.

      async connect(): Promise<void> {
        if (!window.ethereum) return;
        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 });
}
}
```

</Tab>
</Tabs>

## Code Examples
Expand Down
122 changes: 122 additions & 0 deletions sdks/web.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -143,6 +144,83 @@ const HomePage = () => {
export default HomePage;
```

### Angular

<Note>
`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).
</Note>

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<void> {
if (typeof window === 'undefined') return;
this.analytics = await FormoAnalytics.init('<YOUR_WRITE_KEY>', {
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()),
],
};
```

<Note>
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.
</Note>

## Identify users

Call [`identify()`](/data/events/identify) after a user connects their wallet or signs in on your website or app:
Expand Down Expand Up @@ -223,6 +301,29 @@ If no parameters are specified, the Formo SDK will attempt to auto-identify the
></script>
```
</Tab>
<Tab title="Angular">
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<Address | null>(null);

async connect(): Promise<void> {
const [account] = await window.ethereum!.request({ method: 'eth_requestAccounts' });
this.address.set(account);
this.formo.identify(account);
}
Comment on lines +317 to +321

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a non-null assertion (!) on window.ethereum can lead to runtime errors if the user does not have a wallet extension installed. It is safer to check for its existence before attempting to use it.

      async connect(): Promise<void> {
        if (!window.ethereum) return;
        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.
</Tab>
</Tabs>

## Track events
Expand Down Expand Up @@ -335,6 +436,27 @@ You can [track volume, revenue, and points](/data/events/track#tracking-volume,-
>
examples/with-porto
</Card>
<Card
title="Crossmint"
icon="file-code"
href="https://github.com/getformo/examples/tree/main/with-crossmint"
>
Next.js + Crossmint embedded smart wallets. Manual Formo instrumentation (no EIP-1193 / wagmi).
</Card>
<Card
title="Openfort"
icon="file-code"
href="https://github.com/getformo/examples/tree/main/with-openfort"
>
Vite + React + Openfort with Shield encryption sessions (Express backend) and an Aave supply/withdraw demo.
</Card>
<Card
title="Angular"
icon="file-code"
href="https://github.com/getformo/examples/tree/main/with-angular"
>
Angular 21 + bare EIP-1193 + viem. Uses the framework-agnostic SDK core.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

"Angular 21" appears to be a typo, as the current stable version is 19 and version 21 is not yet released.

    Angular 19 + bare EIP-1193 + viem. Uses the framework-agnostic SDK core.

</Card>
<Card
title="Farcaster Mini App"
icon="file-code"
Expand Down
Loading