Skip to content

P-1114 Add Formo + Turnkey embedded wallet integration example#7

Open
yosriady wants to merge 20 commits intomainfrom
claude/add-turnkey-example-RtpGc
Open

P-1114 Add Formo + Turnkey embedded wallet integration example#7
yosriady wants to merge 20 commits intomainfrom
claude/add-turnkey-example-RtpGc

Conversation

@yosriady
Copy link
Contributor

@yosriady yosriady commented Mar 19, 2026

Summary

This PR adds a complete example application demonstrating integration between Formo Analytics SDK and Turnkey embedded wallets. The example shows how to authenticate users with passkeys, connect to embedded wallets via wagmi, and automatically track wallet events with Formo.

Key Changes

  • Main demo page (src/app/page.tsx): Full-featured UI with wallet connection, status display, and event testing capabilities

    • Passkey authentication flow with Turnkey
    • Wallet status display (address, balance, chain ID)
    • Manual event tracking (page views, custom events)
    • Wallet action buttons (sign message, send transaction)
    • Auto-tracked events reference card
  • Custom wagmi connector (src/config/turnkey-connector.ts): Wraps Turnkey's @turnkey/viem to integrate with wagmi

    • Implements wagmi connector interface for Turnkey embedded wallets
    • Handles account creation, connection, and disconnection
    • Enables standard wagmi hooks (useAccount, useSignMessage, etc.)
  • Provider setup (src/app/providers.tsx): Configures provider nesting order

    • TurnkeyProvider for passkey authentication
    • WagmiProvider for wallet interactions
    • QueryClientProvider for async state management
    • FormoAnalyticsProvider with wagmi integration and autocapture enabled
  • Configuration files:

    • src/config/wagmi.ts: Wagmi config with support for Ethereum, Sepolia, Polygon, Arbitrum, Optimism, and Base
    • src/app/layout.tsx: Root layout with metadata
    • src/app/globals.css: Tailwind CSS setup
    • next.config.ts: Next.js configuration
    • postcss.config.mjs: PostCSS configuration
    • tsconfig.json: TypeScript configuration
    • package.json: Dependencies and scripts
    • .env.example: Environment variable template
  • Documentation (README.md): Comprehensive guide covering setup, features, project structure, and integration details

Notable Implementation Details

  • Event tracking: Formo automatically captures wallet events (connect, disconnect, chain changes, signatures, transactions) via wagmi integration when autocapture: true
  • Manual disconnect tracking: Includes a useEffect hook to manually track Turnkey logout events since Turnkey's logout doesn't trigger wagmi's disconnect status
  • User identification: Captures Turnkey user ID and organization ID for Formo analytics
  • React 19 compatibility: Uses @ts-ignore comments for @turnkey/sdk-react which is built for React 18
  • Responsive UI: Tailwind CSS-based responsive grid layout with dark theme

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS


Open with Devin

Adds a new example demonstrating Formo Analytics SDK integration with
Turnkey embedded wallets. Includes a custom wagmi connector wrapping
@turnkey/viem for signing, TurnkeyProvider for passkey auth, and
FormoAnalyticsProvider for automatic wallet event tracking.

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@yosriady yosriady changed the title Add Formo + Turnkey embedded wallet integration example P-1114 Add Formo + Turnkey embedded wallet integration example Mar 19, 2026
@linear
Copy link

linear bot commented Mar 19, 2026

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new example application that demonstrates the powerful synergy between Formo Analytics and Turnkey embedded wallets. It provides a practical, working model for developers to understand how to integrate passkey-based authentication with web3 wallet interactions, ensuring that all relevant wallet activities are automatically captured for analytics purposes. The example highlights a robust architecture for modern web3 applications, focusing on user experience and data insights.

