Skip to content

Commit 6f3cebb

Browse files
authored
add angular docs and example links (#78)
1 parent 3121bbd commit 6f3cebb

2 files changed

Lines changed: 259 additions & 0 deletions

File tree

install.mdx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,143 @@ Make sure you use the same `<SDK_WRITE_KEY>` for both your website and your app.
637637
```
638638

639639
</Tab>
640+
<Tab icon="angular" title="Angular">
641+
642+
<Note>
643+
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.
644+
</Note>
645+
646+
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`).
647+
648+
#### 1. Install the Formo SDK
649+
650+
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:
651+
652+
```bash
653+
pnpm add @formo/analytics buffer viem
654+
pnpm add -D @ngx-env/builder
655+
```
656+
657+
Wire the polyfill in `src/polyfills.ts`:
658+
659+
```ts
660+
// src/polyfills.ts
661+
import { Buffer } from 'buffer';
662+
(globalThis as unknown as { Buffer?: typeof Buffer }).Buffer ??= Buffer;
663+
```
664+
665+
Reference it from `angular.json` and silence the SDK's React-re-export warnings:
666+
667+
```jsonc
668+
// angular.json (build > options)
669+
{
670+
"polyfills": ["src/polyfills.ts"],
671+
"allowedCommonJsDependencies": ["react", "react/jsx-runtime", "react-dom", "viem"]
672+
}
673+
```
674+
675+
#### 2. Wrap `FormoAnalytics.init()` in an injectable service
676+
677+
```ts
678+
// src/app/services/formo-analytics.service.ts
679+
680+
import { Injectable } from '@angular/core';
681+
import { FormoAnalytics } from '@formo/analytics';
682+
import type { IFormoAnalytics, IFormoEventProperties } from '@formo/analytics';
683+
684+
@Injectable({ providedIn: 'root' })
685+
export class FormoAnalyticsService {
686+
private analytics: IFormoAnalytics | null = null;
687+
688+
async init(): Promise<void> {
689+
if (typeof window === 'undefined') return;
690+
const writeKey = import.meta.env.NG_APP_FORMO_WRITE_KEY;
691+
if (!writeKey) return;
692+
693+
this.analytics = await FormoAnalytics.init(writeKey, {
694+
tracking: true,
695+
autocapture: { connect: true, disconnect: true, chain: true, signature: true, transaction: true },
696+
});
697+
}
698+
699+
identify(address: string): void {
700+
void this.analytics?.identify({ address });
701+
}
702+
703+
track(event: string, properties?: IFormoEventProperties): void {
704+
void this.analytics?.track(event, properties);
705+
}
706+
}
707+
```
708+
709+
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).
710+
711+
#### 3. Initialize before bootstrap
712+
713+
Use `provideAppInitializer` so the SDK's autocapture wraps `window.ethereum` **before** any wallet interaction can happen:
714+
715+
```ts
716+
// src/app/app.config.ts
717+
718+
import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core';
719+
import { provideRouter } from '@angular/router';
720+
721+
import { routes } from './app.routes';
722+
import { FormoAnalyticsService } from './services/formo-analytics.service';
723+
724+
export const appConfig: ApplicationConfig = {
725+
providers: [
726+
provideRouter(routes),
727+
provideAppInitializer(() => inject(FormoAnalyticsService).init()),
728+
],
729+
};
730+
```
731+
732+
<Warning>
733+
Don't initialize from `ngOnInit` — it runs after first render, leaving a race window where early wallet interactions are not captured.
734+
</Warning>
735+
736+
#### 4. Identify users
737+
738+
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.
739+
740+
```ts
741+
import { Injectable, inject, signal } from '@angular/core';
742+
import { FormoAnalyticsService } from './services/formo-analytics.service';
743+
import type { Address } from 'viem';
744+
745+
@Injectable({ providedIn: 'root' })
746+
export class WalletService {
747+
private readonly formo = inject(FormoAnalyticsService);
748+
readonly address = signal<Address | null>(null);
749+
750+
async connect(): Promise<void> {
751+
const [account] = await window.ethereum!.request({ method: 'eth_requestAccounts' });
752+
this.address.set(account);
753+
this.formo.identify(account);
754+
}
755+
}
756+
```
757+
758+
#### 5. Track custom events
759+
760+
Formo autocaptures page views, wallet connect/disconnect, chain switches, signatures, and transactions. Use `track()` for app-specific actions:
761+
762+
```ts
763+
import { Component, inject } from '@angular/core';
764+
import { FormoAnalyticsService } from './services/formo-analytics.service';
765+
766+
@Component({ /* ... */ })
767+
export class Home {
768+
private readonly formo = inject(FormoAnalyticsService);
769+
770+
onSwapCompleted(): void {
771+
this.formo.track('Swap Completed', { points: 100 });
772+
}
773+
}
774+
```
775+
776+
</Tab>
640777
</Tabs>
641778

642779
## Code Examples

