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
6 changes: 2 additions & 4 deletions with-react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,8 @@ function MyScreen() {
const formo = useFormo();

useEffect(() => {
formo.screen("MyScreen", {
category: "main",
source: "navigation",
});
// screen(name, category?, properties?)
formo.screen("MyScreen", "main", { source: "navigation" });
}, [formo]);

return <View>...</View>;
Expand Down
20 changes: 17 additions & 3 deletions with-react-native/__tests__/formo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,25 @@ describe("Formo Configuration", () => {
expect(typeof options).toBe("object");
});

it("should include app configuration", () => {
it("should take app identity from app.json, not a hardcoded copy", () => {
// These reach Formo as `app_version` and as the mobile `origin`. A
// hardcoded value that drifts from app.json reports the wrong version on
// every event indefinitely, so assert they are the SAME value rather than
// pinning a literal here (which is how they drifted in the first place).
const appJson = require("../app.json").expo;
const options = createFormoOptions(mockWagmiConfig, mockQueryClient);

expect(options.app).toBeDefined();
expect(options.app!.name).toBe("Formo Analytics Demo");
expect(options.app!.version).toBe("1.1.0");
expect(options.app!.name).toBe(appJson.name);
expect(options.app!.version).toBe(appJson.version);
expect(options.app!.bundleId).toBe(appJson.ios.bundleIdentifier);
});

it("keeps the iOS bundle id and Android package in sync", () => {
// The SDK sends one bundleId for both platforms, so a mismatch would make
// the same app report two different origins depending on the platform.
const appJson = require("../app.json").expo;
expect(appJson.ios.bundleIdentifier).toBe(appJson.android.package);
});

it("should include wagmi configuration", () => {
Expand Down
23 changes: 19 additions & 4 deletions with-react-native/config/formo.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
import type { Options } from "@formo/analytics-react-native";
import Constants from "expo-constants";
import type { Config } from "wagmi";
import type { QueryClient } from "@tanstack/react-query";

// Get your write key from https://app.formo.so
export const FORMO_WRITE_KEY =
process.env.EXPO_PUBLIC_FORMO_WRITE_KEY || "YOUR_FORMO_WRITE_KEY";

// Read app identity from the Expo config (app.json) rather than hardcoding it,
// so there is one source of truth. These reach Formo as `app_version` and as
// the mobile `origin`, which means a hardcoded value that drifts from app.json
// silently reports the wrong version on every event forever.
const expoConfig = Constants.expoConfig;

// Base Formo Analytics configuration (without wagmi)
export const baseFormoOptions: Omit<Options, "wagmi"> = {
// App information for context enrichment
// App information for context enrichment.
//
// Worth setting explicitly even though the SDK can auto-detect: in Expo Go
// the native modules report EXPO GO's identity (its bundle id and version),
// not your app's, and on React Native Web nothing resolves a bundle id at
// all. Configuring these keeps dev and web builds reporting the real app.
app: {
name: "Formo Analytics Demo",
version: "1.1.0",
bundleId: "com.formo.analytics.demo",
name: expoConfig?.name ?? "Formo Analytics Demo",
version: expoConfig?.version ?? "0.0.0",
bundleId:
expoConfig?.ios?.bundleIdentifier ??
expoConfig?.android?.package ??
"com.formo.analytics.demo",
},

// Event batching configuration
Expand Down
7 changes: 7 additions & 0 deletions with-react-native/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ jest.mock("@formo/analytics-react-native", () => ({
}),
}));

// Mock expo-constants with the real app.json, so config/formo.ts is exercised
// through its actual source of truth rather than its fallbacks.
jest.mock("expo-constants", () => ({
__esModule: true,
default: { expoConfig: require("./app.json").expo },
}));

// Mock react-native-safe-area-context
jest.mock("react-native-safe-area-context", () => ({
SafeAreaProvider: ({ children }) => children,
Expand Down
25 changes: 24 additions & 1 deletion with-react-native/metro.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { getDefaultConfig } = require("expo/metro-config");
const fs = require("fs");
const path = require("path");

const config = getDefaultConfig(__dirname);
Expand All @@ -9,6 +10,22 @@ const sdkPath = path.resolve(__dirname, "../../sdk-react-native");
const projectRoot = __dirname;
const projectNodeModules = path.resolve(projectRoot, "node_modules");

// Is the SDK a local link rather than an npm install? A `pnpm add link:` (or
// `npm link`) leaves a symlink in node_modules; an npm install leaves a real
// directory. Only in the link case should Metro resolve the package to the
// sibling checkout's TypeScript source.
const isSdkLinked = (() => {
try {
const installed = path.resolve(projectNodeModules, SDK_PACKAGE_NAME);
return (
fs.lstatSync(installed).isSymbolicLink() &&
fs.existsSync(path.resolve(sdkPath, "src/index.ts"))
);
} catch {
return false; // not installed at all, or no sibling checkout
}
})();

// Watch the SDK directory for changes
// Note: sdk-react-native/.watchmanconfig excludes node_modules and lib
config.watchFolders = [sdkPath];
Expand Down Expand Up @@ -44,7 +61,13 @@ config.resolver.resolveRequest = (context, moduleName, platform) => {
// redirect below keys on originModulePath being inside sdkPath, which only
// happens when the SDK is consumed as source. It means edits to the SDK hot
// reload with no rebuild — the point of linking it in the first place.
if (moduleName === SDK_PACKAGE_NAME) {
// ONLY when the package is actually linked. Redirecting unconditionally
// would hijack an npm-installed SDK for anyone who happens to have a sibling
// sdk-react-native checkout — silently running unreleased local code instead
// of the pinned version — and would resolve to a non-existent path for anyone
// who does not. isSdkLinked is computed once at config load; swapping between
// a link and an npm install already requires a Metro restart.
if (moduleName === SDK_PACKAGE_NAME && isSdkLinked) {
return {
filePath: path.resolve(sdkPath, "src/index.ts"),
type: "sourceFile",
Expand Down
Loading