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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ next-env.d.ts
# IDE
.vscode
.idea

# agents
.claude/
.agents/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Check out the [SDK docs](https://docs.formo.so/sdks/web) for full installation i
- [with-porto](./with-porto) - Next.js with Porto wallet
- [with-tempo](./with-tempo) - Next.js with Tempo Accounts wallet
- [with-openfort](./with-openfort) - Vite + React with Openfort embedded wallets (Shield) and Aave, plus an Express backend for Shield encryption sessions
- [with-crossmint](./with-crossmint) - Next.js with Crossmint embedded wallets (manual Formo event instrumentation)

## Blockchain Platforms

Expand Down
11 changes: 11 additions & 0 deletions with-crossmint/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Crossmint client-side API key — get yours at https://docs.crossmint.com/introduction/platform/api-keys
# Required scopes: users.create, users.read, wallets.read, wallets.create,
# wallets:transactions.create, wallets:transactions.sign, wallets:balance.read, wallets.fund
NEXT_PUBLIC_CROSSMINT_API_KEY=

# Chain the embedded wallet is created on.
# See all supported chains: https://docs.crossmint.com/introduction/supported-chains
NEXT_PUBLIC_CHAIN=base-sepolia

# Formo Analytics write key — get yours at https://app.formo.so
NEXT_PUBLIC_FORMO_WRITE_KEY=
41 changes: 41 additions & 0 deletions with-crossmint/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files
.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
173 changes: 173 additions & 0 deletions with-crossmint/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Formo + Crossmint Example

This example demonstrates how to integrate the [Formo Analytics SDK](https://formo.so/) with [Crossmint](https://www.crossmint.com/products/wallet-infrastructure) embedded wallets.

It is based on the [Crossmint Wallets Quickstart](https://github.com/Crossmint/wallets-quickstart) and adds Formo for wallet-event analytics.

## Why this example is different

Crossmint embedded wallets are **smart wallets** created from an email or Google login. Unlike browser-extension wallets (MetaMask) or wagmi-based integrations (Privy, Openfort), they expose **no `window.ethereum` provider and no wagmi config**. That means Formo's automatic wallet-event capture has nothing to hook into.

So this example instruments **every wallet event manually** with Formo's event API — which is exactly the pattern you need for any embedded / smart-contract wallet:

- `formo.identify()` / `formo.connect()` when the wallet is ready
- `formo.disconnect()` on logout
- `formo.signature()` around a message signing
- `formo.transaction()` around a transfer
- `formo.track()` for custom events

Compare this with [`with-openfort`](../with-openfort) and [`with-privy`](../with-privy), where Openfort/Privy bridge their wallets into wagmi and Formo auto-captures those events for you.

## Features

- **Crossmint Embedded Wallets**: Passwordless smart wallets created via email or Google login
- **USDXM transfers**: Send Crossmint's test stablecoin on Base Sepolia
- **Manual Formo instrumentation**: Every wallet event wired by hand — the core of this example
- **Formo Event Tester**: A UI panel to trigger `signature` and `track` events on demand
- **Gas sponsored by Crossmint**: Transactions are gasless — no testnet ETH required

## Tech Stack

- [Next.js 15](https://nextjs.org/) (App Router) + [React 19](https://react.dev/)
- [TypeScript](https://www.typescriptlang.org/) + [Tailwind CSS](https://tailwindcss.com/)
- [@crossmint/client-sdk-react-ui](https://docs.crossmint.com/wallets/quickstarts/react) — embedded wallets
- [@formo/analytics](https://docs.formo.so/) — web3 analytics

## Project Structure

```
with-crossmint/
├── app/
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Landing page / dashboard switch
│ └── providers.tsx # Crossmint + Formo providers
├── components/
│ ├── formo-bridge.tsx # Emits identify / connect / disconnect
│ ├── formo-event-tester.tsx # Emits signature / track on demand
│ ├── transfer.tsx # Emits transaction / track on a transfer
│ ├── dashboard.tsx # Wallet dashboard
│ └── ... # balance, activity, landing-page, etc.
└── lib/
└── chain.ts # Base Sepolia chain ID for Formo events
```

## Prerequisites

1. **Crossmint account**: Create a project at the [Crossmint Dashboard](https://www.crossmint.com/console) and create a **client-side API key**. It needs these scopes:
`users.create`, `users.read`, `wallets.read`, `wallets.create`, `wallets:transactions.create`, `wallets:transactions.sign`, `wallets:balance.read`, `wallets.fund`.
2. **Formo account**: Get your write key at [app.formo.so](https://app.formo.so).

> No crypto wallet or testnet tokens are required — the embedded wallet is created on login, USDXM is funded in-app, and Crossmint sponsors gas.

## Quick Start

### 1. Clone the repository

```bash
git clone https://github.com/getformo/examples.git
cd examples/with-crossmint
```

### 2. Configure environment variables

```bash
cp .env.example .env # then add your keys
```

### 3. Install and run

```bash
pnpm install
pnpm dev # runs on http://localhost:3000
```

Visit `http://localhost:3000`, log in with email or Google, and a wallet is created for you.

## Environment Variables

| Variable | Description | Required |
|----------|-------------|----------|
| `NEXT_PUBLIC_CROSSMINT_API_KEY` | Crossmint client-side API key (see scopes above) | Yes |
| `NEXT_PUBLIC_CHAIN` | Chain the wallet is created on (`base-sepolia`) | Yes |
| `NEXT_PUBLIC_FORMO_WRITE_KEY` | Your Formo Analytics write key | No* |

\* If `NEXT_PUBLIC_FORMO_WRITE_KEY` is omitted the app still runs, just without analytics.

> **Changing the chain?** Update `NEXT_PUBLIC_CHAIN` **and** the `CHAIN_ID` in [`lib/chain.ts`](./lib/chain.ts) — Formo events need the numeric EVM chain ID.

## How It Works

### Provider setup (`app/providers.tsx`)

`FormoAnalyticsProvider` wraps the Crossmint providers. Because the embedded
wallet has no `window.ethereum` and no wagmi config, autocapture is turned
**off** — every wallet event is emitted by hand:

```tsx
<FormoAnalyticsProvider
writeKey={formoWriteKey}
options={{
autocapture: false, // no wagmi / EIP-1193 — events are emitted manually
evm: false, // don't wrap an unrelated injected wallet
tracking: true, // track on localhost too
logger: { enabled: true, levels: ["info", "warn", "error"] },
}}
>
```

### Events in this example

This example covers every event type in the [Formo events spec](https://docs.formo.so/data/events/overview).

| Event | How it's tracked | Where |
|-------|------------------|-------|
| `page` | Automatic on page load | — |
| `identify` | Manual — on wallet ready, with the wallet address + Crossmint user id | `components/formo-bridge.tsx` |
| `connect` | Manual — on login when the embedded wallet is ready | `components/formo-bridge.tsx` |
| `disconnect` | Manual — on logout | `components/formo-bridge.tsx` |
| `signature` | Manual — around `wallet.signMessage()` | `components/formo-event-tester.tsx` |
| `transaction` | Manual — around `wallet.send()` (`started` → `broadcasted` / `rejected`) | `components/transfer.tsx` |
| `track` | Manual — `crossmint_transfer` + `event_tester_clicked` | `components/transfer.tsx`, `components/formo-event-tester.tsx` |
| `chain` | **N/A** — Crossmint embedded wallets are single-chain (fixed at `createOnLogin`); there is no network switch to capture | — |
| `detect` | **N/A** — `detect` identifies an injected wallet provider; a smart wallet exposes none | — |

### Lifecycle events (`components/formo-bridge.tsx`)

`FormoCrossmintBridge` is a render-less component mounted inside the providers.
It watches Crossmint's `useWallet()` and `useCrossmintAuth()` and emits
`identify` + `connect` once the wallet is ready, and `disconnect` on logout.

### Transaction events (`components/transfer.tsx`)

The Transfer card wraps `wallet.send(...)` with `formo.transaction()` calls
(`started` before, `broadcasted` with the tx hash after, `rejected` on error)
plus a custom `formo.track("crossmint_transfer", ...)` event.

### Event Tester (`components/formo-event-tester.tsx`)

A panel with buttons to fire events on demand:

- **Sign message** → `signature` event — signs a message with the embedded wallet (free, no gas).
- **Track custom event** → `track` event.

## Verifying it works

With the SDK logger enabled you'll see events in the browser console, and they
appear in your [Formo dashboard](https://app.formo.so):

1. Log in → `page`, `identify`, `connect`.
2. Add money (USDXM) then run a transfer → `transaction` + `crossmint_transfer`.
3. Event Tester → `signature`, `track`.
4. Log out → `disconnect`.

## Resources

- [Formo Documentation](https://docs.formo.so)
- [Formo SDK Installation](https://docs.formo.so/sdks/web#installation)
- [Formo Events Overview](https://docs.formo.so/data/events/overview)
- [Crossmint Wallets Documentation](https://docs.crossmint.com/wallets/quickstarts/react)
- [Crossmint Wallets Quickstart (base template)](https://github.com/Crossmint/wallets-quickstart)

## License

MIT
Binary file added with-crossmint/app/favicon.ico
Binary file not shown.
118 changes: 118 additions & 0 deletions with-crossmint/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
@import "tailwindcss";
@import "tw-animate-css";


:root {
--radius: 0.625rem;
--background: oklch(96.83% 0.0069 247.9);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(21% 0.034 264.665);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(68.76% 0.1856 150.65);
--accent-foreground: oklch(60.29% 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}

@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}

button:not([disabled]),
[role="button"]:not([disabled]) {
cursor: pointer;
}

.filter-green {
filter: brightness(0) saturate(100%) invert(29%) sepia(66%) saturate(2945%) hue-rotate(91deg) brightness(95%) contrast(104%);
}

.filter-blue {
filter: brightness(0) saturate(100%) invert(37%) sepia(90%) saturate(2066%) hue-rotate(210deg) brightness(97%) contrast(98%);
}

/* Simple page transitions */
.page-transition-enter {
opacity: 0;
transform: scale(0.95);
}
.page-transition-enter-active {
opacity: 1;
transform: scale(1);
transition: opacity 400ms ease-out, transform 400ms ease-out;
}
.page-transition-exit {
opacity: 1;
transform: scale(1);
}
.page-transition-exit-active {
opacity: 0;
transform: scale(1.05);
transition: opacity 400ms ease-in, transform 400ms ease-in;
}
}
36 changes: 36 additions & 0 deletions with-crossmint/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Providers } from "@/app/providers";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "Formo + Crossmint Example",
description:
"Example app demonstrating Crossmint embedded wallets with the Formo Analytics SDK",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Providers>{children}</Providers>
</body>
</html>
);
}
Loading
Loading