Highlights

  • Formo + Turnkey Integration Example: Added a complete Next.js example application demonstrating the integration of Formo Analytics SDK with Turnkey embedded wallets, showcasing passkey authentication, wagmi wallet connection, and automatic event tracking.
  • Main Demo Page: Implemented a full-featured UI in src/app/page.tsx for wallet connection, status display, manual event tracking, and wallet actions (sign message, send transaction), along with a reference card for auto-tracked events.
  • Custom Wagmi Connector: Created a custom wagmi connector in src/config/turnkey-connector.ts to wrap Turnkey's @turnkey/viem library, enabling seamless integration of Turnkey embedded wallets with wagmi hooks.
  • Provider Setup: Configured provider nesting order in src/app/providers.tsx for TurnkeyProvider, WagmiProvider, QueryClientProvider, and FormoAnalyticsProvider, with autocapture enabled for wallet events.
  • Comprehensive Documentation: Included a detailed README.md providing setup instructions, feature descriptions, project structure, and explanations of how Turnkey authentication and Formo analytics work within the example.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive example application for integrating Formo Analytics with Turnkey embedded wallets. The code is well-structured and provides a clear demonstration of the integration. I've identified a couple of minor issues on the main page: an unused variable and an incorrect GitHub link in the footer. My review includes suggestions to address these points for improved code quality and correctness.

});

// Register the connector with wagmi and connect
const augmentedConfig = wagmiConfig;

Choose a reason for hiding this comment

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

medium

The augmentedConfig variable is declared but is not used anywhere in the function. It can be removed to improve code clarity.

</p>
<p>
<a
href="https://github.com/getformo/formo-example-turnkey"

Choose a reason for hiding this comment

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

medium

The "View on GitHub" link points to a non-existent repository. Since this example is part of the getformo/examples repository, the link should be updated to point to the correct directory within that repository.

Suggested change
href="https://github.com/getformo/formo-example-turnkey"
href="https://github.com/getformo/examples/tree/main/with-turnkey"

devin-ai-integration[bot]

This comment was marked as resolved.

Comment on lines +60 to +64
async getProvider(): Promise<EIP1193Provider> {
// Return a minimal EIP-1193 provider that delegates to viem
// For most wagmi usage the connector methods above are sufficient
return undefined as unknown as EIP1193Provider;
},

This comment was marked as outdated.

- Implement proper EIP-1193 provider in turnkey-connector.ts that
  handles personal_sign, eth_signTypedData_v4, eth_sendTransaction,
  and forwards unknown RPC calls to the chain's public endpoint
- Remove unused augmentedConfig variable and wagmiConfig import
- Fix GitHub link to point to getformo/examples/tree/main/with-turnkey

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
devin-ai-integration[bot]

This comment was marked as resolved.

