Skip to content
38 changes: 19 additions & 19 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,11 +329,11 @@ pnpm test

# Generating example apps using cursor

1) Open the product folder under /examples/{product}
1) Open the product folder under /examples/{product}/_prompts

2) Under the {product} directory, open the prompt-part2.txt file.
2) Under the {product}/_prompts directory, open the example-app-2-add-feature.txt file.

3) Copy all of the content in the prompt-part2.txt file.
3) Copy all of the content in the example-app-2-add-feature.txt file.

4) Paste it on to Cursor. Make sure that you're using Agent mode and the model to be used is Claude 3.7 (Thinking)

Expand All @@ -347,15 +347,15 @@ the <feature name> has the manually-edited field value as true in the feature.js

7) If the flag above exists, it will ask for a confirmation, otherwise, it will use best practices to update the existing feature page.

8) Once the feature page is completed, under the same directory as prompt-part2.txt, open prompt-part3.txt and copy all of its contents.
8) Once the feature page is completed, under the same directory as example-app-2-add-feature.txt, open example-app-3-fix-ui.txt and copy all of its contents.

9) Paste it on to a new Cursor window and in the chat window you can type:

feature name: <name of the feature>

Here, cursor will check and fix pages with styling issues to ensure that it looks consistent with other example apps.

10) Once the styling changes have been made, under the same directory as prompt-part2.txt, open prompt-part4.txt and copy all of its contents.
10) Once the styling changes have been made, under the same directory as example-app-2-add-feature.txt, open example-app-4-testing.txt and copy all of its contents.

11) Paste it on to a new Cursor window and in the chat window you can type:
app name: <app name(if exists)>
Expand All @@ -369,7 +369,7 @@ feature name: <name of the feature>

## If an App Doesn't Exist Yet

You must create the app first by going to the prompt-part1.txt file and paste it on to Cursor's chat window and use Agent Mode + Claude 3.7 Sonnet Thinking.
You must create the app first by going to the example-app-1-create-app-template.txt file and paste it on to Cursor's chat window and use Agent Mode + Claude 3.7 Sonnet Thinking.

In the chat window, type in:
feature name: <name>
Expand All @@ -383,35 +383,35 @@ IMPORTANT: For any prompts, if cursor is not done with its operations but it has

The example generation process uses three different prompt files, each with a specific purpose:

### prompt-part1.txt
### example-app-1-create-app-template.txt
- **Purpose**: Creates the initial app structure and boilerplate
- **When to use**: Only when you need to create a completely new example app
- **What it does**: Sets up the project structure, configuration files, basic components, and placeholder pages
- **Output**: A functioning but minimal app with no implemented features

### prompt-part2.txt
### example-app-2-add-feature.txt
- **Purpose**: Implements or updates a specific feature within an existing app
- **When to use**: After creating an app with prompt-part1.txt, or when adding/updating features
- **When to use**: After creating an app with example-app-1-create-app-template.txt, or when adding/updating features
- **What it does**: Creates or modifies the feature implementation with proper error handling, UI states, and best practices
- **Output**: A fully implemented feature page within the app structure

### prompt-part3.txt
### example-app-3-fix-ui.txt
- **Purpose**: Apply styling changes to the given example app if possible to ensure styling consistency with other example apps
- **When to use**: After running prompt-part2.txt to implement a feature
- **When to use**: After running example-app-2-add-feature.txt to implement a feature
- **What it does**: Modifies the example app's UI/UX to ensure that the app looks consistent with other example apps
- **Output**: A fully implemented feature page within the app structure with consistent UI/UX looks.

### prompt-part4.txt
### example-app-4-testing.txt
- **Purpose**: Tests, validates, and fixes issues in the implementation
- **When to use**: After running prompt-part2.txt to implement a feature
- **When to use**: After running example-app-2-add-feature.txt to implement a feature
- **What it does**: Runs tests, checks coverage, builds the app, and fixes any detected issues
- **Output**: A tested, validated feature ready for use

**Typical Workflow:**
1. Use prompt-part1.txt to create a new app (only once per app)
1. Use prompt-part2.txt to implement each feature in the app
1. Use prompt-part3.txt to fix the app's UI/UX styling and make it look consistent to other example apps.
1. Use prompt-part4.txt after each feature implementation to test and validate
1. Use example-app-1-create-app-template.txt to create a new app (only once per app)
1. Use example-app-2-add-feature.txt to implement each feature in the app
1. Use example-app-3-fix-ui.txt to fix the app's UI/UX styling and make it look consistent to other example apps.
1. Use example-app-4-testing.txt after each feature implementation to test and validate
1. Once you've generated the example app or feature, you should manually review the implementation and the UI. It's likely you will need to make some manual adjustments to get the app to function and look like our other example apps because the cursor can not reliably get it 100% correct.
1. Once you're happy with your example app or feature, you need to [re-run the tutorial generation prompt](#generating-tutorials-and-metadata-with-cursor) for your example app before creating your PR so the new example app or feature is piped into the docs site with it's corresponding tutorial.

