-
Notifications
You must be signed in to change notification settings - Fork 0
P-2202: Examples: Add Angular example for the Formo Web SDK #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| ``` | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a non-null assertion ( |
||
| } | ||
| ``` | ||
|
|
||
| #### 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
| <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: | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a non-null assertion ( |
||
| } | ||
| ``` | ||
|
|
||
| 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 | ||
|
|
@@ -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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| </Card> | ||
| <Card | ||
| title="Farcaster Mini App" | ||
| icon="file-code" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency with the rest of the documentation which primarily uses
npm, it's better to usenpmcommands here as well.