- Update @turnkey/sdk-browser to ^5.15.2, @turnkey/sdk-react to ^5.5.6,
  @turnkey/viem to ^0.14.26 (^3.0.0 doesn't exist on npm)
- Replace getCurrentUser() with getSession() (v5 API change)
- Replace getActiveClient() with client from useTurnkey() (v5 API change)
- Fix wagmi v3 connect() withCapabilities type compatibility
- Fix viem TransactionSerializable union type compatibility

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
sentry[bot]

This comment was marked as resolved.

The provider returned by getProvider() was missing on(), removeListener(),
and emit() methods required by the EIP-1193 standard. Wagmi calls these
to subscribe to accountsChanged and other provider events, causing a
TypeError at runtime.

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
devin-ai-integration[bot]

This comment was marked as resolved.

claude added 2 commits March 20, 2026 01:30
Previously each getProvider() call created a new provider with fresh
event listeners, causing wagmi's event subscriptions to be silently
lost. Now the provider and its listeners map are cached at the
connector level and cleared on disconnect.

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
Rewrites the connector to use Turnkey's official EIP-1193 provider
package, following the approach from docs.turnkey.com/wallets/wagmi.

This eliminates ~150 lines of hand-rolled provider code (signing,
transaction building, gas estimation, nonce management, RPC
forwarding, event emitter) and replaces it with the battle-tested
@turnkey/eip-1193-provider which handles all of this out of the box.

Changes:
- Replace @turnkey/viem with @turnkey/eip-1193-provider
- Connector now accepts walletId instead of signWith address
- Add switchChain support via wallet_switchEthereumChain
- Remove manual wallet account fetching from page.tsx
- Update README to reference the official Turnkey wagmi docs

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
devin-ai-integration[bot]

This comment was marked as resolved.

The fire-and-forget connect() mutate function doesn't throw on failure,
so the existing try/catch never caught wagmi connection errors and
setTurnkeyUser ran before connection was confirmed. Switching to
connectAsync ensures errors propagate to the catch block and
setTurnkeyUser only runs on successful connection.

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
devin-ai-integration[bot]

This comment was marked as resolved.

Formo's autocapture (enabled in providers.tsx) already monitors wagmi
state changes and fires disconnect events. The manual useEffect bridge
watching isConnected caused duplicate disconnect events. Removed the
bridge and the now-unused prevAuthRef/useRef import.

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
@yosriady
Copy link
Contributor Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive example application for integrating Formo with Turnkey embedded wallets. The code is well-structured and includes good documentation. My review identifies a critical bug in the custom turnkeyConnector where it fails to correctly extract the EIP-1193 provider, which would prevent the connector from functioning. I've also included suggestions to improve the display of wallet balances to avoid precision loss, to refactor the main page component for better maintainability, and to reconsider the use of force-dynamic which has performance implications. Addressing these points will make the example more robust and a better reference for developers.

Comment on lines +100 to +108
const provider = await createEIP1193Provider({
walletId: walletId as UUID,
organizationId: organizationId as UUID,
turnkeyClient: client,
chains,
});

cachedProvider = provider as unknown as EIP1193Provider;
return cachedProvider;

Choose a reason for hiding this comment

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

critical

The createEIP1193Provider function from Turnkey returns an object containing the provider on the eip1193Provider property, not the provider itself. The current code incorrectly assigns the entire returned object to cachedProvider. This will cause runtime errors when methods like request are called on it. You should destructure eip1193Provider from the result, as shown in the official Turnkey documentation.

Suggested change
const provider = await createEIP1193Provider({
walletId: walletId as UUID,
organizationId: organizationId as UUID,
turnkeyClient: client,
chains,
});
cachedProvider = provider as unknown as EIP1193Provider;
return cachedProvider;
const { eip1193Provider } = await createEIP1193Provider({
walletId: walletId as UUID,
organizationId: organizationId as UUID,
turnkeyClient: client,
chains,
});
cachedProvider = eip1193Provider as unknown as EIP1193Provider;
return cachedProvider;

import { Providers } from "./providers";
import "./globals.css";

export const dynamic = "force-dynamic";

Choose a reason for hiding this comment

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

medium

Using export const dynamic = "force-dynamic"; opts the entire route out of caching and forces it to be dynamically rendered on the server for every request. While this might be acceptable for a demo, it has significant performance implications for a production application. Since the main page is a client component ("use client" in page.tsx), the primary interactions are client-side. You might not need to disable all server-side caching so aggressively. Please consider if this is truly necessary or if a more granular caching strategy (or removing this line to use Next.js defaults) would be more appropriate for a real-world scenario this example might inspire.


import { turnkeyConnector } from "@/config/turnkey-connector";

export default function Home() {

Choose a reason for hiding this comment

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

medium

This Home component is over 400 lines long, handling state, logic, and rendering for multiple distinct sections (authentication, wallet status, analytics actions, etc.). For better maintainability and reusability, consider breaking it down into smaller, more focused components. For example, WalletConnection, WalletStatus, and AnalyticsActions could each be their own component. While this is an example, demonstrating good component structure would be beneficial for developers using this as a reference.

Comment on lines +254 to +256
? `${parseFloat(
formatUnits(balance.value, balance.decimals)
).toFixed(4)} ${balance.symbol}`

Choose a reason for hiding this comment

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

medium

Using parseFloat on values representing cryptocurrency amounts is risky as it can lead to loss of precision due to floating-point arithmetic limitations. viem's formatUnits correctly returns a string to maintain precision. It's safer to perform string manipulation to truncate the value for display purposes, rather than converting to a floating-point number.

For example:

const formatted = formatUnits(balance.value, balance.decimals);
const dotIndex = formatted.indexOf('.');
const displayValue = dotIndex === -1 ? formatted : formatted.slice(0, dotIndex + 5); // 1 for dot, 4 for decimals
// ... then use displayValue

- Use string slicing instead of parseFloat for balance display to avoid
  floating-point precision loss with cryptocurrency amounts
- Remove force-dynamic from layout.tsx since client components don't
  need server-side dynamic rendering forced on every request

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
@yosriady
Copy link
Contributor Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds a comprehensive example for integrating Formo with Turnkey embedded wallets. The code is well-structured and the example is feature-rich. I've found a critical issue regarding a server-side module being imported in client code, which will cause a runtime error. I've also included a couple of medium-severity suggestions to improve documentation accuracy and code readability. Overall, great work on this example application.

import { getAddress, type Address, type EIP1193Provider } from "viem";
import type { TurnkeyBrowserClient } from "@turnkey/sdk-browser";
import type { AddEthereumChainParameter } from "viem";
import type { UUID } from "crypto";

Choose a reason for hiding this comment

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

critical

The crypto module is a Node.js built-in and is not available in the browser. Importing UUID from it will cause a runtime error in this client-side code (due to 'use client'). To resolve this, you can replace the import with a local type alias for UUID, as the underlying type expected by the Turnkey SDK is a string.

Suggested change
import type { UUID } from "crypto";
type UUID = string;

Comment on lines +254 to +258
? `${(() => {
const formatted = formatUnits(balance.value, balance.decimals);
const dot = formatted.indexOf(".");
return dot === -1 ? formatted : formatted.slice(0, dot + 5);
})()} ${balance.symbol}`

Choose a reason for hiding this comment

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

medium

This immediately-invoked function expression (IIFE) for formatting the balance adds complexity within the JSX, making it harder to read and maintain. It's better to extract this logic into a helper function for improved clarity and separation of concerns.

For example, you could define a helper function:

const formatDisplayBalance = (balance: { value: bigint; decimals: number; }) => {
  const formatted = formatUnits(balance.value, balance.decimals);
  const dotIndex = formatted.indexOf('.');
  // Truncate to 4 decimal places without rounding
  if (dotIndex === -1) {
    return formatted;
  }
  return formatted.slice(0, dotIndex + 5);
};

And then use it in your component like this:

{balance ? `${formatDisplayBalance(balance)} ${balance.symbol}` : "Loading..."}

- `wagmiConfig.subscribe()` for wallet connect/disconnect/chain events
- `queryClient.getMutationCache().subscribe()` for signature and transaction events

**Note:** Turnkey's logout doesn't trigger wagmi's disconnect status, so the demo includes a `useEffect` that manually tracks disconnect events when the connection state changes.

Choose a reason for hiding this comment

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

medium

This note mentions a useEffect for manually tracking disconnect events when the connection state changes. However, the implementation in page.tsx only shows a handleDisconnect function that is manually triggered by a button click. This part of the documentation seems to be inconsistent with the code. Please update the README to accurately describe the implemented disconnect behavior.

…ADME

- Replace `import type { UUID } from "crypto"` with a local type alias
  to avoid importing from a Node.js built-in in client-side code
- Extract IIFE balance formatting into a named helper function
- Remove stale README note about manual disconnect useEffect bridge

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
@yosriady
Copy link
Contributor Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds a comprehensive example for integrating Formo with Turnkey embedded wallets. The implementation is well-structured, with a clear separation of concerns between UI, configuration, and the custom wagmi connector. The code is generally of high quality. I've pointed out a couple of areas for improvement in page.tsx related to handling side effects and ensuring robust state management for user sessions.

return dot === -1 ? formatted : formatted.slice(0, dot + 5);
}

export default function Home() {

Choose a reason for hiding this comment

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

medium

The pull request description mentions: "Includes a useEffect hook to manually track Turnkey logout events since Turnkey's logout doesn't trigger wagmi's disconnect status". However, this functionality does not appear to be implemented in the current code. Without it, the application's state could become inconsistent if a user logs out of Turnkey through other means (e.g., another browser tab). Consider implementing a mechanism, perhaps using useEffect to poll turnkey.getSession(), to detect external logout events and synchronize the application state.

});