Expand Down Expand Up @@ -507,7 +507,7 @@ If this happens you will need to log into the Netlify site, check the error and

# Generating tutorials and metadata with cursor

Whenever you add a new example app, or update an existing example app, you can use the prompts in the `prompt.txt` files in each `examples/product` folder to generate the tutorials and metadata for the example apps using Cursor AI.
Whenever you add a new example app, or update an existing example app, you can use the prompts in the `tutorial-generation-prompt.txt` files in each `examples/product/_prompts` folder to generate the tutorials and metadata for the example apps using Cursor AI.

These AI generated tutorials and metadata files are then piped through to the docs site in the CI/CD pipeline, where they are used to display the example apps and their code walkthroughs. If you don't follow these steps, your example app will not be displayed on the docs site.

Expand All @@ -520,7 +520,7 @@ Follow these steps to generate the tutorials and metadata for the example apps:
1. Delete the existing tutorial.md and metadata.json files in the example app you are wanting to generate the tutorials and metadata for.
2. Open the Composer window in Cursor IDE (Claude 3.7-sonnet-thinking).
3. Press the `+` button clear the context of the composer window.
4. Open the `prompt.txt` file in the examples/product folder you are wanting to generate the tutorials and metadata for e.g. `examples/passport/prompt.txt`.
4. Open the `tutorial-generation-prompt.txt` file in the examples/product/_prompts folder you are wanting to generate the tutorials and metadata for e.g. `examples/passport/_prompts/tutorial-generation-prompt.txt`.
5. Copy and pate the prompt into the composer window, or attach it as a file.
6. After adding the prompt, in the composer window, type `app name: <name of the example app>` e.g. `app name: login-with-nextjs` where the app name is the folder name of the example app in the examples/product folder you are wanting to generate the tutorials and metadata for.
7. Press enter and let the AI generate the tutorials and metadata.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
'use client';

import { useEffect, useState, useCallback } from 'react';
import { Button, Heading, Stack, Body, Table, Link } from '@biom3/react';
import NextLink from 'next/link';
import { passportInstance } from '../utils/setupLogoutSilent';
import { Provider, ProviderEvent } from '@imtbl/sdk/passport';


export default function EventHandlingPage() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [provider, setProvider] = useState<Provider | undefined>(undefined);
const [events, setEvents] = useState<Array<{event: string, data: string, timestamp: string}>>([]);
const [address, setAddress] = useState<string>('');
const [chainId, setChainId] = useState<string>('');
const [loading, setLoading] = useState(false);
const [accountsState, setAccountsState] = useState<string[]>([]);

// Add a new event to the event log
const logEvent = useCallback((eventName: string, data: any) => {
setEvents(prev => [
{
event: eventName,
data: JSON.stringify(data, null, 2),
timestamp: new Date().toLocaleTimeString()
},
...prev
].slice(0, 10)); // Keep only the last 10 events
}, []);

// Handler for accountsChanged event
const handleAccountsChanged = useCallback((accounts: string[]) => {
console.log('accounts changed:', accounts);
setAccountsState(accounts);
logEvent(ProviderEvent.ACCOUNTS_CHANGED, { accounts });

if (accounts.length === 0) {
// User has disconnected their account
setIsLoggedIn(false);
setAddress('');
setChainId(''); // Clear chainId on disconnect
} else {
setAddress(accounts[0]);
// Potentially fetch chainId again if needed, or assume it hasn't changed
}
}, [logEvent]);

// Initialize provider on mount
useEffect(() => {
const fetchPassportProvider = async () => {
// Check if user is already logged in
const user = await passportInstance.getUserInfo();
if (user) {
const provider = await passportInstance.connectEvm();
setProvider(provider);
if (provider) {
const accounts = await provider.request({ method: 'eth_accounts' });
if (accounts && accounts.length > 0) {
setAddress(accounts[0]);
setAccountsState(accounts);
logEvent('initial_accounts', { accounts });
const chainId = await provider.request({ method: 'eth_chainId' });
setChainId(chainId);
logEvent('initial_chain_id', { chainId });
}
}
setIsLoggedIn(true);
} else {
// Optionally connectEvm even if not logged in to set up listeners early
// const provider = await passportInstance.connectEvm();
// setProvider(provider);
}
};

fetchPassportProvider();
}, [logEvent]); // Added logEvent dependency

// Set up accountsChanged event listener
useEffect(() => {
if (!provider) return;

// Register event listener
provider.on(ProviderEvent.ACCOUNTS_CHANGED, handleAccountsChanged);

// Log that event listener was registered
logEvent('provider_event_registered', { event: ProviderEvent.ACCOUNTS_CHANGED });

// Cleanup function to remove event listener
return () => {
provider.removeListener(ProviderEvent.ACCOUNTS_CHANGED, handleAccountsChanged);
};
}, [provider, handleAccountsChanged, logEvent]);

