From 7c8abe59874a84ef962250022c3bddab5728cbff Mon Sep 17 00:00:00 2001 From: darrenmelvison1 Date: Tue, 8 Apr 2025 10:20:30 +1000 Subject: [PATCH 01/10] event handling app WIP --- .../event-handling-with-nextjs/.env.example | 2 + .../event-handling-with-nextjs/.eslintrc.json | 4 + .../event-handling-with-nextjs/.gitignore | 36 ++ .../event-handling-with-nextjs/README.md | 36 ++ .../event-handling-with-nextjs/features.json | 11 + .../event-handling-with-nextjs/metadata.json | 8 + .../next.config.mjs | 4 + .../event-handling-with-nextjs/package.json | 29 ++ .../playwright.config.ts | 30 ++ .../src/app/event-handling/page.tsx | 355 ++++++++++++++++++ .../src/app/globals.css | 39 ++ .../src/app/layout.tsx | 26 ++ .../src/app/logout/page.tsx | 25 ++ .../src/app/page.tsx | 18 + .../src/app/redirect/page.tsx | 33 ++ .../src/app/utils/setupDefault.ts | 17 + .../src/app/utils/wrapper.tsx | 18 + .../tests/event-handling.spec.ts | 26 ++ .../event-handling-with-nextjs/tsconfig.json | 26 ++ pnpm-lock.yaml | 43 +++ 20 files changed, 786 insertions(+) create mode 100644 examples/passport/event-handling-with-nextjs/.env.example create mode 100644 examples/passport/event-handling-with-nextjs/.eslintrc.json create mode 100644 examples/passport/event-handling-with-nextjs/.gitignore create mode 100644 examples/passport/event-handling-with-nextjs/README.md create mode 100644 examples/passport/event-handling-with-nextjs/features.json create mode 100644 examples/passport/event-handling-with-nextjs/metadata.json create mode 100644 examples/passport/event-handling-with-nextjs/next.config.mjs create mode 100644 examples/passport/event-handling-with-nextjs/package.json create mode 100644 examples/passport/event-handling-with-nextjs/playwright.config.ts create mode 100644 examples/passport/event-handling-with-nextjs/src/app/event-handling/page.tsx create mode 100644 examples/passport/event-handling-with-nextjs/src/app/globals.css create mode 100644 examples/passport/event-handling-with-nextjs/src/app/layout.tsx create mode 100644 examples/passport/event-handling-with-nextjs/src/app/logout/page.tsx create mode 100644 examples/passport/event-handling-with-nextjs/src/app/page.tsx create mode 100644 examples/passport/event-handling-with-nextjs/src/app/redirect/page.tsx create mode 100644 examples/passport/event-handling-with-nextjs/src/app/utils/setupDefault.ts create mode 100644 examples/passport/event-handling-with-nextjs/src/app/utils/wrapper.tsx create mode 100644 examples/passport/event-handling-with-nextjs/tests/event-handling.spec.ts create mode 100644 examples/passport/event-handling-with-nextjs/tsconfig.json diff --git a/examples/passport/event-handling-with-nextjs/.env.example b/examples/passport/event-handling-with-nextjs/.env.example new file mode 100644 index 0000000000..7efb22feb5 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/.env.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_PUBLISHABLE_KEY= +NEXT_PUBLIC_CLIENT_ID= \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/.eslintrc.json b/examples/passport/event-handling-with-nextjs/.eslintrc.json new file mode 100644 index 0000000000..acd2aad42f --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": "next/core-web-vitals" +} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/.gitignore b/examples/passport/event-handling-with-nextjs/.gitignore new file mode 100644 index 0000000000..54fe4d6114 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/README.md b/examples/passport/event-handling-with-nextjs/README.md new file mode 100644 index 0000000000..e515d86c57 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/README.md @@ -0,0 +1,36 @@ +# Immutable Passport Event Handling with Next.js + +This example demonstrates how to handle events from the Immutable Passport SDK in a Next.js application. + +## Getting Started + +1. Clone the repository +2. Install dependencies +```bash +npm install +``` +3. Create a `.env` file with your Immutable Hub credentials: +```bash +NEXT_PUBLIC_PUBLISHABLE_KEY= +NEXT_PUBLIC_CLIENT_ID= +``` +4. Run the development server: +```bash +npm run dev +``` + +## Features + +- Event handling for Passport SDK events +- Login and logout functionality +- Real-time event logging +- Handles authentication redirects + +## Events Demonstrated + +- LOGIN_SUCCESS +- LOGIN_FAILURE +- LOGOUT_SUCCESS +- LOGOUT_FAILURE +- NETWORK_CHANGED +- ACCOUNT_CHANGED \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/features.json b/examples/passport/event-handling-with-nextjs/features.json new file mode 100644 index 0000000000..81dc3ba3c5 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/features.json @@ -0,0 +1,11 @@ +{ + "features": [ + { + "feature": "event-handling", + "manually-edited": false, + "silent-login": true, + "silent-logout": true + } + ], + "order": 7 +} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/metadata.json b/examples/passport/event-handling-with-nextjs/metadata.json new file mode 100644 index 0000000000..a8e106fff8 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/metadata.json @@ -0,0 +1,8 @@ +{ + "title": "Event Handling with NextJS", + "description": "A comprehensive example demonstrating how to handle Passport SDK events in a Next.js application.", + "keywords": ["Immutable", "SDK", "Passport", "Events", "Event Handling", "Authentication", "Login", "Logout", "Network", "Account"], + "tech_stack": ["NextJS", "TypeScript", "React"], + "product": "Passport", + "programming_language": "TypeScript" +} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/next.config.mjs b/examples/passport/event-handling-with-nextjs/next.config.mjs new file mode 100644 index 0000000000..4201d2eb6e --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/package.json b/examples/passport/event-handling-with-nextjs/package.json new file mode 100644 index 0000000000..9e255b6a3c --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/package.json @@ -0,0 +1,29 @@ +{ + "name": "@examples/event-handling-with-nextjs", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "test": "playwright test" + }, + "dependencies": { + "@biom3/react": "^0.25.21", + "@imtbl/sdk": "workspace:*", + "ethers": "^6.13.4", + "next": "14.2.25", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@playwright/test": "^1.45.3", + "@types/node": "^20", + "@types/react": "^18.3.4", + "@types/react-dom": "^18.3.0", + "eslint": "^8", + "eslint-config-next": "14.2.7", + "typescript": "^5.6.2" + } +} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/playwright.config.ts b/examples/passport/event-handling-with-nextjs/playwright.config.ts new file mode 100644 index 0000000000..c8ae12b40e --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/playwright.config.ts @@ -0,0 +1,30 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: "80%", + reporter: "html", + + use: { + baseURL: "http://localhost:3000", + trace: "on-first-retry", + }, + + projects: [ + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { name: "webkit", use: { ...devices["Desktop Safari"] } }, + + { name: "Mobile Chrome", use: { ...devices["Pixel 5"] } }, + { name: "Mobile Safari", use: { ...devices["iPhone 12"] } }, + ], + + webServer: { + command: "npm start", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + }, +}); \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/event-handling/page.tsx b/examples/passport/event-handling-with-nextjs/src/app/event-handling/page.tsx new file mode 100644 index 0000000000..40b8f4c4f8 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/src/app/event-handling/page.tsx @@ -0,0 +1,355 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Button, Heading, Stack, Body, Badge } from '@biom3/react'; +import { passportInstance } from '../utils/setupDefault'; +import { passport } from '@imtbl/sdk'; + +// Define the event constants as specified in Immutable documentation +const ProviderEvent = { + ACCOUNTS_CHANGED: 'accountsChanged', + CHAIN_CHANGED: 'chainChanged', + CONNECT: 'connect', + DISCONNECT: 'disconnect', + MESSAGE: 'message' +}; + +export default function EventHandlingPage() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [provider, setProvider] = useState(null); + const [events, setEvents] = useState>([]); + const [address, setAddress] = useState(''); + const [chainId, setChainId] = useState(''); + const [loading, setLoading] = useState(false); + const [accountsState, setAccountsState] = useState([]); + // Add a new event to the event log + const logEvent = (eventName: string, data: any) => { + // Get a readable event name for display + let displayEventName = eventName; + + // Map enum constants to readable names if needed + if (eventName === ProviderEvent.ACCOUNTS_CHANGED) displayEventName = 'accountsChanged'; + if (eventName === ProviderEvent.CHAIN_CHANGED) displayEventName = 'chainChanged'; + if (eventName === ProviderEvent.CONNECT) displayEventName = 'connect'; + if (eventName === ProviderEvent.DISCONNECT) displayEventName = 'disconnect'; + if (eventName === ProviderEvent.MESSAGE) displayEventName = 'message'; + + setEvents(prev => [ + { + event: displayEventName, + data: JSON.stringify(data, null, 2), + timestamp: new Date().toLocaleTimeString() + }, + ...prev + ].slice(0, 10)); // Keep only the last 10 events + }; + + // Check login status on mount + useEffect(() => { + const checkLoginStatus = async () => { + try { + const userProfile = await passportInstance.getUserInfo(); + if (userProfile) { + setIsLoggedIn(true); + + // Get provider and user info + const provider = await passportInstance.connectEvm(); + setProvider(provider); + + if (provider) { + // Get accounts + const accounts = await provider.request({ method: 'eth_accounts' }); + if (accounts && accounts.length > 0) { + setAddress(accounts[0]); + // Keep accountsState in sync + setAccountsState(accounts); + } + + // Get chain ID + const chainId = await provider.request({ method: 'eth_chainId' }); + setChainId(chainId); + } + } + } catch (error) { + console.error('Error checking login status:', error); + } + }; + + checkLoginStatus(); + }, []); + + // Setup event listeners when provider is available + useEffect(() => { + if (!provider) return; + + const handleConnect = (connectInfo: any) => { + logEvent('connect', connectInfo); + setIsLoggedIn(true); + }; + + const handleDisconnect = (error: any) => { + logEvent('disconnect', error); + setIsLoggedIn(false); + setAddress(''); + setChainId(''); + }; + + const handleChainChanged = (chainId: string) => { + logEvent('chainChanged', { chainId }); + setChainId(chainId); + + // Optional: Reload the page when chain changes to ensure all state is fresh + // window.location.reload(); + }; + + const handleAccountsChanged = (accounts: string[]) => { + console.log('accounts', accounts); + setAccountsState(accounts); + logEvent('accountsChanged', { accounts }); + if (accounts.length === 0) { + // User has disconnected their account + setIsLoggedIn(false); + setAddress(''); + } else { + setAddress(accounts[0]); + } + }; + + const handleMessage = (message: any) => { + logEvent('message', message); + }; + + // Register event listeners using ProviderEvent enum from SDK + provider.on(ProviderEvent.CONNECT, handleConnect); + provider.on(ProviderEvent.DISCONNECT, handleDisconnect); + provider.on(ProviderEvent.CHAIN_CHANGED, handleChainChanged); + provider.on(ProviderEvent.ACCOUNTS_CHANGED, handleAccountsChanged); + provider.on(ProviderEvent.MESSAGE, handleMessage); + + // Log that event listeners were registered + logEvent('listeners_registered', { + events: [ + ProviderEvent.CONNECT, + ProviderEvent.DISCONNECT, + ProviderEvent.CHAIN_CHANGED, + ProviderEvent.ACCOUNTS_CHANGED, + ProviderEvent.MESSAGE + ] + }); + + // Cleanup function to remove event listeners + return () => { + if (provider) { + provider.removeListener(ProviderEvent.CONNECT, handleConnect); + provider.removeListener(ProviderEvent.DISCONNECT, handleDisconnect); + provider.removeListener(ProviderEvent.CHAIN_CHANGED, handleChainChanged); + provider.removeListener(ProviderEvent.ACCOUNTS_CHANGED, handleAccountsChanged); + provider.removeListener(ProviderEvent.MESSAGE, handleMessage); + } + }; + }, [provider]); // Only re-run when provider changes + + // 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]); + // Manually update accountsState since the event might not fire during initial login + setAccountsState(accounts); + + // Manually log the accounts information as if the event fired + logEvent(ProviderEvent.ACCOUNTS_CHANGED, { accounts }); + } + + // Get chain ID + const chainId = await provider.request({ method: 'eth_chainId' }); + setChainId(chainId); + } + + setIsLoggedIn(true); + logEvent('login', { success: true }); + } catch (error) { + console.error('Login error:', error); + logEvent('login_error', error); + } finally { + setLoading(false); + } + }; + + // Handle logout + const handleLogout = async () => { + try { + setLoading(true); + await passportInstance.logout(); + setIsLoggedIn(false); + setAddress(''); + setChainId(''); + // Clear accounts state on logout + setAccountsState([]); + logEvent('logout', { success: true }); + } catch (error) { + console.error('Logout error:', error); + logEvent('logout_error', error); + } finally { + setLoading(false); + } + }; + + // Trigger a custom event (switch chain request) + const requestChainSwitch = async () => { + if (!provider) return; + + try { + setLoading(true); + // Request to switch to Ethereum mainnet (chain ID 0x1) + await provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x1' }], + }); + logEvent('request_chain_switch', { chainId: '0x1' }); + } catch (error) { + console.error('Chain switch error:', error); + logEvent('chain_switch_error', error); + } finally { + setLoading(false); + } + }; + + // Manually request accounts + const requestAccounts = async () => { + if (!provider) return; + + try { + setLoading(true); + const accounts = await provider.request({ + method: 'eth_requestAccounts' + }); + logEvent('request_accounts', { accounts }); + if (accounts && accounts.length > 0) { + setAddress(accounts[0]); + } + } catch (error) { + console.error('Request accounts error:', error); + logEvent('request_accounts_error', error); + } finally { + setLoading(false); + } + }; + + return ( + + Passport SDK - Event Handling Example + + + Status: {isLoggedIn ? 'Logged In' : 'Logged Out'} + + {isLoggedIn && address && ( + + Connected Account: + {address} + + )} + + {isLoggedIn && chainId && ( + + Chain ID: + {chainId} + + )} + {accountsState && accountsState.length > 0 && ( + + Accounts: +
+ {accountsState.map((account, idx) => ( +
+ {account} {idx === 0 && (active)} +
+ ))} +
+
+ )} +
+ + + + + + + {isLoggedIn && ( + + + + + )} + + + Event Log: +
+ {events.length === 0 ? ( + No events logged yet + ) : ( + events.map((event, index) => ( +
+ +
{event.event}
+ {event.timestamp} +
+
+                  {event.data}
+                
+
+ )) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/globals.css b/examples/passport/event-handling-with-nextjs/src/app/globals.css new file mode 100644 index 0000000000..5d7b99a321 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/src/app/globals.css @@ -0,0 +1,39 @@ +html, body { + height: 100%; +} +body { + margin: 0; +} +.flex-container { + height: 100%; + padding: 0; + margin: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.mb-1 { + margin-bottom: 1rem; +} + +.event-log { + height: 200px; + width: 100%; + overflow-y: auto; + border: 1px solid #ccc; + padding: 10px; + margin-top: 10px; + background-color: #f5f5f5; +} + +.event-tag { + display: inline-block; + background-color: #3471F2; + color: #FFFFFF; + font-size: 12px; + font-weight: bold; + padding: 4px 8px; + border-radius: 4px; + text-transform: uppercase; +} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/layout.tsx b/examples/passport/event-handling-with-nextjs/src/app/layout.tsx new file mode 100644 index 0000000000..a630a3127f --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/src/app/layout.tsx @@ -0,0 +1,26 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import AppWrapper from "./utils/wrapper"; +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Passport SDK - Event Handling with NextJS", + description: "Examples of event handling with Passport SDK in NextJS", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + ); +} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/logout/page.tsx b/examples/passport/event-handling-with-nextjs/src/app/logout/page.tsx new file mode 100644 index 0000000000..cb8ddd0ae6 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/src/app/logout/page.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { useEffect } from 'react'; +import { Stack, Body, Heading } from '@biom3/react'; +import { useRouter } from 'next/navigation'; + +export default function LogoutPage() { + const router = useRouter(); + + useEffect(() => { + // Redirect to home page after a short delay + const timer = setTimeout(() => { + router.push('/event-handling'); + }, 2000); + + return () => clearTimeout(timer); + }, [router]); + + return ( + + You have been logged out + Redirecting back to the app... + + ); +} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/page.tsx b/examples/passport/event-handling-with-nextjs/src/app/page.tsx new file mode 100644 index 0000000000..f588cae9b8 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/src/app/page.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function HomePage() { + const router = useRouter(); + + useEffect(() => { + router.push('/event-handling'); + }, [router]); + + return ( +
+

Redirecting to Event Handling Example...

+
+ ); +} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/redirect/page.tsx b/examples/passport/event-handling-with-nextjs/src/app/redirect/page.tsx new file mode 100644 index 0000000000..52343c7dc7 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/src/app/redirect/page.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Stack, Body, Heading } from '@biom3/react'; +import { useRouter } from 'next/navigation'; +import { passportInstance } from '../utils/setupDefault'; + +export default function RedirectPage() { + const router = useRouter(); + const [error, setError] = useState(null); + + useEffect(() => { + const handleRedirect = async () => { + try { + // Handle the redirect + await passportInstance.loginCallback(); + router.push('/event-handling'); + } catch (e: any) { + console.error('Error handling redirect', e); + setError(e.message || 'An unknown error occurred during login'); + } + }; + + handleRedirect(); + }, [router]); + + return ( + + Processing login... + {error && {error}} + + ); +} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/utils/setupDefault.ts b/examples/passport/event-handling-with-nextjs/src/app/utils/setupDefault.ts new file mode 100644 index 0000000000..5a0475edb2 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/src/app/utils/setupDefault.ts @@ -0,0 +1,17 @@ +import { config, passport } from '@imtbl/sdk'; + +// #doc passport-instance +export const passportInstance = new passport.Passport({ + baseConfig: { + environment: config.Environment.SANDBOX, // or config.Environment.PRODUCTION + publishableKey: + process.env.NEXT_PUBLIC_PUBLISHABLE_KEY || '', // replace with your publishable API key from Hub + }, + clientId: process.env.NEXT_PUBLIC_CLIENT_ID || '', // replace with your client ID from Hub + redirectUri: 'http://localhost:3000/redirect', // replace with one of your redirect URIs from Hub + logoutRedirectUri: 'http://localhost:3000/logout', // replace with one of your logout URIs from Hub + audience: 'platform_api', + scope: 'openid offline_access email transact', + logoutMode: 'silent', // Using silent logout mode + }); +// #enddoc passport-instance \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/utils/wrapper.tsx b/examples/passport/event-handling-with-nextjs/src/app/utils/wrapper.tsx new file mode 100644 index 0000000000..4d19169031 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/src/app/utils/wrapper.tsx @@ -0,0 +1,18 @@ +'use client'; +import { BiomeCombinedProviders, Stack } from '@biom3/react'; + +export default function AppWrapper({ + children, + }: Readonly<{ + children: React.ReactNode; + }>) { + return ( +
+ + + { children } + + +
+ ); +} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/tests/event-handling.spec.ts b/examples/passport/event-handling-with-nextjs/tests/event-handling.spec.ts new file mode 100644 index 0000000000..066adfdf9e --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/tests/event-handling.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Event Handling', () => { + test('should navigate to event handling page', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveURL(/.*\/event-handling/); + }); + + test('should display event handling UI elements', async ({ page }) => { + await page.goto('/event-handling'); + + // Check for heading + await expect(page.getByRole('heading', { name: 'Passport SDK - Event Handling Example' })).toBeVisible(); + + // Check for status text + await expect(page.getByText('Status: Logged Out')).toBeVisible(); + + // Check for buttons + await expect(page.getByRole('button', { name: 'Login' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible(); + + // Check for event log + await expect(page.getByText('Event Log:')).toBeVisible(); + await expect(page.locator('.event-log')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/tsconfig.json b/examples/passport/event-handling-with-nextjs/tsconfig.json new file mode 100644 index 0000000000..afa72d9966 --- /dev/null +++ b/examples/passport/event-handling-with-nextjs/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 503505ac0a..ff46206027 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -623,6 +623,49 @@ importers: specifier: ^5.6.2 version: 5.6.2 + examples/passport/event-handling-with-nextjs: + dependencies: + '@biom3/react': + specifier: ^0.25.21 + version: 0.25.21(@emotion/react@11.11.3(@types/react@18.3.12)(react@18.3.1))(@rive-app/react-canvas-lite@4.9.0(react@18.3.1))(embla-carousel-react@8.1.5(react@18.3.1))(framer-motion@11.18.2(@emotion/is-prop-valid@0.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@imtbl/sdk': + specifier: workspace:* + version: link:../../../sdk + ethers: + specifier: ^6.13.4 + version: 6.13.5(bufferutil@4.0.8)(utf-8-validate@5.0.10) + next: + specifier: 14.2.25 + version: 14.2.25(@babel/core@7.26.9)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18 + version: 18.3.1 + react-dom: + specifier: ^18 + version: 18.3.1(react@18.3.1) + devDependencies: + '@playwright/test': + specifier: ^1.45.3 + version: 1.45.3 + '@types/node': + specifier: ^20 + version: 20.14.13 + '@types/react': + specifier: ^18.3.4 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.0 + eslint: + specifier: ^8 + version: 8.57.0 + eslint-config-next: + specifier: 14.2.7 + version: 14.2.7(eslint@8.57.0)(typescript@5.6.2) + typescript: + specifier: ^5.6.2 + version: 5.6.2 + examples/passport/logged-in-user-with-nextjs: dependencies: '@biom3/react': From 45105a5fd44e28d42700d053f6d70a91b31288bc Mon Sep 17 00:00:00 2001 From: darrenmelvison1 Date: Tue, 8 Apr 2025 12:23:48 +1000 Subject: [PATCH 02/10] moved event-handling into login-with-nextjs and use available events only --- .../event-handling-with-nextjs/.env.example | 2 - .../event-handling-with-nextjs/.eslintrc.json | 4 - .../event-handling-with-nextjs/.gitignore | 36 ---- .../event-handling-with-nextjs/README.md | 36 ---- .../event-handling-with-nextjs/features.json | 11 -- .../event-handling-with-nextjs/metadata.json | 8 - .../next.config.mjs | 4 - .../event-handling-with-nextjs/package.json | 29 --- .../playwright.config.ts | 30 --- .../src/app/globals.css | 39 ---- .../src/app/layout.tsx | 26 --- .../src/app/logout/page.tsx | 25 --- .../src/app/page.tsx | 18 -- .../src/app/redirect/page.tsx | 33 ---- .../src/app/utils/setupDefault.ts | 17 -- .../src/app/utils/wrapper.tsx | 18 -- .../tests/event-handling.spec.ts | 26 --- .../event-handling-with-nextjs/tsconfig.json | 26 --- .../src/app/auth-event-handling}/page.tsx | 187 +++++------------- .../login-with-nextjs/src/app/page.tsx | 6 + 20 files changed, 52 insertions(+), 529 deletions(-) delete mode 100644 examples/passport/event-handling-with-nextjs/.env.example delete mode 100644 examples/passport/event-handling-with-nextjs/.eslintrc.json delete mode 100644 examples/passport/event-handling-with-nextjs/.gitignore delete mode 100644 examples/passport/event-handling-with-nextjs/README.md delete mode 100644 examples/passport/event-handling-with-nextjs/features.json delete mode 100644 examples/passport/event-handling-with-nextjs/metadata.json delete mode 100644 examples/passport/event-handling-with-nextjs/next.config.mjs delete mode 100644 examples/passport/event-handling-with-nextjs/package.json delete mode 100644 examples/passport/event-handling-with-nextjs/playwright.config.ts delete mode 100644 examples/passport/event-handling-with-nextjs/src/app/globals.css delete mode 100644 examples/passport/event-handling-with-nextjs/src/app/layout.tsx delete mode 100644 examples/passport/event-handling-with-nextjs/src/app/logout/page.tsx delete mode 100644 examples/passport/event-handling-with-nextjs/src/app/page.tsx delete mode 100644 examples/passport/event-handling-with-nextjs/src/app/redirect/page.tsx delete mode 100644 examples/passport/event-handling-with-nextjs/src/app/utils/setupDefault.ts delete mode 100644 examples/passport/event-handling-with-nextjs/src/app/utils/wrapper.tsx delete mode 100644 examples/passport/event-handling-with-nextjs/tests/event-handling.spec.ts delete mode 100644 examples/passport/event-handling-with-nextjs/tsconfig.json rename examples/passport/{event-handling-with-nextjs/src/app/event-handling => login-with-nextjs/src/app/auth-event-handling}/page.tsx (53%) diff --git a/examples/passport/event-handling-with-nextjs/.env.example b/examples/passport/event-handling-with-nextjs/.env.example deleted file mode 100644 index 7efb22feb5..0000000000 --- a/examples/passport/event-handling-with-nextjs/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -NEXT_PUBLIC_PUBLISHABLE_KEY= -NEXT_PUBLIC_CLIENT_ID= \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/.eslintrc.json b/examples/passport/event-handling-with-nextjs/.eslintrc.json deleted file mode 100644 index acd2aad42f..0000000000 --- a/examples/passport/event-handling-with-nextjs/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "root": true, - "extends": "next/core-web-vitals" -} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/.gitignore b/examples/passport/event-handling-with-nextjs/.gitignore deleted file mode 100644 index 54fe4d6114..0000000000 --- a/examples/passport/event-handling-with-nextjs/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/README.md b/examples/passport/event-handling-with-nextjs/README.md deleted file mode 100644 index e515d86c57..0000000000 --- a/examples/passport/event-handling-with-nextjs/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Immutable Passport Event Handling with Next.js - -This example demonstrates how to handle events from the Immutable Passport SDK in a Next.js application. - -## Getting Started - -1. Clone the repository -2. Install dependencies -```bash -npm install -``` -3. Create a `.env` file with your Immutable Hub credentials: -```bash -NEXT_PUBLIC_PUBLISHABLE_KEY= -NEXT_PUBLIC_CLIENT_ID= -``` -4. Run the development server: -```bash -npm run dev -``` - -## Features - -- Event handling for Passport SDK events -- Login and logout functionality -- Real-time event logging -- Handles authentication redirects - -## Events Demonstrated - -- LOGIN_SUCCESS -- LOGIN_FAILURE -- LOGOUT_SUCCESS -- LOGOUT_FAILURE -- NETWORK_CHANGED -- ACCOUNT_CHANGED \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/features.json b/examples/passport/event-handling-with-nextjs/features.json deleted file mode 100644 index 81dc3ba3c5..0000000000 --- a/examples/passport/event-handling-with-nextjs/features.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "features": [ - { - "feature": "event-handling", - "manually-edited": false, - "silent-login": true, - "silent-logout": true - } - ], - "order": 7 -} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/metadata.json b/examples/passport/event-handling-with-nextjs/metadata.json deleted file mode 100644 index a8e106fff8..0000000000 --- a/examples/passport/event-handling-with-nextjs/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "title": "Event Handling with NextJS", - "description": "A comprehensive example demonstrating how to handle Passport SDK events in a Next.js application.", - "keywords": ["Immutable", "SDK", "Passport", "Events", "Event Handling", "Authentication", "Login", "Logout", "Network", "Account"], - "tech_stack": ["NextJS", "TypeScript", "React"], - "product": "Passport", - "programming_language": "TypeScript" -} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/next.config.mjs b/examples/passport/event-handling-with-nextjs/next.config.mjs deleted file mode 100644 index 4201d2eb6e..0000000000 --- a/examples/passport/event-handling-with-nextjs/next.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = {}; - -export default nextConfig; \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/package.json b/examples/passport/event-handling-with-nextjs/package.json deleted file mode 100644 index 9e255b6a3c..0000000000 --- a/examples/passport/event-handling-with-nextjs/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@examples/event-handling-with-nextjs", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "test": "playwright test" - }, - "dependencies": { - "@biom3/react": "^0.25.21", - "@imtbl/sdk": "workspace:*", - "ethers": "^6.13.4", - "next": "14.2.25", - "react": "^18", - "react-dom": "^18" - }, - "devDependencies": { - "@playwright/test": "^1.45.3", - "@types/node": "^20", - "@types/react": "^18.3.4", - "@types/react-dom": "^18.3.0", - "eslint": "^8", - "eslint-config-next": "14.2.7", - "typescript": "^5.6.2" - } -} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/playwright.config.ts b/examples/passport/event-handling-with-nextjs/playwright.config.ts deleted file mode 100644 index c8ae12b40e..0000000000 --- a/examples/passport/event-handling-with-nextjs/playwright.config.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; - -export default defineConfig({ - testDir: "./tests", - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: "80%", - reporter: "html", - - use: { - baseURL: "http://localhost:3000", - trace: "on-first-retry", - }, - - projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - { name: "firefox", use: { ...devices["Desktop Firefox"] } }, - { name: "webkit", use: { ...devices["Desktop Safari"] } }, - - { name: "Mobile Chrome", use: { ...devices["Pixel 5"] } }, - { name: "Mobile Safari", use: { ...devices["iPhone 12"] } }, - ], - - webServer: { - command: "npm start", - url: "http://localhost:3000", - reuseExistingServer: !process.env.CI, - }, -}); \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/globals.css b/examples/passport/event-handling-with-nextjs/src/app/globals.css deleted file mode 100644 index 5d7b99a321..0000000000 --- a/examples/passport/event-handling-with-nextjs/src/app/globals.css +++ /dev/null @@ -1,39 +0,0 @@ -html, body { - height: 100%; -} -body { - margin: 0; -} -.flex-container { - height: 100%; - padding: 0; - margin: 0; - display: flex; - align-items: center; - justify-content: center; -} - -.mb-1 { - margin-bottom: 1rem; -} - -.event-log { - height: 200px; - width: 100%; - overflow-y: auto; - border: 1px solid #ccc; - padding: 10px; - margin-top: 10px; - background-color: #f5f5f5; -} - -.event-tag { - display: inline-block; - background-color: #3471F2; - color: #FFFFFF; - font-size: 12px; - font-weight: bold; - padding: 4px 8px; - border-radius: 4px; - text-transform: uppercase; -} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/layout.tsx b/examples/passport/event-handling-with-nextjs/src/app/layout.tsx deleted file mode 100644 index a630a3127f..0000000000 --- a/examples/passport/event-handling-with-nextjs/src/app/layout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; -import AppWrapper from "./utils/wrapper"; -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "Passport SDK - Event Handling with NextJS", - description: "Examples of event handling with Passport SDK in NextJS", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - - {children} - - - - ); -} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/logout/page.tsx b/examples/passport/event-handling-with-nextjs/src/app/logout/page.tsx deleted file mode 100644 index cb8ddd0ae6..0000000000 --- a/examples/passport/event-handling-with-nextjs/src/app/logout/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import { Stack, Body, Heading } from '@biom3/react'; -import { useRouter } from 'next/navigation'; - -export default function LogoutPage() { - const router = useRouter(); - - useEffect(() => { - // Redirect to home page after a short delay - const timer = setTimeout(() => { - router.push('/event-handling'); - }, 2000); - - return () => clearTimeout(timer); - }, [router]); - - return ( - - You have been logged out - Redirecting back to the app... - - ); -} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/page.tsx b/examples/passport/event-handling-with-nextjs/src/app/page.tsx deleted file mode 100644 index f588cae9b8..0000000000 --- a/examples/passport/event-handling-with-nextjs/src/app/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; - -export default function HomePage() { - const router = useRouter(); - - useEffect(() => { - router.push('/event-handling'); - }, [router]); - - return ( -
-

Redirecting to Event Handling Example...

-
- ); -} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/redirect/page.tsx b/examples/passport/event-handling-with-nextjs/src/app/redirect/page.tsx deleted file mode 100644 index 52343c7dc7..0000000000 --- a/examples/passport/event-handling-with-nextjs/src/app/redirect/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { Stack, Body, Heading } from '@biom3/react'; -import { useRouter } from 'next/navigation'; -import { passportInstance } from '../utils/setupDefault'; - -export default function RedirectPage() { - const router = useRouter(); - const [error, setError] = useState(null); - - useEffect(() => { - const handleRedirect = async () => { - try { - // Handle the redirect - await passportInstance.loginCallback(); - router.push('/event-handling'); - } catch (e: any) { - console.error('Error handling redirect', e); - setError(e.message || 'An unknown error occurred during login'); - } - }; - - handleRedirect(); - }, [router]); - - return ( - - Processing login... - {error && {error}} - - ); -} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/utils/setupDefault.ts b/examples/passport/event-handling-with-nextjs/src/app/utils/setupDefault.ts deleted file mode 100644 index 5a0475edb2..0000000000 --- a/examples/passport/event-handling-with-nextjs/src/app/utils/setupDefault.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { config, passport } from '@imtbl/sdk'; - -// #doc passport-instance -export const passportInstance = new passport.Passport({ - baseConfig: { - environment: config.Environment.SANDBOX, // or config.Environment.PRODUCTION - publishableKey: - process.env.NEXT_PUBLIC_PUBLISHABLE_KEY || '', // replace with your publishable API key from Hub - }, - clientId: process.env.NEXT_PUBLIC_CLIENT_ID || '', // replace with your client ID from Hub - redirectUri: 'http://localhost:3000/redirect', // replace with one of your redirect URIs from Hub - logoutRedirectUri: 'http://localhost:3000/logout', // replace with one of your logout URIs from Hub - audience: 'platform_api', - scope: 'openid offline_access email transact', - logoutMode: 'silent', // Using silent logout mode - }); -// #enddoc passport-instance \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/utils/wrapper.tsx b/examples/passport/event-handling-with-nextjs/src/app/utils/wrapper.tsx deleted file mode 100644 index 4d19169031..0000000000 --- a/examples/passport/event-handling-with-nextjs/src/app/utils/wrapper.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; -import { BiomeCombinedProviders, Stack } from '@biom3/react'; - -export default function AppWrapper({ - children, - }: Readonly<{ - children: React.ReactNode; - }>) { - return ( -
- - - { children } - - -
- ); -} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/tests/event-handling.spec.ts b/examples/passport/event-handling-with-nextjs/tests/event-handling.spec.ts deleted file mode 100644 index 066adfdf9e..0000000000 --- a/examples/passport/event-handling-with-nextjs/tests/event-handling.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Event Handling', () => { - test('should navigate to event handling page', async ({ page }) => { - await page.goto('/'); - await expect(page).toHaveURL(/.*\/event-handling/); - }); - - test('should display event handling UI elements', async ({ page }) => { - await page.goto('/event-handling'); - - // Check for heading - await expect(page.getByRole('heading', { name: 'Passport SDK - Event Handling Example' })).toBeVisible(); - - // Check for status text - await expect(page.getByText('Status: Logged Out')).toBeVisible(); - - // Check for buttons - await expect(page.getByRole('button', { name: 'Login' })).toBeVisible(); - await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible(); - - // Check for event log - await expect(page.getByText('Event Log:')).toBeVisible(); - await expect(page.locator('.event-log')).toBeVisible(); - }); -}); \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/tsconfig.json b/examples/passport/event-handling-with-nextjs/tsconfig.json deleted file mode 100644 index afa72d9966..0000000000 --- a/examples/passport/event-handling-with-nextjs/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} \ No newline at end of file diff --git a/examples/passport/event-handling-with-nextjs/src/app/event-handling/page.tsx b/examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx similarity index 53% rename from examples/passport/event-handling-with-nextjs/src/app/event-handling/page.tsx rename to examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx index 40b8f4c4f8..067ee387da 100644 --- a/examples/passport/event-handling-with-nextjs/src/app/event-handling/page.tsx +++ b/examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx @@ -1,153 +1,73 @@ 'use client'; -import { useEffect, useState } from 'react'; -import { Button, Heading, Stack, Body, Badge } from '@biom3/react'; -import { passportInstance } from '../utils/setupDefault'; +import { useEffect, useState, useCallback } from 'react'; +import { Button, Heading, Stack, Body } from '@biom3/react'; +import { passportInstance } from '../utils/setupLogoutSilent'; import { passport } from '@imtbl/sdk'; +import { Provider, ProviderEvent } from '@imtbl/sdk/passport'; -// Define the event constants as specified in Immutable documentation -const ProviderEvent = { - ACCOUNTS_CHANGED: 'accountsChanged', - CHAIN_CHANGED: 'chainChanged', - CONNECT: 'connect', - DISCONNECT: 'disconnect', - MESSAGE: 'message' -}; export default function EventHandlingPage() { const [isLoggedIn, setIsLoggedIn] = useState(false); - const [provider, setProvider] = useState(null); + const [provider, setProvider] = useState(undefined); const [events, setEvents] = useState>([]); const [address, setAddress] = useState(''); const [chainId, setChainId] = useState(''); const [loading, setLoading] = useState(false); const [accountsState, setAccountsState] = useState([]); + // Add a new event to the event log - const logEvent = (eventName: string, data: any) => { - // Get a readable event name for display - let displayEventName = eventName; - - // Map enum constants to readable names if needed - if (eventName === ProviderEvent.ACCOUNTS_CHANGED) displayEventName = 'accountsChanged'; - if (eventName === ProviderEvent.CHAIN_CHANGED) displayEventName = 'chainChanged'; - if (eventName === ProviderEvent.CONNECT) displayEventName = 'connect'; - if (eventName === ProviderEvent.DISCONNECT) displayEventName = 'disconnect'; - if (eventName === ProviderEvent.MESSAGE) displayEventName = 'message'; - + const logEvent = useCallback((eventName: string, data: any) => { setEvents(prev => [ { - event: displayEventName, + event: eventName, data: JSON.stringify(data, null, 2), timestamp: new Date().toLocaleTimeString() }, ...prev ].slice(0, 10)); // Keep only the last 10 events - }; + }, []); - // Check login status on mount + // 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(''); + } else { + setAddress(accounts[0]); + } + }, [logEvent]); + + // Initialize provider on mount useEffect(() => { - const checkLoginStatus = async () => { - try { - const userProfile = await passportInstance.getUserInfo(); - if (userProfile) { - setIsLoggedIn(true); - - // Get provider and user info - const provider = await passportInstance.connectEvm(); - setProvider(provider); - - if (provider) { - // Get accounts - const accounts = await provider.request({ method: 'eth_accounts' }); - if (accounts && accounts.length > 0) { - setAddress(accounts[0]); - // Keep accountsState in sync - setAccountsState(accounts); - } - - // Get chain ID - const chainId = await provider.request({ method: 'eth_chainId' }); - setChainId(chainId); - } - } - } catch (error) { - console.error('Error checking login status:', error); - } + const fetchPassportProvider = async () => { + const provider = await passportInstance.connectEvm(); + setProvider(provider); }; - checkLoginStatus(); + fetchPassportProvider(); }, []); - // Setup event listeners when provider is available + // Set up accountsChanged event listener useEffect(() => { if (!provider) return; - const handleConnect = (connectInfo: any) => { - logEvent('connect', connectInfo); - setIsLoggedIn(true); - }; - - const handleDisconnect = (error: any) => { - logEvent('disconnect', error); - setIsLoggedIn(false); - setAddress(''); - setChainId(''); - }; - - const handleChainChanged = (chainId: string) => { - logEvent('chainChanged', { chainId }); - setChainId(chainId); - - // Optional: Reload the page when chain changes to ensure all state is fresh - // window.location.reload(); - }; - - const handleAccountsChanged = (accounts: string[]) => { - console.log('accounts', accounts); - setAccountsState(accounts); - logEvent('accountsChanged', { accounts }); - if (accounts.length === 0) { - // User has disconnected their account - setIsLoggedIn(false); - setAddress(''); - } else { - setAddress(accounts[0]); - } - }; - - const handleMessage = (message: any) => { - logEvent('message', message); - }; - - // Register event listeners using ProviderEvent enum from SDK - provider.on(ProviderEvent.CONNECT, handleConnect); - provider.on(ProviderEvent.DISCONNECT, handleDisconnect); - provider.on(ProviderEvent.CHAIN_CHANGED, handleChainChanged); + // Register event listener provider.on(ProviderEvent.ACCOUNTS_CHANGED, handleAccountsChanged); - provider.on(ProviderEvent.MESSAGE, handleMessage); - // Log that event listeners were registered - logEvent('listeners_registered', { - events: [ - ProviderEvent.CONNECT, - ProviderEvent.DISCONNECT, - ProviderEvent.CHAIN_CHANGED, - ProviderEvent.ACCOUNTS_CHANGED, - ProviderEvent.MESSAGE - ] - }); + // Log that event listener was registered + logEvent('provider_event_registered', { event: ProviderEvent.ACCOUNTS_CHANGED }); - // Cleanup function to remove event listeners + // Cleanup function to remove event listener return () => { - if (provider) { - provider.removeListener(ProviderEvent.CONNECT, handleConnect); - provider.removeListener(ProviderEvent.DISCONNECT, handleDisconnect); - provider.removeListener(ProviderEvent.CHAIN_CHANGED, handleChainChanged); - provider.removeListener(ProviderEvent.ACCOUNTS_CHANGED, handleAccountsChanged); - provider.removeListener(ProviderEvent.MESSAGE, handleMessage); - } + provider.removeListener(ProviderEvent.ACCOUNTS_CHANGED, handleAccountsChanged); }; - }, [provider]); // Only re-run when provider changes + }, [provider, handleAccountsChanged, logEvent]); // Handle login const handleLogin = async () => { @@ -164,10 +84,8 @@ export default function EventHandlingPage() { const accounts = await provider.request({ method: 'eth_requestAccounts' }); if (accounts && accounts.length > 0) { setAddress(accounts[0]); - // Manually update accountsState since the event might not fire during initial login setAccountsState(accounts); - - // Manually log the accounts information as if the event fired + // Log the accounts change manually since the event might not fire logEvent(ProviderEvent.ACCOUNTS_CHANGED, { accounts }); } @@ -177,7 +95,6 @@ export default function EventHandlingPage() { } setIsLoggedIn(true); - logEvent('login', { success: true }); } catch (error) { console.error('Login error:', error); logEvent('login_error', error); @@ -194,9 +111,7 @@ export default function EventHandlingPage() { setIsLoggedIn(false); setAddress(''); setChainId(''); - // Clear accounts state on logout setAccountsState([]); - logEvent('logout', { success: true }); } catch (error) { console.error('Logout error:', error); logEvent('logout_error', error); @@ -205,21 +120,17 @@ export default function EventHandlingPage() { } }; - // Trigger a custom event (switch chain request) - const requestChainSwitch = async () => { + // Get current chain info + const getChainInfo = async () => { if (!provider) return; try { setLoading(true); - // Request to switch to Ethereum mainnet (chain ID 0x1) - await provider.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x1' }], - }); - logEvent('request_chain_switch', { chainId: '0x1' }); + const currentChainId = await provider.request({ method: 'eth_chainId' }); + setChainId(currentChainId); } catch (error) { - console.error('Chain switch error:', error); - logEvent('chain_switch_error', error); + console.error('Chain info error:', error); + logEvent('chain_info_error', error); } finally { setLoading(false); } @@ -237,6 +148,7 @@ export default function EventHandlingPage() { logEvent('request_accounts', { accounts }); if (accounts && accounts.length > 0) { setAddress(accounts[0]); + setAccountsState(accounts); } } catch (error) { console.error('Request accounts error:', error); @@ -297,14 +209,7 @@ export default function EventHandlingPage() { {isLoggedIn && ( - - + - + )} diff --git a/examples/passport/login-with-nextjs/src/app/page.tsx b/examples/passport/login-with-nextjs/src/app/page.tsx index 7496b67093..a6219f2513 100644 --- a/examples/passport/login-with-nextjs/src/app/page.tsx +++ b/examples/passport/login-with-nextjs/src/app/page.tsx @@ -39,5 +39,11 @@ export default function Home() { rc={}> Logout with Silent Mode + ); } \ No newline at end of file From 09284a3f89d0e8572f5b0cb6fc1179925ae595ce Mon Sep 17 00:00:00 2001 From: darrenmelvison1 Date: Tue, 8 Apr 2025 12:41:22 +1000 Subject: [PATCH 03/10] spacing changes --- .../src/app/auth-event-handling/page.tsx | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx b/examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx index 067ee387da..828e59a5b1 100644 --- a/examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx +++ b/examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx @@ -3,7 +3,6 @@ import { useEffect, useState, useCallback } from 'react'; import { Button, Heading, Stack, Body } from '@biom3/react'; import { passportInstance } from '../utils/setupLogoutSilent'; -import { passport } from '@imtbl/sdk'; import { Provider, ProviderEvent } from '@imtbl/sdk/passport'; @@ -120,22 +119,6 @@ export default function EventHandlingPage() { } }; - // Get current chain info - const getChainInfo = async () => { - if (!provider) return; - - try { - setLoading(true); - const currentChainId = await provider.request({ method: 'eth_chainId' }); - setChainId(currentChainId); - } catch (error) { - console.error('Chain info error:', error); - logEvent('chain_info_error', error); - } finally { - setLoading(false); - } - }; - // Manually request accounts const requestAccounts = async () => { if (!provider) return; @@ -192,7 +175,7 @@ export default function EventHandlingPage() { )} - + - + )} From 163537dfbc7868c88f651681753029f15e543eea Mon Sep 17 00:00:00 2001 From: darrenmelvison1 Date: Tue, 8 Apr 2025 16:12:48 +1000 Subject: [PATCH 04/10] reverted pnpm.lock --- pnpm-lock.yaml | 45 +-------------------------------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff46206027..1fe0b6a558 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -623,49 +623,6 @@ importers: specifier: ^5.6.2 version: 5.6.2 - examples/passport/event-handling-with-nextjs: - dependencies: - '@biom3/react': - specifier: ^0.25.21 - version: 0.25.21(@emotion/react@11.11.3(@types/react@18.3.12)(react@18.3.1))(@rive-app/react-canvas-lite@4.9.0(react@18.3.1))(embla-carousel-react@8.1.5(react@18.3.1))(framer-motion@11.18.2(@emotion/is-prop-valid@0.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@imtbl/sdk': - specifier: workspace:* - version: link:../../../sdk - ethers: - specifier: ^6.13.4 - version: 6.13.5(bufferutil@4.0.8)(utf-8-validate@5.0.10) - next: - specifier: 14.2.25 - version: 14.2.25(@babel/core@7.26.9)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: - specifier: ^18 - version: 18.3.1 - react-dom: - specifier: ^18 - version: 18.3.1(react@18.3.1) - devDependencies: - '@playwright/test': - specifier: ^1.45.3 - version: 1.45.3 - '@types/node': - specifier: ^20 - version: 20.14.13 - '@types/react': - specifier: ^18.3.4 - version: 18.3.12 - '@types/react-dom': - specifier: ^18.3.0 - version: 18.3.0 - eslint: - specifier: ^8 - version: 8.57.0 - eslint-config-next: - specifier: 14.2.7 - version: 14.2.7(eslint@8.57.0)(typescript@5.6.2) - typescript: - specifier: ^5.6.2 - version: 5.6.2 - examples/passport/logged-in-user-with-nextjs: dependencies: '@biom3/react': @@ -38359,4 +38316,4 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 immer: 9.0.21 - react: 18.3.1 + react: 18.3.1 \ No newline at end of file From 484451a2590e2608368c7e13136127bb13f32177 Mon Sep 17 00:00:00 2001 From: darrenmelvison1 Date: Wed, 9 Apr 2025 13:03:33 +1000 Subject: [PATCH 05/10] fixed the event handling page layout --- .../src/app/auth-event-handling/page.tsx | 199 ++++++++++-------- pnpm-lock.yaml | 2 +- 2 files changed, 116 insertions(+), 85 deletions(-) diff --git a/examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx b/examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx index 828e59a5b1..ef3df74034 100644 --- a/examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx +++ b/examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx @@ -1,7 +1,8 @@ 'use client'; import { useEffect, useState, useCallback } from 'react'; -import { Button, Heading, Stack, Body } from '@biom3/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'; @@ -37,20 +38,42 @@ export default function EventHandlingPage() { // 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 () => { - const provider = await passportInstance.connectEvm(); - setProvider(provider); + // 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(() => { @@ -96,7 +119,7 @@ export default function EventHandlingPage() { setIsLoggedIn(true); } catch (error) { console.error('Login error:', error); - logEvent('login_error', error); + logEvent('login_error', { message: error instanceof Error ? error.message : String(error) }); } finally { setLoading(false); } @@ -111,106 +134,99 @@ export default function EventHandlingPage() { 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', error); - } finally { - setLoading(false); - } - }; - - // Manually request accounts - const requestAccounts = async () => { - if (!provider) return; - - try { - setLoading(true); - const accounts = await provider.request({ - method: 'eth_requestAccounts' - }); - logEvent('request_accounts', { accounts }); - if (accounts && accounts.length > 0) { - setAddress(accounts[0]); - setAccountsState(accounts); - } - } catch (error) { - console.error('Request accounts error:', error); - logEvent('request_accounts_error', error); + logEvent('logout_error', { message: error instanceof Error ? error.message : String(error) }); } finally { setLoading(false); } }; return ( - - Passport SDK - Event Handling Example + <> + Passport SDK - Event Handling Example - - Status: {isLoggedIn ? 'Logged In' : 'Logged Out'} - - {isLoggedIn && address && ( - - Connected Account: - {address} - - )} - - {isLoggedIn && chainId && ( - - Chain ID: - {chainId} - - )} - {accountsState && accountsState.length > 0 && ( - - Accounts: -
- {accountsState.map((account, idx) => ( -
- {account} {idx === 0 && (active)} -
- ))} -
-
- )} -
+ {/* Buttons Section */} - + {!isLoggedIn && ( + )} + {isLoggedIn && ( - - - {isLoggedIn && ( - - - )} - + {/* State Data Table */} + {(isLoggedIn || accountsState.length > 0) && ( + <> + + + + Key + Value + + + + + Status + {isLoggedIn ? 'Logged In' : 'Logged Out'} + + {address && ( + + Address + {address} + + )} + {chainId && ( + + Chain ID + {chainId} + + )} + {accountsState.length > 0 && ( + + Accounts ({accountsState.length}) + +
+ {accountsState.map((account, idx) => ( +
+ {account} {idx === 0 && (active)} +
+ ))} +
+
+
+ )} +
+
+ + )} +
+ {/* Event Log Section */} + <> Event Log:
{events.length === 0 ? ( No events logged yet @@ -223,13 +239,26 @@ export default function EventHandlingPage() { borderRadius: '4px' }}> -
{event.event}
+ {/* Use a more distinct tag style */} + + {event.event} + {event.timestamp}
                   {event.data}
                 
@@ -237,7 +266,9 @@ export default function EventHandlingPage() { )) )}
-
-
+ +
+ }>Return to Examples + ); } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fe0b6a558..503505ac0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38316,4 +38316,4 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 immer: 9.0.21 - react: 18.3.1 \ No newline at end of file + react: 18.3.1 From 73c7930ab58d42afd5fb02730275e5957011784c Mon Sep 17 00:00:00 2001 From: darrenmelvison1 Date: Wed, 9 Apr 2025 13:10:37 +1000 Subject: [PATCH 06/10] added E2E test for event handling --- .../passport/login-with-nextjs/tests/base.spec.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/passport/login-with-nextjs/tests/base.spec.ts b/examples/passport/login-with-nextjs/tests/base.spec.ts index dee5e54860..996f6893bc 100644 --- a/examples/passport/login-with-nextjs/tests/base.spec.ts +++ b/examples/passport/login-with-nextjs/tests/base.spec.ts @@ -13,7 +13,8 @@ test.describe("home page", () => { "Login with EtherJS", "Login with Identity only", "Logout with Redirect Mode", - "Logout with Silent Mode" + "Logout with Silent Mode", + "Auth Event Handling" ]; for (const name of buttonNames) { @@ -66,4 +67,10 @@ test.describe("sub-pages navigation", () => { await expect(page.getByRole("button", { name: /Login|Logout/ })).toBeVisible(); await expect(page.getByRole("link", { name: "Return to Examples" })).toBeVisible(); }); -}); \ No newline at end of file + + test("Check Auth Event Handling", async ({ page }) => { + await page.click("text=Auth Event Handling"); + await expect(page.getByRole("heading", { name: "Passport SDK - Event Handling Example" })).toBeVisible(); + await expect(page.getByRole("link", { name: "Return to Examples" })).toBeVisible(); + }); +}); From 5b2f2f9fba9c36b44e5b77b17c9bf4e43ba308c3 Mon Sep 17 00:00:00 2001 From: darrenmelvison1 Date: Wed, 9 Apr 2025 13:19:25 +1000 Subject: [PATCH 07/10] renamed prompt files --- .../{prompt-part1.txt => example-app-1-create-app-template.txt} | 0 .../passport/{prompt-part2.txt => example-app-2-add-feature.txt} | 0 examples/passport/{prompt-part3.txt => example-app-3-fix-ui.txt} | 0 examples/passport/{prompt-part4.txt => example-app-4-testing.txt} | 0 examples/passport/{prompt.txt => tutorial-generation-prompt.txt} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename examples/passport/{prompt-part1.txt => example-app-1-create-app-template.txt} (100%) rename examples/passport/{prompt-part2.txt => example-app-2-add-feature.txt} (100%) rename examples/passport/{prompt-part3.txt => example-app-3-fix-ui.txt} (100%) rename examples/passport/{prompt-part4.txt => example-app-4-testing.txt} (100%) rename examples/passport/{prompt.txt => tutorial-generation-prompt.txt} (100%) diff --git a/examples/passport/prompt-part1.txt b/examples/passport/example-app-1-create-app-template.txt similarity index 100% rename from examples/passport/prompt-part1.txt rename to examples/passport/example-app-1-create-app-template.txt diff --git a/examples/passport/prompt-part2.txt b/examples/passport/example-app-2-add-feature.txt similarity index 100% rename from examples/passport/prompt-part2.txt rename to examples/passport/example-app-2-add-feature.txt diff --git a/examples/passport/prompt-part3.txt b/examples/passport/example-app-3-fix-ui.txt similarity index 100% rename from examples/passport/prompt-part3.txt rename to examples/passport/example-app-3-fix-ui.txt diff --git a/examples/passport/prompt-part4.txt b/examples/passport/example-app-4-testing.txt similarity index 100% rename from examples/passport/prompt-part4.txt rename to examples/passport/example-app-4-testing.txt diff --git a/examples/passport/prompt.txt b/examples/passport/tutorial-generation-prompt.txt similarity index 100% rename from examples/passport/prompt.txt rename to examples/passport/tutorial-generation-prompt.txt From cb2e3984b64f26c84bf1d5d7f15e3c723ba9bd6d Mon Sep 17 00:00:00 2001 From: darrenmelvison1 Date: Wed, 9 Apr 2025 13:41:17 +1000 Subject: [PATCH 08/10] updated login-with-nextjs tutorial content --- .../passport/login-with-nextjs/tutorial.md | 198 +++++++----------- 1 file changed, 81 insertions(+), 117 deletions(-) diff --git a/examples/passport/login-with-nextjs/tutorial.md b/examples/passport/login-with-nextjs/tutorial.md index 03a220e5b4..752d727f94 100644 --- a/examples/passport/login-with-nextjs/tutorial.md +++ b/examples/passport/login-with-nextjs/tutorial.md @@ -1,10 +1,10 @@ -This example demonstrates various login and logout implementations using Immutable Passport with Next.js. The app showcases different authentication methods and logout strategies including standard login, login with ethers.js, identity-only login, and different logout modes. +This example demonstrates various login and logout implementations using the Immutable Passport SDK in a Next.js application. It showcases different authentication methods including login with EthersJS, identity-only login, and various logout strategies, along with authentication event handling.
@@ -13,90 +13,40 @@ This example demonstrates various login and logout implementations using Immutab
## Features Overview - -- Login with Passport (EVM wallet connection) -- Login with ethers.js integration -- Login with identity only (without wallet) +- Login with EthersJS integration +- Identity-only login (without wallet connection) - Standard logout functionality - Logout with redirect mode -- Logout with silent mode +- Silent logout implementation +- Authentication event handling ## SDK Integration Details -### Login with Passport -**Feature Name**: Standard login with Passport connecting an EVM wallet -**Source Code**: [Source code file](https://github.com/immutable/ts-immutable-sdk/blob/main/examples/passport/login-with-nextjs/src/app/login-with-passport/page.tsx) -**Implementation**: -```typescript -const loginWithPassport = async () => { - if (!passportInstance) return; - try { - const provider = await passportInstance.connectEvm(); - const accounts = await provider.request({ method: 'eth_requestAccounts' }); - if (accounts) { - setIsLoggedIn(true); - setAccountAddress(accounts[0] || null); - } else { - setIsLoggedIn(false); - } - } catch (error) { - console.error('Error connecting to Passport:', error); - setIsLoggedIn(false); - } -}; -``` -**Explanation**: This implementation creates a connection to the user's EVM wallet through Passport. The `connectEvm()` method returns a provider that follows the EIP-1193 interface, which can then be used to request account access with `eth_requestAccounts`. Once connected, the user's wallet address is stored in the application state. +### Login with EthersJS +**Feature Name**: Login with EthersJS allows users to connect their wallet and authenticate using EthersJS provider. + +**Source Code**: [login-with-etherjs/page.tsx](https://github.com/immutable/ts-immutable-sdk/blob/main/examples/passport/login-with-nextjs/src/app/login-with-etherjs/page.tsx) -### Login with ethers.js -**Feature Name**: Login integration with ethers.js library -**Source Code**: [Source code file](https://github.com/immutable/ts-immutable-sdk/blob/main/examples/passport/login-with-nextjs/src/app/login-with-etherjs/page.tsx) **Implementation**: ```typescript -const loginWithEthersjs = async () => { - if (!passportInstance) return; - try { - const passportProvider = await passportInstance.connectEvm(); - const web3Provider = new BrowserProvider(passportProvider); - const accounts = await web3Provider.send('eth_requestAccounts', []); - if (accounts && accounts.length > 0) { - setIsLoggedIn(true); - setAccountAddress(accounts[0] || null); - } else { - setIsLoggedIn(false); - } - } catch (error) { - console.error('Error connecting to Passport with Ethers.js:', error); - setIsLoggedIn(false); - } -}; +const passportProvider = await passportInstance.connectEvm(); +const web3Provider = new BrowserProvider(passportProvider); +const accounts = await web3Provider.send('eth_requestAccounts', []); ``` -**Explanation**: This implementation demonstrates how to integrate Passport with ethers.js. It gets the Passport provider and wraps it in an ethers.js BrowserProvider, allowing developers to leverage ethers.js functionality with Passport authentication. -### Login with Identity Only -**Feature Name**: Login with Passport identity without connecting a wallet -**Source Code**: [Source code file](https://github.com/immutable/ts-immutable-sdk/blob/main/examples/passport/login-with-nextjs/src/app/login-with-identity-only/page.tsx) +**Explanation**: This implementation uses EthersJS's BrowserProvider to interact with the Passport provider. It requests user accounts and manages the connection state, displaying the connected account address when successful. + +### Identity-only Login +**Feature Name**: Login without wallet connection, focusing on user identity authentication. + +**Source Code**: [login-with-identity-only/page.tsx](https://github.com/immutable/ts-immutable-sdk/blob/main/examples/passport/login-with-nextjs/src/app/login-with-identity-only/page.tsx) + **Implementation**: ```typescript -const loginWithIdentiy = async () => { - if (!passportInstance) return; - try { - const profile: passport.UserProfile | null = await passportInstance.login(); - if (profile) { - console.log(profile.email); - console.log(profile.sub); - setIsLoggedIn(true); - setEmail(profile.email || 'No Email'); - setSub(profile.sub || 'No Subject'); - } else { - setIsLoggedIn(false); - } - } catch (error) { - console.error('Error connecting to Passport', error); - setIsLoggedIn(false); - } -}; +const profile: passport.UserProfile | null = await passportInstance.login(); ``` -**Explanation**: This approach allows users to authenticate with Passport without connecting a wallet. The `login()` method returns a UserProfile object containing user information like email and subject identifier (sub), enabling user identification without requiring wallet interaction. + +**Explanation**: This implementation demonstrates how to authenticate users without requiring a wallet connection. It retrieves the user's profile information including email and subject ID. ### Logout **Feature Name**: Standard logout functionality @@ -116,6 +66,7 @@ const logout = async () => { ``` **Explanation**: The standard logout implementation disconnects the user from Passport using the `logout()` method. This ends the user's session and clears any authentication state. + ### Logout with Redirect Mode **Feature Name**: Logout with redirect to a specific page **Source Code**: [Source code file](https://github.com/immutable/ts-immutable-sdk/blob/main/examples/passport/login-with-nextjs/src/app/utils/setupLogoutRedirect.ts) @@ -137,60 +88,73 @@ export const passportInstance = new passport.Passport({ ``` **Explanation**: This configuration sets up Passport to use 'redirect' mode for logout. When `logout()` is called, the user will be redirected to the specified `logoutRedirectUri` after being logged out. -### Logout with Silent Mode -**Feature Name**: Logout without page redirection -**Source Code**: [Source code file](https://github.com/immutable/ts-immutable-sdk/blob/main/examples/passport/login-with-nextjs/src/app/utils/setupLogoutSilent.ts) + +### Silent Logout +**Feature Name**: Logout implementation that silently signs out the user without redirects. + +**Source Code**: [logout-with-silent-mode/page.tsx](https://github.com/immutable/ts-immutable-sdk/blob/main/examples/passport/login-with-nextjs/src/app/logout-with-silent-mode/page.tsx) + **Implementation**: ```typescript -export const passportInstance = new passport.Passport({ - baseConfig: { - environment: config.Environment.SANDBOX, - publishableKey: - process.env.NEXT_PUBLIC_PUBLISHABLE_KEY || '', - }, - clientId: process.env.NEXT_PUBLIC_CLIENT_ID || '', - redirectUri: 'http://localhost:3000/redirect', - logoutMode: 'silent', - logoutRedirectUri: 'http://localhost:3000/silent-logout', - audience: 'platform_api', - scope: 'openid offline_access email transact', - }); +await passportInstance.logout(); +``` + +**Explanation**: This implementation shows how to implement a silent logout that disconnects the user without redirecting to another page, providing a seamless user experience. + +### Authentication Event Handling +**Feature Name**: Handling authentication-related events such as account changes. + +**Source Code**: [auth-event-handling/page.tsx](https://github.com/immutable/ts-immutable-sdk/blob/main/examples/passport/login-with-nextjs/src/app/auth-event-handling/page.tsx) + +**Implementation**: +```typescript +provider.on(ProviderEvent.ACCOUNTS_CHANGED, handleAccountsChanged); + +const handleAccountsChanged = (accounts: string[]) => { + setAccountsState(accounts); + if (accounts.length === 0) { + setIsLoggedIn(false); + setAddress(''); + setChainId(''); + } else { + setAddress(accounts[0]); + } +}; ``` -**Explanation**: This configuration sets up Passport to use 'silent' mode for logout. When `logout()` is called, the user remains on the current page while the logout process happens in the background, providing a smoother user experience. + +**Explanation**: This implementation shows how to listen for and handle authentication events, specifically the `ACCOUNTS_CHANGED` event. It updates the application state when accounts are changed or disconnected, maintaining synchronization between the wallet state and the application. ## Running the App ### Prerequisites -1. Node.js installed on your machine -2. [Immutable Hub account](https://hub.immutable.com/) for environment setup -3. A registered application in Immutable Hub with client ID and publishable key +- Node.js 16 or higher +- pnpm package manager +- [Immutable Developer Hub](https://hub.immutable.com/) account for environment setup + +### Local Setup Steps +1. Clone the repository: +```bash +git clone https://github.com/immutable/ts-immutable-sdk.git +``` -### Setup and Run -1. Clone the repository 2. Navigate to the example directory: - ```bash - cd examples/passport/login-with-nextjs - ``` +```bash +cd examples/passport/login-with-nextjs +``` + 3. Install dependencies: - ```bash - pnpm install - ``` -4. Create a `.env.local` file in the root directory with your credentials: - ``` - NEXT_PUBLIC_CLIENT_ID= - NEXT_PUBLIC_PUBLISHABLE_KEY= - ``` -5. Start the development server: - ```bash - pnpm dev - ``` -6. Open [http://localhost:3000](http://localhost:3000) in your browser +```bash +pnpm install +``` -## Summary +4. Copy the `.env.example` file to `.env` and update with your Passport client credentials from the [Immutable Developer Hub](https://hub.immutable.com/). -This example demonstrates various ways to implement authentication with Immutable Passport in a Next.js application. It showcases: -- Different login methods: standard Passport login, ethers.js integration, and identity-only login -- Multiple logout strategies: standard logout, redirect mode, and silent mode -- How to set up Passport SDK with different configurations +5. Start the development server: +```bash +pnpm dev +``` + +6. Open your browser and navigate to `http://localhost:3000` -By exploring these examples, developers can choose the authentication approach that best suits their application's needs while maintaining a smooth user experience. \ No newline at end of file +## Summary +This example demonstrates various authentication patterns using the Immutable Passport SDK in a Next.js application. It showcases different login methods including EthersJS integration and identity-only authentication, various logout strategies (redirect and silent modes), and proper handling of authentication events. Developers can use this example as a reference for implementing comprehensive Passport authentication in their own Next.js applications. \ No newline at end of file From 95c3adbfb9c5c8346a06a4eee4bb6ad0a2007980 Mon Sep 17 00:00:00 2001 From: darrenmelvison1 Date: Wed, 9 Apr 2025 14:58:19 +1000 Subject: [PATCH 09/10] renamed other prompt file names and updated readme to reflect new file name --- examples/README.md | 36 +++++++++---------- ... => example-app-1-create-app-template.txt} | 0 ...art2.txt => example-app-2-add-feature.txt} | 0 ...mpt-part3.txt => example-app-3-fix-ui.txt} | 0 ...pt-part4.txt => example-app-4-testing.txt} | 0 ...mpt.txt => tutorial-generation-prompt.txt} | 0 ... => example-app-1-create-app-template.txt} | 0 ...art2.txt => example-app-2-add-feature.txt} | 0 ...mpt-part3.txt => example-app-3-fix-ui.txt} | 0 ...pt-part4.txt => example-app-4-testing.txt} | 0 ...mpt.txt => tutorial-generation-prompt.txt} | 0 ... => example-app-1-create-app-template.txt} | 0 ...art2.txt => example-app-2-add-feature.txt} | 0 ...mpt-part3.txt => example-app-3-fix-ui.txt} | 0 ...pt-part4.txt => example-app-4-testing.txt} | 0 ...mpt.txt => tutorial-generation-prompt.txt} | 0 16 files changed, 18 insertions(+), 18 deletions(-) rename examples/checkout/{prompt-part1.txt => example-app-1-create-app-template.txt} (100%) rename examples/checkout/{prompt-part2.txt => example-app-2-add-feature.txt} (100%) rename examples/checkout/{prompt-part3.txt => example-app-3-fix-ui.txt} (100%) rename examples/checkout/{prompt-part4.txt => example-app-4-testing.txt} (100%) rename examples/checkout/{prompt.txt => tutorial-generation-prompt.txt} (100%) rename examples/contracts/{prompt-part1.txt => example-app-1-create-app-template.txt} (100%) rename examples/contracts/{prompt-part2.txt => example-app-2-add-feature.txt} (100%) rename examples/contracts/{prompt-part3.txt => example-app-3-fix-ui.txt} (100%) rename examples/contracts/{prompt-part4.txt => example-app-4-testing.txt} (100%) rename examples/contracts/{prompt.txt => tutorial-generation-prompt.txt} (100%) rename examples/orderbook/{prompt-part1.txt => example-app-1-create-app-template.txt} (100%) rename examples/orderbook/{prompt-part2.txt => example-app-2-add-feature.txt} (100%) rename examples/orderbook/{prompt-part3.txt => example-app-3-fix-ui.txt} (100%) rename examples/orderbook/{prompt-part4.txt => example-app-4-testing.txt} (100%) rename examples/orderbook/{prompt.txt => tutorial-generation-prompt.txt} (100%) diff --git a/examples/README.md b/examples/README.md index b061fd5d45..21b500db4b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -331,9 +331,9 @@ pnpm test 1) Open the product folder under /examples/{product} -2) Under the {product} directory, open the prompt-part2.txt file. +2) Under the {product} 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) @@ -347,7 +347,7 @@ the 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: @@ -355,7 +355,7 @@ feature name: 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: @@ -369,7 +369,7 @@ feature name: ## 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: @@ -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. @@ -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` 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. @@ -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 folder you are wanting to generate the tutorials and metadata for e.g. `examples/passport/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: ` 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. diff --git a/examples/checkout/prompt-part1.txt b/examples/checkout/example-app-1-create-app-template.txt similarity index 100% rename from examples/checkout/prompt-part1.txt rename to examples/checkout/example-app-1-create-app-template.txt diff --git a/examples/checkout/prompt-part2.txt b/examples/checkout/example-app-2-add-feature.txt similarity index 100% rename from examples/checkout/prompt-part2.txt rename to examples/checkout/example-app-2-add-feature.txt diff --git a/examples/checkout/prompt-part3.txt b/examples/checkout/example-app-3-fix-ui.txt similarity index 100% rename from examples/checkout/prompt-part3.txt rename to examples/checkout/example-app-3-fix-ui.txt diff --git a/examples/checkout/prompt-part4.txt b/examples/checkout/example-app-4-testing.txt similarity index 100% rename from examples/checkout/prompt-part4.txt rename to examples/checkout/example-app-4-testing.txt diff --git a/examples/checkout/prompt.txt b/examples/checkout/tutorial-generation-prompt.txt similarity index 100% rename from examples/checkout/prompt.txt rename to examples/checkout/tutorial-generation-prompt.txt diff --git a/examples/contracts/prompt-part1.txt b/examples/contracts/example-app-1-create-app-template.txt similarity index 100% rename from examples/contracts/prompt-part1.txt rename to examples/contracts/example-app-1-create-app-template.txt diff --git a/examples/contracts/prompt-part2.txt b/examples/contracts/example-app-2-add-feature.txt similarity index 100% rename from examples/contracts/prompt-part2.txt rename to examples/contracts/example-app-2-add-feature.txt diff --git a/examples/contracts/prompt-part3.txt b/examples/contracts/example-app-3-fix-ui.txt similarity index 100% rename from examples/contracts/prompt-part3.txt rename to examples/contracts/example-app-3-fix-ui.txt diff --git a/examples/contracts/prompt-part4.txt b/examples/contracts/example-app-4-testing.txt similarity index 100% rename from examples/contracts/prompt-part4.txt rename to examples/contracts/example-app-4-testing.txt diff --git a/examples/contracts/prompt.txt b/examples/contracts/tutorial-generation-prompt.txt similarity index 100% rename from examples/contracts/prompt.txt rename to examples/contracts/tutorial-generation-prompt.txt diff --git a/examples/orderbook/prompt-part1.txt b/examples/orderbook/example-app-1-create-app-template.txt similarity index 100% rename from examples/orderbook/prompt-part1.txt rename to examples/orderbook/example-app-1-create-app-template.txt diff --git a/examples/orderbook/prompt-part2.txt b/examples/orderbook/example-app-2-add-feature.txt similarity index 100% rename from examples/orderbook/prompt-part2.txt rename to examples/orderbook/example-app-2-add-feature.txt diff --git a/examples/orderbook/prompt-part3.txt b/examples/orderbook/example-app-3-fix-ui.txt similarity index 100% rename from examples/orderbook/prompt-part3.txt rename to examples/orderbook/example-app-3-fix-ui.txt diff --git a/examples/orderbook/prompt-part4.txt b/examples/orderbook/example-app-4-testing.txt similarity index 100% rename from examples/orderbook/prompt-part4.txt rename to examples/orderbook/example-app-4-testing.txt diff --git a/examples/orderbook/prompt.txt b/examples/orderbook/tutorial-generation-prompt.txt similarity index 100% rename from examples/orderbook/prompt.txt rename to examples/orderbook/tutorial-generation-prompt.txt From b7ae8bd6a59af68f6fd034492e31271b5cb6c871 Mon Sep 17 00:00:00 2001 From: darrenmelvison1 Date: Wed, 9 Apr 2025 15:10:46 +1000 Subject: [PATCH 10/10] moved all prompts to be under a directory and updated readme --- examples/README.md | 8 ++++---- .../{ => _prompts}/example-app-1-create-app-template.txt | 0 .../checkout/{ => _prompts}/example-app-2-add-feature.txt | 0 examples/checkout/{ => _prompts}/example-app-3-fix-ui.txt | 0 .../checkout/{ => _prompts}/example-app-4-testing.txt | 0 .../{ => _prompts}/tutorial-generation-prompt.txt | 0 .../{ => _prompts}/example-app-1-create-app-template.txt | 0 .../{ => _prompts}/example-app-2-add-feature.txt | 0 .../contracts/{ => _prompts}/example-app-3-fix-ui.txt | 0 .../contracts/{ => _prompts}/example-app-4-testing.txt | 0 .../{ => _prompts}/tutorial-generation-prompt.txt | 0 .../{ => _prompts}/example-app-1-create-app-template.txt | 0 .../{ => _prompts}/example-app-2-add-feature.txt | 0 .../orderbook/{ => _prompts}/example-app-3-fix-ui.txt | 0 .../orderbook/{ => _prompts}/example-app-4-testing.txt | 0 .../{ => _prompts}/tutorial-generation-prompt.txt | 0 .../{ => _prompts}/example-app-1-create-app-template.txt | 0 .../passport/{ => _prompts}/example-app-2-add-feature.txt | 0 examples/passport/{ => _prompts}/example-app-3-fix-ui.txt | 0 .../passport/{ => _prompts}/example-app-4-testing.txt | 0 .../{ => _prompts}/tutorial-generation-prompt.txt | 0 21 files changed, 4 insertions(+), 4 deletions(-) rename examples/checkout/{ => _prompts}/example-app-1-create-app-template.txt (100%) rename examples/checkout/{ => _prompts}/example-app-2-add-feature.txt (100%) rename examples/checkout/{ => _prompts}/example-app-3-fix-ui.txt (100%) rename examples/checkout/{ => _prompts}/example-app-4-testing.txt (100%) rename examples/checkout/{ => _prompts}/tutorial-generation-prompt.txt (100%) rename examples/contracts/{ => _prompts}/example-app-1-create-app-template.txt (100%) rename examples/contracts/{ => _prompts}/example-app-2-add-feature.txt (100%) rename examples/contracts/{ => _prompts}/example-app-3-fix-ui.txt (100%) rename examples/contracts/{ => _prompts}/example-app-4-testing.txt (100%) rename examples/contracts/{ => _prompts}/tutorial-generation-prompt.txt (100%) rename examples/orderbook/{ => _prompts}/example-app-1-create-app-template.txt (100%) rename examples/orderbook/{ => _prompts}/example-app-2-add-feature.txt (100%) rename examples/orderbook/{ => _prompts}/example-app-3-fix-ui.txt (100%) rename examples/orderbook/{ => _prompts}/example-app-4-testing.txt (100%) rename examples/orderbook/{ => _prompts}/tutorial-generation-prompt.txt (100%) rename examples/passport/{ => _prompts}/example-app-1-create-app-template.txt (100%) rename examples/passport/{ => _prompts}/example-app-2-add-feature.txt (100%) rename examples/passport/{ => _prompts}/example-app-3-fix-ui.txt (100%) rename examples/passport/{ => _prompts}/example-app-4-testing.txt (100%) rename examples/passport/{ => _prompts}/tutorial-generation-prompt.txt (100%) diff --git a/examples/README.md b/examples/README.md index 21b500db4b..3d967f2c49 100644 --- a/examples/README.md +++ b/examples/README.md @@ -329,9 +329,9 @@ 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 example-app-2-add-feature.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 example-app-2-add-feature.txt file. @@ -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 `tutorial-generation-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. @@ -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 `tutorial-generation-prompt.txt` file in the examples/product folder you are wanting to generate the tutorials and metadata for e.g. `examples/passport/tutorial-generation-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: ` 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. diff --git a/examples/checkout/example-app-1-create-app-template.txt b/examples/checkout/_prompts/example-app-1-create-app-template.txt similarity index 100% rename from examples/checkout/example-app-1-create-app-template.txt rename to examples/checkout/_prompts/example-app-1-create-app-template.txt diff --git a/examples/checkout/example-app-2-add-feature.txt b/examples/checkout/_prompts/example-app-2-add-feature.txt similarity index 100% rename from examples/checkout/example-app-2-add-feature.txt rename to examples/checkout/_prompts/example-app-2-add-feature.txt diff --git a/examples/checkout/example-app-3-fix-ui.txt b/examples/checkout/_prompts/example-app-3-fix-ui.txt similarity index 100% rename from examples/checkout/example-app-3-fix-ui.txt rename to examples/checkout/_prompts/example-app-3-fix-ui.txt diff --git a/examples/checkout/example-app-4-testing.txt b/examples/checkout/_prompts/example-app-4-testing.txt similarity index 100% rename from examples/checkout/example-app-4-testing.txt rename to examples/checkout/_prompts/example-app-4-testing.txt diff --git a/examples/checkout/tutorial-generation-prompt.txt b/examples/checkout/_prompts/tutorial-generation-prompt.txt similarity index 100% rename from examples/checkout/tutorial-generation-prompt.txt rename to examples/checkout/_prompts/tutorial-generation-prompt.txt diff --git a/examples/contracts/example-app-1-create-app-template.txt b/examples/contracts/_prompts/example-app-1-create-app-template.txt similarity index 100% rename from examples/contracts/example-app-1-create-app-template.txt rename to examples/contracts/_prompts/example-app-1-create-app-template.txt diff --git a/examples/contracts/example-app-2-add-feature.txt b/examples/contracts/_prompts/example-app-2-add-feature.txt similarity index 100% rename from examples/contracts/example-app-2-add-feature.txt rename to examples/contracts/_prompts/example-app-2-add-feature.txt diff --git a/examples/contracts/example-app-3-fix-ui.txt b/examples/contracts/_prompts/example-app-3-fix-ui.txt similarity index 100% rename from examples/contracts/example-app-3-fix-ui.txt rename to examples/contracts/_prompts/example-app-3-fix-ui.txt diff --git a/examples/contracts/example-app-4-testing.txt b/examples/contracts/_prompts/example-app-4-testing.txt similarity index 100% rename from examples/contracts/example-app-4-testing.txt rename to examples/contracts/_prompts/example-app-4-testing.txt diff --git a/examples/contracts/tutorial-generation-prompt.txt b/examples/contracts/_prompts/tutorial-generation-prompt.txt similarity index 100% rename from examples/contracts/tutorial-generation-prompt.txt rename to examples/contracts/_prompts/tutorial-generation-prompt.txt diff --git a/examples/orderbook/example-app-1-create-app-template.txt b/examples/orderbook/_prompts/example-app-1-create-app-template.txt similarity index 100% rename from examples/orderbook/example-app-1-create-app-template.txt rename to examples/orderbook/_prompts/example-app-1-create-app-template.txt diff --git a/examples/orderbook/example-app-2-add-feature.txt b/examples/orderbook/_prompts/example-app-2-add-feature.txt similarity index 100% rename from examples/orderbook/example-app-2-add-feature.txt rename to examples/orderbook/_prompts/example-app-2-add-feature.txt diff --git a/examples/orderbook/example-app-3-fix-ui.txt b/examples/orderbook/_prompts/example-app-3-fix-ui.txt similarity index 100% rename from examples/orderbook/example-app-3-fix-ui.txt rename to examples/orderbook/_prompts/example-app-3-fix-ui.txt diff --git a/examples/orderbook/example-app-4-testing.txt b/examples/orderbook/_prompts/example-app-4-testing.txt similarity index 100% rename from examples/orderbook/example-app-4-testing.txt rename to examples/orderbook/_prompts/example-app-4-testing.txt diff --git a/examples/orderbook/tutorial-generation-prompt.txt b/examples/orderbook/_prompts/tutorial-generation-prompt.txt similarity index 100% rename from examples/orderbook/tutorial-generation-prompt.txt rename to examples/orderbook/_prompts/tutorial-generation-prompt.txt diff --git a/examples/passport/example-app-1-create-app-template.txt b/examples/passport/_prompts/example-app-1-create-app-template.txt similarity index 100% rename from examples/passport/example-app-1-create-app-template.txt rename to examples/passport/_prompts/example-app-1-create-app-template.txt diff --git a/examples/passport/example-app-2-add-feature.txt b/examples/passport/_prompts/example-app-2-add-feature.txt similarity index 100% rename from examples/passport/example-app-2-add-feature.txt rename to examples/passport/_prompts/example-app-2-add-feature.txt diff --git a/examples/passport/example-app-3-fix-ui.txt b/examples/passport/_prompts/example-app-3-fix-ui.txt similarity index 100% rename from examples/passport/example-app-3-fix-ui.txt rename to examples/passport/_prompts/example-app-3-fix-ui.txt diff --git a/examples/passport/example-app-4-testing.txt b/examples/passport/_prompts/example-app-4-testing.txt similarity index 100% rename from examples/passport/example-app-4-testing.txt rename to examples/passport/_prompts/example-app-4-testing.txt diff --git a/examples/passport/tutorial-generation-prompt.txt b/examples/passport/_prompts/tutorial-generation-prompt.txt similarity index 100% rename from examples/passport/tutorial-generation-prompt.txt rename to examples/passport/_prompts/tutorial-generation-prompt.txt