setCustomEventSent(true);
setTimeout(() => setCustomEventSent(false), 2000);

Choose a reason for hiding this comment

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

medium

Using setTimeout without a cleanup function can lead to memory leaks and attempts to set state on an unmounted component if the component unmounts before the timeout completes. It's a best practice to manage this kind of side effect within a useEffect hook, which provides a cleanup mechanism.

Consider removing this line and adding a useEffect hook to manage the state reset for customEventSent:

useEffect(() => {
  if (customEventSent) {
    const timer = setTimeout(() => {
      setCustomEventSent(false);
    }, 2000);

    return () => clearTimeout(timer);
  }
}, [customEventSent]);

The bare setTimeout in handleTrackCustomEvent could attempt to set state
on an unmounted component. Moving it to a useEffect with a cleanup
function ensures the timer is cleared if the component unmounts.

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
@yosriady
Copy link
Contributor Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a well-crafted example application demonstrating the integration of Formo Analytics with Turnkey embedded wallets. The implementation is thorough, covering authentication, wallet interactions via a custom wagmi connector, and both automatic and manual analytics event tracking. The code is of high quality, with robust error handling and clear separation of concerns in the provider setup. The accompanying documentation is comprehensive. My review includes one minor suggestion to enhance the clarity of the setup instructions in the README file.