sdks/web.mdx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ There are several ways to install the Formo SDK:
2121
- [Solana](#solana-integration) for Solana apps using framework-kit
2222
- [HTML Snippet](#html-snippet) is recommended for static websites
2323
- [React & Next.js (without Wagmi)](#react-&-next-js-without-wagmi)
24+
- [Angular](#angular) for Angular apps using the bare EIP-1193 provider
2425

2526
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.
2627

@@ -143,6 +144,83 @@ const HomePage = () => {
143144
export default HomePage;
144145
```
145146

147+
### Angular
148+
149+
<Note>
150+
`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).
151+
</Note>
152+
153+
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`.
154+
155+
```bash
156+
npm install @formo/analytics buffer viem --save
157+
```
158+
159+
```ts
160+
// src/polyfills.ts
161+
import { Buffer } from 'buffer';
162+
(globalThis as unknown as { Buffer?: typeof Buffer }).Buffer ??= Buffer;
163+
```
164+
165+
```jsonc
166+
// angular.json (build > options)
167+
{
168+
"polyfills": ["src/polyfills.ts"],
169+
"allowedCommonJsDependencies": ["react", "react/jsx-runtime", "react-dom", "viem"]
170+
}
171+
```
172+
173+
Wrap `FormoAnalytics.init()` in an injectable service:
174+
175+
```ts
176+
// src/app/services/formo-analytics.service.ts
177+
import { Injectable } from '@angular/core';
178+
import { FormoAnalytics } from '@formo/analytics';
179+
import type { IFormoAnalytics, IFormoEventProperties } from '@formo/analytics';
180+
181+
@Injectable({ providedIn: 'root' })
182+
export class FormoAnalyticsService {
183+
private analytics: IFormoAnalytics | null = null;
184+
185+
async init(): Promise<void> {
186+
if (typeof window === 'undefined') return;
187+
this.analytics = await FormoAnalytics.init('<YOUR_WRITE_KEY>', {
188+
autocapture: { connect: true, disconnect: true, chain: true, signature: true, transaction: true },
189+
});
190+
}
191+
192+
identify(address: string): void {
193+
void this.analytics?.identify({ address });
194+
}
195+
196+
track(event: string, properties?: IFormoEventProperties): void {
197+
void this.analytics?.track(event, properties);
198+
}
199+
}
200+
```
201+
202+
Initialize it with `provideAppInitializer` so the SDK's autocapture wraps `window.ethereum` **before** any wallet interaction:
203+
204+
```ts
205+
// src/app/app.config.ts
206+
import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core';
207+
import { provideRouter } from '@angular/router';
208+
209+
import { routes } from './app.routes';
210+
import { FormoAnalyticsService } from './services/formo-analytics.service';
211+
212+
export const appConfig: ApplicationConfig = {
213+
providers: [
214+
provideRouter(routes),
215+
provideAppInitializer(() => inject(FormoAnalyticsService).init()),
216+
],
217+
};
218+
```
219+
220+
<Note>
221+
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.
222+
</Note>
223+
146224
## Identify users
147225

148226
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
223301
></script>
224302
```
225303
</Tab>
304+
<Tab title="Angular">
305+
Inject the analytics service and call `identify()` from the same place you discover the wallet address (for example, a `WalletService` that drives `eth_requestAccounts`):
306+
307+
```ts
308+
import { Injectable, inject, signal } from '@angular/core';
309+
import { FormoAnalyticsService } from './services/formo-analytics.service';
310+
import type { Address } from 'viem';
311+
312+
@Injectable({ providedIn: 'root' })
313+
export class WalletService {
314+
private readonly formo = inject(FormoAnalyticsService);
315+
readonly address = signal<Address | null>(null);
316+
317+
async connect(): Promise<void> {
318+
const [account] = await window.ethereum!.request({ method: 'eth_requestAccounts' });
319+
this.address.set(account);
320+
this.formo.identify(account);
321+
}
322+
}
323+
```
324+
325+
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.
326+
</Tab>
226327
</Tabs>
227328

228329
## Track events
@@ -335,6 +436,27 @@ You can [track volume, revenue, and points](/data/events/track#tracking-volume,-
335436
>
336437
examples/with-porto
337438
</Card>
439+
<Card
440+
title="Crossmint"
441+
icon="file-code"
442+
href="https://github.com/getformo/examples/tree/main/with-crossmint"
443+
>
444+
Next.js + Crossmint embedded smart wallets. Manual Formo instrumentation (no EIP-1193 / wagmi).
445+
</Card>
446+
<Card
447+
title="Openfort"
448+
icon="file-code"
449+
href="https://github.com/getformo/examples/tree/main/with-openfort"
450+
>
451+
Vite + React + Openfort with Shield encryption sessions (Express backend) and an Aave supply/withdraw demo.
452+
</Card>
453+
<Card
454+
title="Angular"
455+
icon="file-code"
456+
href="https://github.com/getformo/examples/tree/main/with-angular"
457+
>
458+
Angular 21 + bare EIP-1193 + viem. Uses the framework-agnostic SDK core.
459+
</Card>
338460
<Card
339461
title="Farcaster Mini App"
340462
icon="file-code"

0 commit comments

Comments
 (0)