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
49 changes: 48 additions & 1 deletion with-react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This is an example React Native app demonstrating the [@formo/react-native-analy
- **Semantic Events**: Track revenue, points, and volume events
- **Automatic Wallet Event Tracking**: Wallet connect, disconnect, signatures, and transactions are automatically tracked via wagmi integration
- **Consent Management**: Built-in opt-out/opt-in functionality for GDPR compliance
- **Android Install Attribution**: `react-native-play-install-referrer` is installed so the SDK can read the Play Store install referrer. Real referrer data only comes back for installs that actually came from the Play Store — a dev build or sideload reports an organic install.

## Getting Started

Expand All @@ -20,6 +21,7 @@ This is an example React Native app demonstrating the [@formo/react-native-analy
- Xcode (for iOS development)
- Android Studio (for Android development)
- iOS Simulator or Android Emulator (Expo Go does not support custom native modules)
- [Foundry](https://getfoundry.sh) — optional, only for the Mock Wallet buttons (see step 4)

### Installation

Expand Down Expand Up @@ -50,7 +52,42 @@ EXPO_PUBLIC_FORMO_WRITE_KEY=your_formo_write_key

You can get your Formo write key from [app.formo.so](https://app.formo.so).

4. Start the development server:
4. (Optional) Start local chains for the Mock Wallet:

The **Mock Wallet** buttons — Send Tx and Sign Message — need an RPC node that has
the test account unlocked. wagmi's `mock` connector only intercepts a handful of
methods and proxies the rest (`eth_sendTransaction`, `eth_sign`) straight to the
transport. Public RPCs hold no keys, so against them Send Tx fails with
`-32601 rpc method is unsupported` and Sign Message with `unknown account`.

Start an [anvil](https://getfoundry.sh) node on the chain you want to test:

Install Foundry first if you don't have it — see the
[installation guide](https://getfoundry.sh/introduction/installation). Then:

```bash
anvil --chain-id 84532 # Base Sepolia → localhost:8545
anvil --chain-id 11155420 --port 8546 # OP Sepolia → localhost:8546
```

Anvil pre-funds and unlocks `0xf39Fd6e5...fFb92266` — the same address the mock
connector is configured with. Chain state is in-memory and resets whenever anvil
restarts.

There is nothing to configure: `config/wagmi.ts` derives the RPC host from
Expo's dev-server address, which is the machine running Metro — the same one
running anvil. That matters because `localhost` only reaches the dev machine
from the iOS simulator; an Android emulator resolves it to the emulator itself,
and a physical device to the device.

The second node is only needed if you want to send or sign **while on OP
Sepolia** — switching chains itself works without any node, since the mock
connector handles `wallet_switchEthereumChain` internally.

You can skip this step entirely if you only need the **Direct SDK Testing**
buttons, which call the SDK directly with no chain involved.

5. Start the development server:

```bash
pnpm start
Expand Down Expand Up @@ -95,6 +132,16 @@ cd ios && pod install && cd ..

Make sure you have the Android SDK installed and `ANDROID_HOME` environment variable set.

### Send Tx or Sign Message fails

A connection error means anvil isn't running — start it as described in
[step 4](#installation). Nothing else needs configuring.

`-32601 rpc method is unsupported` or `unknown account` means the Mock Wallet
reached a public RPC instead of a local node. Check that `config/wagmi.ts` still
overrides `rpcUrls` on the chain objects — overriding `transports` alone has no
effect, since the mock connector reads the URL off the chain.

## Project Structure

```
Expand Down
28 changes: 28 additions & 0 deletions with-react-native/__tests__/wagmi.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
// Stands in for the Metro dev server. Deliberately NOT localhost: the fallback
// when hostUri is absent is localhost, so a hardcoded localhost would satisfy
// the host assertion below and the regression guard would never fail.
jest.mock("expo-constants", () => ({
__esModule: true,
default: { expoConfig: { hostUri: "192.168.1.5:8081" } },
}));

import { chains, wagmiConfig } from "../config/wagmi";

describe("Wagmi Configuration", () => {
Expand All @@ -17,6 +25,26 @@ describe("Wagmi Configuration", () => {
it("should have exactly 2 chains configured", () => {
expect(chains).toHaveLength(2);
});

// The mock connector reads chain.rpcUrls.default.http[0] directly and
// ignores wagmi's `transports`, so the local-node override has to land
// here or Send Tx / Sign Message silently talk to the public RPC instead.
it("points each chain at a local node rather than the public RPC", () => {
for (const chain of chains) {
expect(chain.rpcUrls.default.http[0]).toMatch(/^http:\/\/[^/]+:854[56]$/);
}
});

// Regression guard: hardcoding localhost only reaches the dev machine from
// the iOS simulator. An Android emulator resolves it to the emulator and a
// physical device to itself, so the host must come from Expo's dev-server
// address — the machine actually running anvil.
it("derives the RPC host from the Expo dev server, not localhost", () => {
for (const chain of chains) {
expect(chain.rpcUrls.default.http[0]).toContain("//192.168.1.5:");
expect(chain.rpcUrls.default.http[0]).not.toContain("localhost");
}
});
});

describe("wagmiConfig", () => {
Expand Down
46 changes: 45 additions & 1 deletion with-react-native/config/wagmi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { http, createConfig } from "wagmi";
import type { Chain } from "viem";
import Constants from "expo-constants";
import { baseSepolia, optimismSepolia } from "wagmi/chains";
import { walletConnect } from "wagmi/connectors";
import { mock } from "@wagmi/core";
Expand All @@ -16,7 +18,49 @@ const metadata = {
// Mock wallet address for testing (Hardhat default account #0)
const MOCK_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" as const;

export const chains = [baseSepolia, optimismSepolia] as const;
// The `mock` connector only intercepts a few RPC methods; everything else —
// including eth_sendTransaction and eth_sign — is forwarded to a real node.
// Public RPCs hold no keys, so against them "Send Tx" fails with -32601 (viem
// then retries as wallet_sendTransaction, also unsupported) and "Sign Message"
// fails with "unknown account". Pointing at a local anvil that has MOCK_ADDRESS
// unlocked fixes both:
//
// anvil --chain-id 84532 # Base Sepolia
// anvil --chain-id 11155420 --port 8546 # OP Sepolia
//
// The override has to happen on the CHAIN, not on `transports`. The mock
// connector never reads wagmi's transports — it takes the URL straight off
// `chain.rpcUrls.default.http[0]` (see @wagmi/core/src/connectors/mock.ts,
// `getProvider`). Overriding only the transport silently leaves the mock wallet
// pointed at the public endpoint.
//
// These are derived rather than env-configured because pointing at a local node
// costs nothing when one isn't running: no hook in this app reads chain state,
// so the only calls that reach these URLs are the Mock Wallet's Send Tx and
// Sign Message — which don't work without a local node either way. Without
// anvil you get a connection error instead of -32601; both mean "start anvil".
//
// The host cannot be hardcoded to localhost: that only resolves to the dev
// machine on the iOS simulator. An Android emulator resolves localhost to
// itself (the host is reachable at 10.0.2.2), and a physical device resolves it
// to the device. Expo reports the machine serving the JS bundle in `hostUri` —
// necessarily the same machine the README says to run anvil on — so use that
// and every target reaches the right host. Falls back to localhost when
// hostUri is absent (production builds, where none of this applies anyway).
const devHost = Constants.expoConfig?.hostUri?.split(":")[0] ?? "localhost";
const localRpc = (port: number) => `http://${devHost}:${port}`;
const withRpcUrl = <T extends Chain>(chain: T, url: string): T => ({
...chain,
rpcUrls: {
...chain.rpcUrls,
default: { ...chain.rpcUrls.default, http: [url] },
Comment on lines +55 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve rpcUrls in the wagmi test mock

pnpm test now fails before wagmi.test.ts can run: its Jest mock for wagmi/chains provides only id and name (jest.setup.js lines 62–65), so this dereference throws while importing config/wagmi.ts. Add the required rpcUrls.default shape to that mock (or make the helper tolerate it) so the existing configuration tests remain runnable.

Useful? React with 👍 / 👎.

},
});

export const chains = [
withRpcUrl(baseSepolia, localRpc(8545)),
withRpcUrl(optimismSepolia, localRpc(8546)),
] as const;

export const wagmiConfig = createConfig({
chains,
Expand Down
18 changes: 14 additions & 4 deletions with-react-native/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,19 @@ jest.mock("wagmi", () => ({
WagmiProvider: ({ children }) => children,
}));

// rpcUrls is part of viem's Chain shape and config/wagmi.ts overrides it to
// point the mock wallet at a local node, so the stubs need it to stay realistic.
jest.mock("wagmi/chains", () => ({
baseSepolia: { id: 84532, name: "Base Sepolia" },
optimismSepolia: { id: 11155420, name: "OP Sepolia" },
baseSepolia: {
id: 84532,
name: "Base Sepolia",
rpcUrls: { default: { http: ["https://sepolia.base.org"] } },
},
optimismSepolia: {
id: 11155420,
name: "OP Sepolia",
rpcUrls: { default: { http: ["https://sepolia.optimism.io"] } },
},
}));

jest.mock("wagmi/connectors", () => ({
Expand All @@ -74,8 +84,8 @@ jest.mock("@tanstack/react-query", () => ({
QueryClientProvider: ({ children }) => children,
}));

// Mock @formo/react-native-analytics
jest.mock("@formo/react-native-analytics", () => ({
// Mock @formo/analytics-react-native
jest.mock("@formo/analytics-react-native", () => ({
FormoAnalyticsProvider: ({ children }) => children,
useFormo: () => ({
track: jest.fn(),
Expand Down
13 changes: 3 additions & 10 deletions with-react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"dependencies": {
"@babel/runtime": "^7.28.6",
"@formo/analytics-react-native": "^0.1.6",
"@formo/analytics-react-native": "^1.0.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Install the Android install-referrer peer

The updated SDK declares react-native-play-install-referrer as an optional peer in the generated lockfile, but it is absent from this app's dependencies. Optional peers are not installed or linked into the Android build, so this example cannot exercise the Android install-attribution capability cited for the 1.0.0 upgrade; add the peer dependency and rebuild the development client if that feature is intended to be demonstrated.

Useful? React with 👍 / 👎.

"@react-native-async-storage/async-storage": "^1.21.0",
"@tanstack/react-query": "^5.90.20",
"@wagmi/connectors": "5.7.7",
Expand All @@ -25,6 +25,7 @@
"ethereum-cryptography": "3.2.0",
"expo": "~52.0.0",
"expo-application": "~6.0.0",
"expo-constants": "~17.0.8",
"expo-crypto": "~14.0.0",
"expo-device": "~7.0.0",
"expo-linking": "~7.0.0",
Expand All @@ -35,6 +36,7 @@
"react-native": "0.76.0",
"react-native-get-random-values": "^1.11.0",
"react-native-modal": "14.0.0-rc.1",
"react-native-play-install-referrer": "^1.1.9",
"react-native-safe-area-context": "^4.12.0",
"react-native-screens": "~4.0.0",
"react-native-svg": "^15.15.1",
Expand All @@ -53,14 +55,5 @@
"jest-expo": "~52.0.0",
"typescript": "~5.3.3"
},
"pnpm": {
"overrides": {
"h3": "^1.15.9",
"node-forge": "^1.4.0",
"@wagmi/core": "2.16.4",
"@xmldom/xmldom": "0.8.13",
"undici": "6.27.0"
}
},
"private": true
}
Loading
Loading