## Prerequisites

- Node.js 18+ or pnpm 9.15+

Choose a reason for hiding this comment

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

medium

The use of 'or' in the prerequisites is slightly ambiguous. Since both Node.js and a package manager are required for the project setup, using 'and' would provide more clarity. Given that pnpm is specified in package.json, it's the recommended package manager.

Suggested change
- Node.js 18+ or pnpm 9.15+
- Node.js 18+ and pnpm 9.15+

@yosriady
Copy link
Contributor Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds a well-structured and comprehensive example for integrating Formo with Turnkey embedded wallets. The code is clean, the documentation is excellent, and the UI is well-designed for demonstrating the features. I've found one important issue in the custom wagmi connector implementation that needs to be addressed to ensure correct reactive behavior.

chains,
});

cachedProvider = provider as unknown as EIP1193Provider;

Choose a reason for hiding this comment

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

high

The custom connector is missing event listeners for accountsChanged, chainChanged, and disconnect. Without these, wagmi won't be able to react to state changes that originate outside of this application, such as in another browser tab or wallet interface. You should subscribe to these events on the provider after it's created and forward them to wagmi's emitter.

      provider.on("accountsChanged", this.onAccountsChanged);
      provider.on("chainChanged", this.onChainChanged);
      provider.on("disconnect", this.onDisconnect);
      cachedProvider = provider as unknown as EIP1193Provider;

Wire up accountsChanged, chainChanged, and disconnect event listeners
on the EIP-1193 provider so wagmi can react to state changes originating
outside the application (e.g. another browser tab).

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
devin-ai-integration[bot]

This comment was marked as resolved.

Without cleanup, if the Turnkey provider is kept alive by internal SDK
references, old listeners could fire and interfere with subsequent
connections via the shared wagmi config emitter.

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
@yosriady
Copy link
Contributor Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds a comprehensive example application for integrating Formo with Turnkey embedded wallets. The code is well-structured and provides a clear demonstration of the integration. I've added a few suggestions to improve the documentation clarity and developer experience for those running the example.

Comment on lines +35 to +37
pnpm install
# or
npm install

Choose a reason for hiding this comment

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

medium

The package.json file specifies pnpm as the package manager. Suggesting npm as an alternative can be confusing as it will produce warnings. It's better to stick to pnpm for consistency. The same applies to the npm run dev suggestion below.

Suggested change
pnpm install
# or
npm install
pnpm install

Comment on lines +58 to +60
pnpm dev
# or
npm run dev

Choose a reason for hiding this comment

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

medium