// Handle login
const handleLogin = async () => {
try {
setLoading(true);
await passportInstance.login();

// After login, get the provider
const provider = await passportInstance.connectEvm();
setProvider(provider);

if (provider) {
// Get accounts
const accounts = await provider.request({ method: 'eth_requestAccounts' });
if (accounts && accounts.length > 0) {
setAddress(accounts[0]);
setAccountsState(accounts);
// Log the accounts change manually since the event might not fire
logEvent(ProviderEvent.ACCOUNTS_CHANGED, { accounts });
}

// Get chain ID
const chainId = await provider.request({ method: 'eth_chainId' });
setChainId(chainId);
}

setIsLoggedIn(true);
} catch (error) {
console.error('Login error:', error);
logEvent('login_error', { message: error instanceof Error ? error.message : String(error) });
} finally {
setLoading(false);
}
};

// Handle logout
const handleLogout = async () => {
try {
setLoading(true);
await passportInstance.logout();
setIsLoggedIn(false);
setAddress('');
setChainId('');
setAccountsState([]);
setProvider(undefined); // Clear provider on logout
logEvent('logout_success', {}); // Log successful logout
} catch (error) {
console.error('Logout error:', error);
logEvent('logout_error', { message: error instanceof Error ? error.message : String(error) });
} finally {
setLoading(false);
}
};

return (
<>
<Heading size="medium" className="mb-1">Passport SDK - Event Handling Example</Heading>

{/* Buttons Section */}

{!isLoggedIn && (
<Button
onClick={handleLogin}
disabled={loading}
className="mb-1"
size="medium"
>
Login {loading ? '...' : ''}
</Button>
)}
{isLoggedIn && (
<Button
Comment thread
darrenmelvison1 marked this conversation as resolved.
onClick={handleLogout}
disabled={loading}
className="mb-1"
size="medium"
>
{loading ? 'Logging out...' : 'Logout'}
</Button>
)}

{/* State Data Table */}
{(isLoggedIn || accountsState.length > 0) && (
<>
<Table>
<Table.Head>
<Table.Row>
<Table.Cell>Key</Table.Cell>
<Table.Cell>Value</Table.Cell>
</Table.Row>
</Table.Head>
<Table.Body>
<Table.Row>
<Table.Cell>Status</Table.Cell>
<Table.Cell>{isLoggedIn ? 'Logged In' : 'Logged Out'}</Table.Cell>
</Table.Row>
{address && (
<Table.Row>
<Table.Cell>Address</Table.Cell>
<Table.Cell><code>{address}</code></Table.Cell>
</Table.Row>
)}
{chainId && (
<Table.Row>
<Table.Cell>Chain ID</Table.Cell>
<Table.Cell><code>{chainId}</code></Table.Cell>
</Table.Row>
)}
{accountsState.length > 0 && (
<Table.Row>
<Table.Cell>Accounts ({accountsState.length})</Table.Cell>
<Table.Cell>
<div style={{ maxHeight: '100px', overflowY: 'auto'}}>
{accountsState.map((account, idx) => (
<div key={idx} style={{ marginBottom: "4px" }}>
<code>{account}</code> {idx === 0 && <span style={{ fontSize: "12px", color: "#666" }}>(active)</span>}
</div>
))}
</div>
</Table.Cell>
</Table.Row>
)}
</Table.Body>
</Table>
</>
)}
<br />
{/* Event Log Section */}
<>
<Heading size="small">Event Log:</Heading>
<div className="event-log" style={{
maxHeight: '300px',
overflowY: 'auto',
border: '1px solid #ddd',
borderRadius: '4px',
padding: '12px',
width: '100%'
}}>
{events.length === 0 ? (
<Body>No events logged yet</Body>
) : (
events.map((event, index) => (
<div key={index} style={{
marginBottom: '12px',
padding: '8px',
backgroundColor: '#f5f5f5',
borderRadius: '4px'
}}>
<Stack direction="row" gap="space.xsmall" alignItems="center">
{/* Use a more distinct tag style */}
<span style={{
backgroundColor: '#e0e0e0',
padding: '2px 6px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold'
}}>
{event.event}
</span>
<Body size="small" color="secondary">{event.timestamp}</Body>
</Stack>
<pre style={{
marginTop: '8px',
overflow: 'auto',
fontSize: '12px',
backgroundColor: '#fff', // White background for pre
padding: '8px',
borderRadius: '4px',
border: '1px solid #eee'
}}>
{event.data}
</pre>
</div>
))
)}
</div>
</>
<br />
<Link rc={<NextLink href="/" />}>Return to Examples</Link>
</>
);
Comment thread
darrenmelvison1 marked this conversation as resolved.
}
6 changes: 6 additions & 0 deletions examples/passport/login-with-nextjs/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,11 @@ export default function Home() {
rc={<NextLink href="/logout-with-silent-mode" />}>
Logout with Silent Mode
</Button>
<Button
className="mb-1"
size="medium"
rc={<NextLink href="/auth-event-handling" />}>
Auth Event Handling
</Button>
</>);
}
Loading