The package.json file specifies pnpm as the package manager. To avoid confusion and warnings from using npm, it's best to only document the pnpm command.

Suggested change
pnpm dev
# or
npm run dev
pnpm dev

};

export function Providers({ children }: { children: ReactNode }) {
const formoWriteKey = process.env.NEXT_PUBLIC_FORMO_WRITE_KEY;

Choose a reason for hiding this comment

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

medium

While the app correctly handles a missing formoWriteKey by not rendering FormoAnalyticsProvider, it does so silently. For a better developer experience in this example app, consider adding a console.warn if formoWriteKey is not set to inform the developer that analytics are disabled. This would be consistent with the explicit error handling for the missing Turnkey Organization ID.

For example:

if (!formoWriteKey) {
  console.warn("Missing NEXT_PUBLIC_FORMO_WRITE_KEY. Formo analytics will be disabled.");
}

- Remove npm install/dev alternatives from README since package.json
  specifies pnpm as the package manager
- Add console.warn when NEXT_PUBLIC_FORMO_WRITE_KEY is missing so
  developers know analytics are disabled

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
@yosriady
Copy link
Contributor Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive example application for integrating Formo Analytics with Turnkey embedded wallets. The implementation is well-structured, covering passkey authentication, a custom wagmi connector, and both automatic and manual event tracking. My review focuses on improving code maintainability and consistency by refining how configuration is handled and ensuring documentation accuracy. The changes I've suggested will make the example more robust and easier for other developers to follow.


## Prerequisites

- Node.js 18+ and pnpm 9.15+

Choose a reason for hiding this comment

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

medium

The package.json specifies a fixed version of pnpm (pnpm@9.15.4). For consistency and to ensure a reproducible setup, it would be better to specify the exact version in the prerequisites instead of a range.

Suggested change
- Node.js 18+ and pnpm 9.15+
- Node.js 18+ and pnpm 9.15.4

Comment on lines +81 to +82
const organizationId =
process.env.NEXT_PUBLIC_TURNKEY_ORGANIZATION_ID ?? "";

Choose a reason for hiding this comment

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

medium

Instead of accessing process.env.NEXT_PUBLIC_TURNKEY_ORGANIZATION_ID directly within the component, it's better to use the organizationId from the turnkey object provided by the useTurnkey hook. This improves component encapsulation and relies on the configuration provided by the TurnkeyProvider, making the code more maintainable. Since you've already confirmed the turnkey object exists when you check for session, you can safely access turnkey.organizationId.

Suggested change
const organizationId =
process.env.NEXT_PUBLIC_TURNKEY_ORGANIZATION_ID ?? "";
const organizationId = turnkey.organizationId;

Comment on lines +12 to +22
const turnkeyConfig = {
apiBaseUrl: "https://api.turnkey.com",
defaultOrganizationId: process.env.NEXT_PUBLIC_TURNKEY_ORGANIZATION_ID ?? "",
rpId: process.env.NEXT_PUBLIC_TURNKEY_RP_ID ?? "localhost",
iframeUrl: "https://auth.turnkey.com",
serverSignUrl: process.env.NEXT_PUBLIC_TURNKEY_SERVER_SIGN_URL,
};

export function Providers({ children }: { children: ReactNode }) {
const formoWriteKey = process.env.NEXT_PUBLIC_FORMO_WRITE_KEY;
const [queryClient] = useState(() => new QueryClient());

Choose a reason for hiding this comment

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

medium

The turnkeyConfig object is defined at the module level and depends on process.env. While this works in Next.js with client components, it's a better practice to define such configuration objects within the component body. This ensures that the configuration is created in the client-side rendering context and avoids potential issues with server-side rendering or bundlers in different environments. I'd suggest moving it inside the Providers component.

Suggested change
const turnkeyConfig = {
apiBaseUrl: "https://api.turnkey.com",
defaultOrganizationId: process.env.NEXT_PUBLIC_TURNKEY_ORGANIZATION_ID ?? "",
rpId: process.env.NEXT_PUBLIC_TURNKEY_RP_ID ?? "localhost",
iframeUrl: "https://auth.turnkey.com",
serverSignUrl: process.env.NEXT_PUBLIC_TURNKEY_SERVER_SIGN_URL,
};
export function Providers({ children }: { children: ReactNode }) {
const formoWriteKey = process.env.NEXT_PUBLIC_FORMO_WRITE_KEY;
const [queryClient] = useState(() => new QueryClient());
export function Providers({ children }: { children: ReactNode }) {
const turnkeyConfig = {
apiBaseUrl: "https://api.turnkey.com",
defaultOrganizationId: process.env.NEXT_PUBLIC_TURNKEY_ORGANIZATION_ID ?? "",
rpId: process.env.NEXT_PUBLIC_TURNKEY_RP_ID ?? "localhost",
iframeUrl: "https://auth.turnkey.com",
serverSignUrl: process.env.NEXT_PUBLIC_TURNKEY_SERVER_SIGN_URL,
};
const formoWriteKey = process.env.NEXT_PUBLIC_FORMO_WRITE_KEY;
const [queryClient] = useState(() => new QueryClient());

claude added 2 commits March 22, 2026 15:02
- Fix pnpm version in README to exact 9.15.4 instead of range
- Use turnkey.config.defaultOrganizationId instead of direct env var access
- Move turnkeyConfig inside Providers component for proper client-side context

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
Previously the login handler only checked for an existing Turnkey session
and showed an error if none was found. Now it uses passkeyClient.login()
to initiate a passkey-based session, then proceeds with wallet connection.

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 12 additional findings in Devin Review.

Open in Devin Review

Comment on lines +6 to +16
export const wagmiConfig = createConfig({
chains: [mainnet, sepolia, polygon, arbitrum, optimism, base],
transports: {
[mainnet.id]: http(),
[sepolia.id]: http(),
[polygon.id]: http(),
[arbitrum.id]: http(),
[optimism.id]: http(),
[base.id]: http(),
},
});

Choose a reason for hiding this comment

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

🟡 Missing ssr: true in wagmi config causes hydration mismatches in Next.js App Router

The wagmi config is created without ssr: true, which is required for Next.js (SSR) apps to prevent hydration mismatches. Wagmi persists connection state to localStorage by default. On the server, wagmi renders with the default disconnected state, but on the client, it reads persisted state from localStorage, causing React hydration errors. Other Next.js examples in this repo (with-porto/src/wagmi.ts:15, with-metamask/wagmi.config.ts:9) correctly set ssr: true along with cookieStorage to handle this.

Suggested change
export const wagmiConfig = createConfig({
chains: [mainnet, sepolia, polygon, arbitrum, optimism, base],
transports: {
[mainnet.id]: http(),
[sepolia.id]: http(),
[polygon.id]: http(),
[arbitrum.id]: http(),
[optimism.id]: http(),
[base.id]: http(),
},
});
export const wagmiConfig = createConfig({
chains: [mainnet, sepolia, polygon, arbitrum, optimism, base],
ssr: true,
transports: {
[mainnet.id]: http(),
[sepolia.id]: http(),
[polygon.id]: http(),
[arbitrum.id]: http(),
[optimism.id]: http(),
[base.id]: http(),
},
});
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

claude added 2 commits March 23, 2026 02:37
New users can now create a sub-organization with a passkey and embedded
wallet directly from the demo app. The flow prompts WebAuthn to create a
passkey, creates a Turnkey sub-org with an Ethereum wallet, then logs in
and connects automatically. Existing users use 'Log In with Passkey'.

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
- Add email input field for account creation (Turnkey requires a valid
  email for root users)
- Store sub-org ID in localStorage after account creation so subsequent
  logins can scope the passkey auth to the correct sub-organization
- Use session.organizationId instead of the parent org config for wallet
  fetching, supporting both parent-org and sub-org users

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
devin-ai-integration[bot]

This comment was marked as resolved.

The handleCreateAccount callback captured the initial empty signupEmail
value because it was missing from the dependency array.

https://claude.ai/code/session_012cKNjKPGBnVZQwNDAL3iKS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants