diff --git a/backend/cmd/server/bootstrap/01-default-resources.yaml b/backend/cmd/server/bootstrap/01-default-resources.yaml index 42aa640666..af7134e1ff 100644 --- a/backend/cmd/server/bootstrap/01-default-resources.yaml +++ b/backend/cmd/server/bootstrap/01-default-resources.yaml @@ -3,7 +3,7 @@ id: 01900000-0000-7000-8000-000000000001 handle: default name: Default description: Default organization unit -logoUrl: "avatar:shape=rounded,variant=anonymous_entity,content=pavilion,colors=0,bg=#2c4ed4" +logoUrl: "avatar:shape=rounded,variant=anonymous_entity,content=pavilion,colors=0" --- resource_type: user_type id: 01900000-0000-7000-8000-000000000010 @@ -4900,7 +4900,7 @@ description: Management application for ThunderID type: browser ouId: 01900000-0000-7000-8000-000000000001 url: "{{ .PUBLIC_URL }}/console" -logoUrl: "avatar:shape=rounded,variant=anonymous_entity,content=cube,colors=0,bg=#64be90" +logoUrl: "avatar:shape=rounded,variant=anonymous_entity,content=cube,colors=0" authFlowId: 01900000-0000-7000-8000-000000000068 registrationFlowId: 01900000-0000-7000-8000-000000000069 isRegistrationFlowEnabled: false diff --git a/docs/.gitignore b/docs/.gitignore index 1f8041536f..5b8e15a1f4 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -13,6 +13,7 @@ static/api/next/combined.yaml static/api/next/postman/ static/data/releases.json static/data/sdk-releases.json +static/docs/ # Misc .DS_Store diff --git a/docs/content/getting-started/connect-your-application/android.mdx b/docs/content/getting-started/connect-your-application/android.mdx index d269027256..3fcc7fab5d 100644 --- a/docs/content/getting-started/connect-your-application/android.mdx +++ b/docs/content/getting-started/connect-your-application/android.mdx @@ -64,7 +64,11 @@ Once it's running, the console is available at [https://localhost:8090/console]( 3. Under **Application Type**, select **Mobile App**. 4. Enter a name (e.g. `My Android App`) and create an application. The rest of the settings can stay at their defaults. -5. Copy the **Client ID** from the **General** tab. You'll add the redirect URI in the next step after configuring your URL scheme. +5. Copy the **Application ID** from the **General** tab, under **Quick Copy**. + +:::info App-Native Authentication +This quickstart uses app-native authentication through the Flow Execution API. The Android SDK renders the sign-in and sign-up forms natively in your app, so you only need the **Application ID**, no OAuth 2.0 Client ID and no redirect URI. +::: ## Create an Android App @@ -102,48 +106,6 @@ implementation 'dev.thunderid:compose:0.1.0' ``` ::: -## Configure a Callback URL Scheme - - redirects back to your app after sign-in and sign-out using a custom URL scheme. You need to register this scheme in two places. - -**1. Register the scheme in your app** - -Open `AndroidManifest.xml` and add an intent filter with your scheme to your launcher activity. Use `singleTask` launch mode so the redirect reuses the existing activity instance instead of creating a new one. - -```xml title="AndroidManifest.xml" showLineNumbers - - - - - - - - - - - - - - -``` - -**2. Register the redirect URI in the console** - -In the console, open your registered application and add the following as an **Allowed Redirect URI**: - -```text -dev.thunderid.quickstart://callback -``` - -Add the same value as an **Allowed Post-Logout Redirect URI**: - -```text -dev.thunderid.quickstart://logout -``` - ## Initialize the SDK Open your `MainActivity` and wrap the content you pass to `setContent` with the `ThunderIDProvider` composable. This provides `ThunderIDState` to all child composables through `LocalThunderID`. @@ -167,10 +129,7 @@ class MainActivity : ComponentActivity() { ThunderIDProvider( config = ThunderIDConfig( baseUrl = "https://localhost:8090", - clientId = "", scopes = listOf("openid", "profile", "email"), - afterSignInUrl = "dev.thunderid.quickstart://callback", - afterSignOutUrl = "dev.thunderid.quickstart://logout", applicationId = "" ) ) { @@ -183,7 +142,7 @@ class MainActivity : ComponentActivity() { ``` :::warning Configuration -Replace `` with the **Client ID** and `` with the **Application ID** from your application settings. +Replace `` with the **Application ID** from your application settings. ::: ### Configuration Parameters @@ -191,11 +150,8 @@ Replace `` with the **Client ID** and `` wi | Parameter | Description | |-----------|-------------| | `baseUrl` | Your instance URL. Must use HTTPS. | -| `clientId` | The Client ID from your application | | `scopes` | OAuth 2.0 scopes to request. Include `"openid"` at minimum. | -| `afterSignInUrl` | The redirect URI to return to after sign-in | -| `afterSignOutUrl` | The redirect URI to return to after sign-out | -| `applicationId` | The Application ID used for embedded (app-native) sign-in flows | +| `applicationId` | The Application ID used for the app-native sign-in and sign-up flows | ## Add Sign-In and Sign-Out diff --git a/docs/content/getting-started/connect-your-application/flutter.mdx b/docs/content/getting-started/connect-your-application/flutter.mdx index 162fee924a..590ece4a16 100644 --- a/docs/content/getting-started/connect-your-application/flutter.mdx +++ b/docs/content/getting-started/connect-your-application/flutter.mdx @@ -65,7 +65,11 @@ Once it's running, the console is available at [https://localhost:8090/console]( 3. Under **Application Type**, select **Mobile App**. 4. Enter a name (e.g. `My Flutter App`) and create an application. The rest of the settings can stay at their defaults. -5. Copy the **Client ID** from the **General** tab. You'll add the redirect URI in the next step after configuring your URL scheme. +5. Copy the **Application ID** from the **General** tab, under **Quick Copy**. + +:::info App-Native Authentication +This quickstart uses app-native authentication through the Flow Execution API. The Flutter SDK renders the sign-in and sign-up forms natively in your app, so you only need the **Application ID**, no OAuth 2.0 Client ID and no redirect URI. +::: ## Create a Flutter App @@ -95,55 +99,6 @@ Then install it: flutter pub get ``` -## Configure a Callback URL Scheme - - redirects back to your app after sign-in and sign-out using a custom URL scheme. You need to register this scheme on both platforms. - -### iOS - -Open `ios/Runner/Info.plist` and add a URL scheme under **URL Types**: - -```xml title="ios/Runner/Info.plist" -CFBundleURLTypes - - - CFBundleURLSchemes - - dev.thunderid.app - - - -``` - -### Android - -Open `android/app/src/main/AndroidManifest.xml` and add an intent filter inside your `` tag: - -```xml title="android/app/src/main/AndroidManifest.xml" - - - - - - -``` - -### Register the URIs in the console - -In the console, open your registered application and add the following as an **Allowed Redirect URI**: - -```text -dev.thunderid.app://callback -``` - -Add the same value as an **Allowed Post-Logout Redirect URI**: - -```text -dev.thunderid.app://logout -``` - ## Initialize the SDK Wrap your root widget with `ThunderIDProvider` in `lib/main.dart`: @@ -157,10 +112,7 @@ void main() { ThunderIDProvider( config: ThunderIDConfig( baseUrl: 'https://localhost:8090', - clientId: '', scopes: const ['openid', 'profile', 'email'], - afterSignInUrl: 'dev.thunderid.app://callback', - afterSignOutUrl: 'dev.thunderid.app://logout', applicationId: '', ), child: const MyApp(), @@ -181,7 +133,7 @@ class MyApp extends StatelessWidget { ``` :::warning Configuration -Replace `` with the **Client ID** and `` with the **Application ID** from your application settings. +Replace `` with the **Application ID** from your application settings. ::: ### Configuration Parameters @@ -189,11 +141,8 @@ Replace `` with the **Client ID** and `` wi | Parameter | Description | |-----------|-------------| | `baseUrl` | Your instance URL. Must use HTTPS. | -| `clientId` | The Client ID from your application | | `scopes` | OAuth 2.0 scopes to request. Include `'openid'` at minimum. | -| `afterSignInUrl` | The redirect URI to return to after sign-in | -| `afterSignOutUrl` | The redirect URI to return to after sign-out | -| `applicationId` | The Application ID used for embedded (app-native) sign-in flows | +| `applicationId` | The Application ID used for the app-native sign-in and sign-up flows | ## Add Sign-In and Sign-Out diff --git a/docs/content/getting-started/connect-your-application/ios.mdx b/docs/content/getting-started/connect-your-application/ios.mdx index 8518cc6925..29d9f15e4a 100644 --- a/docs/content/getting-started/connect-your-application/ios.mdx +++ b/docs/content/getting-started/connect-your-application/ios.mdx @@ -65,7 +65,11 @@ Once it's running, the console is available at [https://localhost:8090/console]( 3. Under **Application Type**, select **Mobile App**. 4. Enter a name (e.g. `My iOS App`) and create an application. The rest of the settings can stay at their defaults. -5. Copy the **Client ID** from the **General** tab. You'll add the redirect URI in the next step after configuring your URL scheme. +5. Copy the **Application ID** from the **General** tab, under **Quick Copy**. + +:::info App-Native Authentication +This quickstart uses app-native authentication through the Flow Execution API. The iOS SDK renders the sign-in and sign-up forms natively in your app, so you only need the **Application ID**, no OAuth 2.0 Client ID and no redirect URI. +::: ## Create an iOS App @@ -93,40 +97,6 @@ pod 'ThunderIDSwiftUI' ``` ::: -## Configure a Callback URL Scheme - - redirects back to your app after sign-in and sign-out using a custom URL scheme. You need to register this scheme in two places. - -**1. Register the scheme in your app** - -Open your app's `Info.plist` and add a URL scheme under **URL Types**. For example: - -```xml -CFBundleURLTypes - - - CFBundleURLSchemes - - io.thunderid.b2c - - - -``` - -**2. Register the redirect URI in the console** - -In the console, open your registered application and add the following as an **Allowed Redirect URI**: - -```text -io.thunderid.b2c://callback -``` - -Add the same value as an **Allowed Post-Logout Redirect URI**: - -```text -io.thunderid.b2c://logout -``` - ## Initialize the SDK Open your app's entry point (the file that conforms to `App`) and apply the `.thunderIDProvider(config:)` modifier to your root view. This injects a `ThunderIDState` environment object into all child views. @@ -142,10 +112,7 @@ struct MyApp: App { ContentView() .thunderIDProvider(config: ThunderIDConfig( baseUrl: "https://localhost:8090", - clientId: "", scopes: ["openid", "profile", "email"], - afterSignInUrl: "io.thunderid.b2c://callback", - afterSignOutUrl: "io.thunderid.b2c://logout", applicationId: "" )) } @@ -154,7 +121,7 @@ struct MyApp: App { ``` :::warning Configuration -Replace `` with the **Client ID** and `` with the **Application ID** from your application settings. +Replace `` with the **Application ID** from your application settings. ::: ### Configuration Parameters @@ -162,11 +129,8 @@ Replace `` with the **Client ID** and `` wi | Parameter | Description | |-----------|-------------| | `baseUrl` | Your instance URL. Must use HTTPS. | -| `clientId` | The Client ID from your application | | `scopes` | OAuth 2.0 scopes to request. Include `"openid"` at minimum. | -| `afterSignInUrl` | The redirect URI to return to after sign-in | -| `afterSignOutUrl` | The redirect URI to return to after sign-out | -| `applicationId` | The Application ID used for embedded (app-native) sign-in flows | +| `applicationId` | The Application ID used for the app-native sign-in and sign-up flows | ## Add Sign-In and Sign-Out diff --git a/docs/content/getting-started/connect-your-application/node.mdx b/docs/content/getting-started/connect-your-application/node.mdx index d7100d7911..0875c9a4b9 100644 --- a/docs/content/getting-started/connect-your-application/node.mdx +++ b/docs/content/getting-started/connect-your-application/node.mdx @@ -4,7 +4,7 @@ title: Node.js Quickstart docType: quickstart sidebar_position: 5 persona: app -description: Add {{ProductName}} authentication to a vanilla Node.js application using the @thunderid/node SDK. +description: Authenticate a Node.js service to {{ProductName}} as itself using the OAuth 2.0 client_credentials grant with the @thunderid/node SDK. --- import { @@ -20,7 +20,7 @@ import { # Node.js Quickstart -Use this guide to add authentication to a vanilla Node.js application using the `@thunderid/node` SDK and the built-in `http` module. No framework required. +Use this guide to authenticate a Node.js service to using the `@thunderid/node` SDK. Unlike the other quickstarts in this section, there's no user and no browser sign-in: the service authenticates as **itself** with the OAuth 2.0 `client_credentials` grant, then uses the resulting access token to call another piece of business logic. Use this pattern for background jobs, cron tasks, or one backend service calling another, where there's no human in the loop to redirect through a login page. @@ -28,18 +28,22 @@ Use this guide to add authentication to a vanilla Node.js applic }>Create a Node.js project }>Install the @thunderid/node package -}>Add working sign-in and sign-out routes -}>Protect routes and display the signed-in user's profile +}>Authenticate a service with the client_credentials grant +}>Use the resulting access token to call business logic ## Prerequisites -}>About 15 minutes +}>About 10 minutes }>Node.js 18+ installed on your system }>npm, yarn, or pnpm }>Your preferred code editor +:::tip Example Source Code +Check out the complete Node.js Quickstart Sample in the repository. +::: + ## Run @@ -50,7 +54,9 @@ Start a local instance. Pick the method that works best for you: Once it's running, the console is available at [https://localhost:8090/console](https://localhost:8090/console). -## Create an Application +## Create an Agent + +Agents are 's machine identities, distinct from user-facing applications. A background service like this one authenticates as an agent, not as an application. 1. Sign in to the Console. @@ -58,12 +64,11 @@ Once it's running, the console is available at [https://localhost:8090/console]( If you used the default setup, sign in to the Console as `admin` with the password generated during setup and printed to the setup output (unless you supplied your own). ::: -2. Navigate to **Applications**, and click **Add Application**. - -3. Under **Technology**, select **Node.js**. -4. Enter a name (e.g. `My Node.js App`) and create an application. The rest of the settings can stay at their defaults. -5. Copy both the **Client ID** and **Client Secret** from the window that pops up. The Client ID can also be found in the **General** tab. -6. Under **General**, add `http://localhost:3000/callback` to the list of **Authorized Redirect URIs**. +2. Navigate to **Agents** and click **Add Agent**. +3. Enter an **Agent name** (e.g. `node-service-quickstart`) and select an **Owner**, then click **Create agent**. +4. displays the agent's **Client Secret** once. Copy it now; it cannot be retrieved again. +5. Open the agent's **Advanced Settings** tab, enable the `client_credentials` grant type, and set the **Client authentication method** to `client_secret_basic`. +6. Copy the **Client ID** from the **General** tab. ## Create a Node.js Project @@ -71,20 +76,20 @@ Initialize a new Node.js project: - mkdir my-node-app - cd my-node-app + mkdir my-node-service + cd my-node-service npm init -y - mkdir my-node-app - cd my-node-app + mkdir my-node-service + cd my-node-service yarn init -y - mkdir my-node-app - cd my-node-app + mkdir my-node-service + cd my-node-service pnpm init @@ -107,309 +112,92 @@ Install the Node.js SDK: -## Initialize the Client +## Authenticate as the Service -Create an `index.js` file and initialize the `ThunderIDNodeClient` with your application credentials: +Create an `index.mjs` file and initialize `ThunderIDNodeClient` with `grantType: 'client_credentials'`. With that set, `getAccessToken()` authenticates as the service itself, no session and no sign-in, and transparently fetches, caches, and refreshes the token. -```js title="index.js" showLineNumbers -const http = require('http'); -const { URL } = require('url'); -const { randomUUID } = require('crypto'); -const { ThunderIDNodeClient } = require('@thunderid/node'); +```js title="index.mjs" showLineNumbers +import { ThunderIDNodeClient } from '@thunderid/node'; -const PORT = 3000; -const SESSION_COOKIE = 'tid_session'; +const client = new ThunderIDNodeClient(); -const auth = new ThunderIDNodeClient(); +await client.initialize({ + baseUrl: 'https://localhost:8090', + clientId: '', + clientSecret: '', + grantType: 'client_credentials', +}); -function getSessionId(req) { - const cookieHeader = req.headers.cookie ?? ''; - for (const part of cookieHeader.split(';')) { - const [name, value] = part.trim().split('='); - if (name === SESSION_COOKIE) return decodeURIComponent(value); - } - return null; -} - -async function main() { - await auth.initialize({ - clientId: '', - clientSecret: '', - baseUrl: 'https://localhost:8090', - afterSignInUrl: 'http://localhost:3000/callback', - afterSignOutUrl: 'http://localhost:3000', - }); - - const server = http.createServer(async (req, res) => { - // routes added in the next step - }); - - server.listen(PORT, () => { - console.log(`Server running on http://localhost:${PORT}`); - }); -} +const accessToken = await client.getAccessToken(); +const { scope } = await client.decodeJwtToken(accessToken); -main(); +console.log(`Authenticated with scope: ${scope}`); ``` :::warning Configuration -Replace `` and `` with the values from your application. Set the authorized redirect URL in your application settings to `http://localhost:3000/callback`. +Replace `` and `` with the **Client ID** and **Client Secret** from your agent. ::: ### Configuration Parameters | Parameter | Description | |-----------|-------------| -| `clientId` | The Client ID from your application | -| `clientSecret` | The Client Secret from your application | | `baseUrl` | Your instance URL (e.g., `https://localhost:8090`) | -| `afterSignInUrl` | The callback URL redirects to after sign-in | -| `afterSignOutUrl` | The URL to redirect to after sign-out | - -## Add Sign-In and Sign-Out Routes - -The `signIn` method works in two phases: it first redirects the user to , then handles the authorization code on the callback. Session state is tied to a session ID stored in a cookie. - -Replace the `// routes added in the next step` comment with: - -```js title="index.js" showLineNumbers - const url = new URL(req.url, `http://localhost:${PORT}`); - - try { - if (url.pathname === '/login') { - let sessionId = getSessionId(req); - const extraHeaders = {}; - if (!sessionId) { - sessionId = randomUUID(); - extraHeaders['Set-Cookie'] = - `${SESSION_COOKIE}=${sessionId}; HttpOnly; SameSite=Lax; Path=/`; - } - await auth.signIn((authUrl) => { - res.writeHead(302, { ...extraHeaders, Location: authUrl }); - res.end(); - }, sessionId); - - } else if (url.pathname === '/callback') { - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - const sessionState = url.searchParams.get('session_state'); - const sessionId = getSessionId(req); - - if (!sessionId || !code || !state) { - res.writeHead(400); - return res.end('Bad request'); - } - - await auth.signIn(() => {}, sessionId, code, sessionState, state); - res.writeHead(302, { Location: '/profile' }); - res.end(); - - } else if (url.pathname === '/logout') { - const sessionId = getSessionId(req); - if (!sessionId) { - res.writeHead(302, { Location: '/' }); - return res.end(); - } - const signOutUrl = await auth.signOut(sessionId); - res.writeHead(302, { - Location: signOutUrl, - 'Set-Cookie': `${SESSION_COOKIE}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`, - }); - res.end(); - - } - } catch { - res.writeHead(500); - res.end('Internal server error'); - } -``` +| `clientId` | The Client ID from your agent | +| `clientSecret` | The Client Secret from your agent | +| `grantType` | Set to `'client_credentials'` to authenticate the service as itself, with no user and no browser redirect | -**How the sign-in flow works:** - -1. `GET /login`: generates a session ID, stores it in a cookie, and calls `signIn` with an `authUrlCallback`. The callback receives the authorization URL and redirects the user's browser there. -2. `GET /callback`: redirects back with `code` and `state` query parameters. Calling `signIn` again with those values exchanges the code for tokens and stores the session. -3. `GET /logout`: calls `signOut` to get the OIDC end-session URL, clears the local cookie, then redirects the browser to complete the logout at . - -## Protect a Route and Display User Info - -Use `isSignedIn` to guard routes and `getUser` to retrieve the authenticated user's profile. Add these inside the same `try` block, before the closing `}`: - -```js title="index.js" showLineNumbers - if (url.pathname === '/') { - const sessionId = getSessionId(req); - const signedIn = sessionId && (await auth.isSignedIn(sessionId)); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(signedIn - ? 'View profile | Sign out' - : 'Sign in' - ); - - } else if (url.pathname === '/profile') { - const sessionId = getSessionId(req); - if (!sessionId || !(await auth.isSignedIn(sessionId))) { - res.writeHead(302, { Location: '/login' }); - return res.end(); - } - const user = await auth.getUser(sessionId); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(` -

Welcome, ${user.name || user.username}!

-

Email: ${user.email}

-

First name: ${user.given_name}

-

Last name: ${user.family_name}

- Sign out - `); - - } else if (url.pathname === '/login') { -``` +## Call Business Logic With the Token -## Complete index.js +A real backend keeps authentication in its own module instead of scattering it across business logic. Update `index.mjs` so the rest of the app asks for a token only when it needs one, instead of handling `getAccessToken()` and the `Authorization` header directly: -Here is the full file for reference: +```js title="index.mjs" showLineNumbers +import { ThunderIDNodeClient } from '@thunderid/node'; -```js title="index.js" showLineNumbers -const http = require('http'); -const { URL } = require('url'); -const { randomUUID } = require('crypto'); -const { ThunderIDNodeClient } = require('@thunderid/node'); +const client = new ThunderIDNodeClient(); -const PORT = 3000; -const SESSION_COOKIE = 'tid_session'; +await client.initialize({ + baseUrl: 'https://localhost:8090', + clientId: '', + clientSecret: '', + grantType: 'client_credentials', +}); -const auth = new ThunderIDNodeClient(); +async function getStock(sku) { + const accessToken = await client.getAccessToken(); -function getSessionId(req) { - const cookieHeader = req.headers.cookie ?? ''; - for (const part of cookieHeader.split(';')) { - const [name, value] = part.trim().split('='); - if (name === SESSION_COOKIE) return decodeURIComponent(value); - } - return null; -} + // A real backend would attach this as `Authorization: Bearer ${accessToken}` + // on a request to a separate inventory service. This quickstart just proves + // the token was obtained before answering. + console.log(`Requesting ${sku} with a valid access token`); -async function main() { - await auth.initialize({ - clientId: '', - clientSecret: '', - baseUrl: 'https://localhost:8090', - afterSignInUrl: 'http://localhost:3000/callback', - afterSignOutUrl: 'http://localhost:3000', - }); - - const server = http.createServer(async (req, res) => { - const url = new URL(req.url, `http://localhost:${PORT}`); - - try { - if (url.pathname === '/') { - const sessionId = getSessionId(req); - const signedIn = sessionId && (await auth.isSignedIn(sessionId)); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(signedIn - ? 'View profile | Sign out' - : 'Sign in' - ); - - } else if (url.pathname === '/profile') { - const sessionId = getSessionId(req); - if (!sessionId || !(await auth.isSignedIn(sessionId))) { - res.writeHead(302, { Location: '/login' }); - return res.end(); - } - const user = await auth.getUser(sessionId); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(` -

Welcome, ${user.name || user.username}!

-

Email: ${user.email}

-

First name: ${user.given_name}

-

Last name: ${user.family_name}

- Sign out - `); - - } else if (url.pathname === '/login') { - let sessionId = getSessionId(req); - const extraHeaders = {}; - if (!sessionId) { - sessionId = randomUUID(); - extraHeaders['Set-Cookie'] = - `${SESSION_COOKIE}=${sessionId}; HttpOnly; SameSite=Lax; Path=/`; - } - await auth.signIn((authUrl) => { - res.writeHead(302, { ...extraHeaders, Location: authUrl }); - res.end(); - }, sessionId); - - } else if (url.pathname === '/callback') { - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - const sessionState = url.searchParams.get('session_state'); - const sessionId = getSessionId(req); - - if (!sessionId || !code || !state) { - res.writeHead(400); - return res.end('Bad request'); - } - - await auth.signIn(() => {}, sessionId, code, sessionState, state); - res.writeHead(302, { Location: '/profile' }); - res.end(); - - } else if (url.pathname === '/logout') { - const sessionId = getSessionId(req); - if (!sessionId) { - res.writeHead(302, { Location: '/' }); - return res.end(); - } - const signOutUrl = await auth.signOut(sessionId); - res.writeHead(302, { - Location: signOutUrl, - 'Set-Cookie': `${SESSION_COOKIE}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`, - }); - res.end(); - - } else { - res.writeHead(404); - res.end('Not found'); - } - } catch { - res.writeHead(500); - res.end('Internal server error'); - } - }); - - server.listen(PORT, () => { - console.log(`Server running on http://localhost:${PORT}`); - }); + return { sku, inStock: true }; } -main(); +const item = await getStock('SKU-100'); +console.log(item); ``` -## Run Your App +## Run Your Service -Start the server: +Start the script: - node index.js + node index.mjs - yarn node index.js + yarn node index.mjs - pnpm node index.js + pnpm node index.mjs -Open [http://localhost:3000](http://localhost:3000). - -:::note Test credentials -You'll need a user to sign in with. If you haven't created one yet, open , navigate to **Users**, and add a test user with an email and password. -::: - :::tip Success -You should see the sign-in link. Click it to be redirected to the -hosted sign-in page. After authenticating with your test user, you'll return to the `/profile` route with your user profile displayed. +The script authenticates once, prints the scope it received, then calls `getStock()` and prints the result before exiting. :::
@@ -418,11 +206,11 @@ You should see the sign-in link. Click it to be redirected to the - +
-
Upgrade the sign-in flow
-
Add MFA, passkeys, or social login to your flow without changing your route wiring.
-
Flow Designer →
+
Agent Authentication
+
Request specific scopes, narrow tokens to a resource server, and troubleshoot the token endpoint directly.
+
Read the Guide →
diff --git a/docs/content/getting-started/connect-your-application/prompts/android/redirect-based.txt b/docs/content/getting-started/connect-your-application/prompts/android/redirect-based.txt new file mode 100644 index 0000000000..2affef1349 --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/android/redirect-based.txt @@ -0,0 +1,36 @@ +# Integrate {{productName}} Authentication in Android Application + +## Context +I have an Android application (Kotlin, Jetpack Compose) and I want to integrate {{productName}}'s authentication system using the {{productName}} Android SDK, with sign-in and sign-up forms rendered natively in the app (app-native authentication). + +## Requirements +- Use the `dev.thunderid:compose` Gradle dependency for authentication +- Use app-native authentication through the Flow Execution API (no OAuth 2.0 client ID, no redirect URI) +- Implement sign-in and sign-up with the prebuilt `SignIn`/`SignUp` composables +- Guard content with the `SignedIn`/`SignedOut` composables +- Display the signed-in user's profile information +- Implement sign-out with the prebuilt `SignOutButton` composable + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: dev.thunderid:compose + +## IMPORTANT Configuration Rules +- Add the {{productName}} Maven repository (`https://maven.thunderid.dev/releases`) to `settings.gradle.kts` before adding the dependency +- Wrap the content passed to `setContent` with the `ThunderIDProvider` composable, configured with `ThunderIDConfig(baseUrl, scopes, applicationId)` +- Access authentication state via `LocalThunderID.current`, not a separate hook or singleton +- Required scopes: at minimum `"openid"` +- Use `applicationId`, NOT a client ID or redirect URI — this SDK only supports app-native authentication + +## Implementation Steps +1. Create a new Android Studio project using the Empty Activity template with Kotlin and Jetpack Compose +2. Add the {{productName}} Maven repository to `settings.gradle.kts` +3. Add the `dev.thunderid:compose` dependency to `build.gradle.kts` +4. Wrap your `setContent` content with `ThunderIDProvider`, configured with `baseUrl`, `scopes`, and `applicationId` +5. Build a root composable that checks `thunder.isInitialized` and renders a `SignedIn`/fallback split +6. Build an auth screen using the `SignIn` composable, passing the `applicationId` +7. Build a home screen that reads `LocalThunderID.current.user` to display the signed-in user's name and email, with a `SignOutButton` +8. Run the app on an API 24+ emulator or device + +Please provide complete, working code with proper configuration for {{productName}} authentication using the {{productName}} Android SDK for Jetpack Compose. diff --git a/docs/content/getting-started/connect-your-application/prompts/browser/embedded.txt b/docs/content/getting-started/connect-your-application/prompts/browser/embedded.txt new file mode 100644 index 0000000000..b3a5777548 --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/browser/embedded.txt @@ -0,0 +1,36 @@ +# Integrate {{productName}} Authentication in Vanilla JavaScript Application (Custom UI Mode) + +## Context +I have a vanilla JavaScript application and I want to integrate {{productName}}'s authentication system using the ThunderID Browser SDK's embedded flow functions, rendering my own sign-in form instead of redirecting to {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/browser SDK for authentication +- Use `executeEmbeddedSignInFlow` (re-exported by @thunderid/browser from the core @thunderid/javascript client) to drive the sign-in flow directly, instead of `ThunderIDBrowserClient.signIn()` (which is redirect-only) +- Render your own sign-in form driven by the flow response's `components` array +- Display signed-in user's profile information +- Handle authentication state + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: @thunderid/browser + +## IMPORTANT Configuration Rules +- `@thunderid/browser` has NO bundled ``-style DOM component — it is framework-agnostic with no rendering layer. Build the form yourself from the flow response +- Call `executeEmbeddedSignInFlow({applicationId, baseUrl, flowType: 'AUTHENTICATION'})` to start the flow +- Each step returns `components` describing the fields to render next (e.g. identifier, password, OTP); render inputs for those fields, then call `executeEmbeddedSignInFlow` again with the same `executionId` and the user's submitted `inputs` to advance the flow +- Repeat until the flow response indicates completion (tokens/session issued) +- NEVER call `ThunderIDBrowserClient.signIn()` or navigate to a {{productName}}-hosted URL in this mode — that redirects the browser instead of using the embedded flow +- Still use `ThunderIDBrowserClient.initialize(config)` for session/token management (`isSignedIn()`, `getUser()`, `getAccessToken()`, `signOut()`) once the embedded flow completes + +## Implementation Steps +1. Create a vanilla JS app using Vite by running: `npm create vite@latest js-demo -- --template vanilla` +2. Navigate into the project directory: `cd js-demo` +3. Install dependencies: `npm install` +4. Install @thunderid/browser package: `npm install @thunderid/browser` +5. Create src/auth.js to initialize ThunderIDBrowserClient with applicationId and baseUrl +6. Create a sign-in form in src/main.js; on submit, call `executeEmbeddedSignInFlow` and render whatever `components` the response specifies for the next step +7. Once the flow completes, use `isSignedIn()`/`getUser()` to show the signed-in profile, and `signOut()` for sign-out +8. Run the development server: `npm run dev` + +Please provide complete, working code for {{productName}} authentication using the ThunderID Browser SDK's embedded flow functions, with a custom sign-in form (not a redirect). diff --git a/docs/content/getting-started/connect-your-application/prompts/browser/redirect-based.txt b/docs/content/getting-started/connect-your-application/prompts/browser/redirect-based.txt new file mode 100644 index 0000000000..f5022077b9 --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/browser/redirect-based.txt @@ -0,0 +1,57 @@ +# Integrate {{productName}} Authentication in Vanilla JavaScript Application + +## Context +I have a vanilla JavaScript application and I want to integrate {{productName}}'s authentication system using the ThunderID Browser SDK with {{productName}}-hosted login pages. + +## Requirements +- Use @thunderid/browser SDK for authentication +- Configure {{productName}}-hosted login pages +- Implement sign-in and sign-out functionality +- Display signed-in user's profile information +- Handle authentication state + +## Configuration +- **Client ID**: {{clientId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: @thunderid/browser + +## IMPORTANT Configuration Rules +- Create a ThunderIDBrowserClient instance and call initialize() with a config object +- Required config properties: `clientId` and `baseUrl` +- Optional config properties: `afterSignInUrl`, `afterSignOutUrl`, `scopes` (string array), `storage` +- Storage options: 'sessionStorage' (default), 'localStorage', 'browserMemory' +- NEVER use property names like `signInRedirectURL` or `signOutRedirectURL` + +## Available SDK APIs + +### ThunderIDBrowserClient +- `initialize(config)` - Initialize the client with configuration +- `signIn()` - Redirect to {{productName}} sign-in page +- `signOut()` - Sign out and clear session +- `isSignedIn()` - Check if user is signed in (returns Promise) +- `getUser()` - Get authenticated user profile (returns Promise) +- `getAccessToken()` - Get current access token +- `getIdToken()` - Get ID token +- `getDecodedIdToken()` - Get decoded ID token claims +- `httpRequest(config)` - Make authenticated HTTP request +- `on(hook, callback)` - Register event callbacks (Hooks.SignIn, Hooks.SignOut, etc.) + +### User Object Properties +- `displayName` - User's display name +- `username` - Username +- `email` - Email address +- `given_name` - First name +- `family_name` - Last name +- `picture` - Profile picture URL + +## Implementation Steps +1. Create a vanilla JS app using Vite by running: `npm create vite@latest js-demo -- --template vanilla` +2. Navigate into the project directory: `cd js-demo` +3. Install dependencies: `npm install` +4. Install @thunderid/browser package: `npm install @thunderid/browser` +5. Create src/auth.js to initialize ThunderIDBrowserClient with clientId and baseUrl +6. Update src/main.js to check isSignedIn(), show sign-in button or user profile +7. Add event listeners for sign-in and sign-out buttons +8. Run the development server: `npm run dev` + +Please provide complete, working code with proper configuration for {{productName}} authentication using the ThunderID Browser SDK. diff --git a/docs/content/getting-started/connect-your-application/prompts/express/embedded.txt b/docs/content/getting-started/connect-your-application/prompts/express/embedded.txt new file mode 100644 index 0000000000..9ca1b63f34 --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/express/embedded.txt @@ -0,0 +1,47 @@ +# Integrate {{productName}} Authentication in an Express Application (Custom UI / Embedded Mode) + +## Context +I have an Express application and I want to integrate {{productName}} authentication using the ThunderID Express SDK in embedded (app-native) mode, rendering my own sign-in form instead of redirecting to {{productName}}-hosted pages. + +## Requirements +- Use `@thunderid/express` in a Node.js + Express application +- Use `cookie-parser` and `express.json()` middleware +- Configure ThunderID middleware with `baseUrl`, `applicationId`, and `mode: 'embedded'` +- Add a `/flow/sign-in` route using `handleFlow()` that proxies each step of the flow to the Flow Execution API +- Serve a `/login` page with a plain HTML form that POSTs to `/flow/sign-in` and re-renders whatever fields the flow response asks for next +- Add `/logout` with `handleSignOut()` +- Protect a route (`/protected`) using `protect((res) => res.redirect('/login'))` +- Add a `/me` route that returns the authenticated user profile as JSON +- Keep the code minimal, production-lean, and fully runnable + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **App URL**: http://localhost:3000 +- **Sign-In Page**: `/login` +- **Flow Route**: `/flow/sign-in` +- **Logout Callback Route**: `/logout` +- **SDK**: @thunderid/express + +## Important Rules +- Use CommonJS syntax (`require`) in `index.js` +- `@thunderid/express` has NO bundled sign-in UI component — this is a server-side SDK. `mode: 'embedded'` only changes what `handleFlow()` proxies to; you must write the actual HTML/JS form yourself +- Set `mode: 'embedded'` in the `thunderID({...})` middleware config (defaults to `'redirect'` if omitted) +- `handleFlow()` calls the Flow Execution API on each POST and returns the flow's JSON step (`executionId`, `challengeToken`, `authId`, `components`) — render whatever `components` the response contains as form fields, and POST the user's input back to the same route with the returned `executionId` +- Do not invent unsupported SDK APIs or custom wrappers +- Do not use `clientId`/`clientSecret` in this mode — embedded mode is driven by `applicationId`, not an OAuth2 client + +## Implementation Steps +1. Create a new project and install `express` and `cookie-parser` +2. Install `@thunderid/express` +3. Add `index.js` with `thunderID({ applicationId, baseUrl, mode: 'embedded' })` middleware +4. Add `/login` route serving a minimal HTML form, and `/flow/sign-in` using `handleFlow()` +5. Add `/logout`, `/protected`, and `/me` routes +6. Start the server with `node index.js` +7. Validate the flow by opening `/login`, submitting the form, then `/protected` and `/me` + +Please provide: +- The exact terminal commands +- A complete `index.js` file +- A minimal HTML sign-in form driven by the flow's `components` response (not a redirect) +- A short verification checklist for sign-in, sign-out, and protected route behavior diff --git a/docs/content/getting-started/connect-your-application/prompts/express/redirect-based.txt b/docs/content/getting-started/connect-your-application/prompts/express/redirect-based.txt new file mode 100644 index 0000000000..e2aeaea4d6 --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/express/redirect-based.txt @@ -0,0 +1,42 @@ +# Integrate {{productName}} Authentication in an Express Application (Inbuilt Mode) + +## Context +I have an Express application and I want to integrate {{productName}} authentication using the ThunderID Express SDK with {{productName}}-hosted sign-in pages. + +## Requirements +- Use `@thunderid/express` in a Node.js + Express application +- Use `cookie-parser` and `express.json()` middleware +- Configure ThunderID middleware with `baseUrl`, `clientId`, `clientSecret`, `afterSignInUrl`, and `afterSignOutUrl` +- Implement `/login` with `handleSignIn()` and `/logout` with `handleSignOut()` +- Protect a route (`/protected`) using `protect((res) => res.redirect('/login'))` +- Add a `/me` route that returns the authenticated user profile as JSON +- Keep the code minimal, production-lean, and fully runnable + +## Configuration +- **Client ID**: {{clientId}} +- **Client Secret**: `` +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **App URL**: http://localhost:3000 +- **Login Callback Route**: `/login` +- **Logout Callback Route**: `/logout` +- **SDK**: @thunderid/express + +## Important Rules +- Use CommonJS syntax (`require`) in `index.js` +- Use these SDK imports exactly: `thunderID`, `handleSignIn`, `handleSignOut`, `protect` +- Ensure redirect URL alignment: the app callback URL must be `http://localhost:3000/login`, and the post-logout redirect URL must be `http://localhost:3000/logout` +- Do not invent unsupported SDK APIs or custom wrappers +- Keep route names and behavior exactly as specified + +## Implementation Steps +1. Create a new project and install `express` and `cookie-parser` +2. Install `@thunderid/express` +3. Add `index.js` with ThunderID middleware and auth routes +4. Add `/protected` and `/me` routes with `protect()` +5. Start the server with `node index.js` +6. Validate the flow by opening `/protected`, then `/me` + +Please provide: +- The exact terminal commands +- A complete `index.js` file +- A short verification checklist for sign-in, sign-out, and protected route behavior diff --git a/docs/content/getting-started/connect-your-application/prompts/flutter/redirect-based.txt b/docs/content/getting-started/connect-your-application/prompts/flutter/redirect-based.txt new file mode 100644 index 0000000000..bb4da78d3e --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/flutter/redirect-based.txt @@ -0,0 +1,34 @@ +# Integrate {{productName}} Authentication in Flutter Application + +## Context +I have a Flutter application (Dart) and I want to integrate {{productName}}'s authentication system using the `thunderid_flutter` package, with sign-in and sign-up forms rendered natively in the app (app-native authentication). + +## Requirements +- Use the `thunderid_flutter` package for authentication +- Use app-native authentication through the Flow Execution API (no OAuth 2.0 client ID, no redirect URI) +- Implement sign-in and sign-up with the prebuilt `SignIn`/`SignUp` widgets +- Route between an auth screen and a home screen based on `thunder.isSignedIn` +- Display the signed-in user's profile information +- Implement sign-out with the prebuilt `SignOutButton` widget + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: thunderid_flutter + +## IMPORTANT Configuration Rules +- Wrap your root widget with `ThunderIDProvider`, configured with `ThunderIDConfig(baseUrl, scopes, applicationId)` +- Access authentication state via `ThunderIDProvider.of(context)`, not a separate hook or singleton +- Required scopes: at minimum `'openid'` +- Use `applicationId`, NOT a client ID or redirect URI — this SDK only supports app-native authentication + +## Implementation Steps +1. Create a new Flutter project: `flutter create my_app` +2. Add `thunderid_flutter` to `pubspec.yaml` and run `flutter pub get` +3. Wrap your root widget with `ThunderIDProvider` in `lib/main.dart`, configured with `baseUrl`, `scopes`, and `applicationId` +4. Build a root screen that checks `thunder.initialized`/`thunder.isLoading` and routes to an auth or home screen based on `thunder.isSignedIn` +5. Build an auth screen using the `SignIn`/`SignUp` widgets, passing the `applicationId` +6. Build a home screen that reads `thunder.user` to display the signed-in user's name and email, with a `SignOutButton` +7. Run the app on an iOS simulator or Android device: `flutter run` + +Please provide complete, working code with proper configuration for {{productName}} authentication using the `thunderid_flutter` package. diff --git a/docs/content/getting-started/connect-your-application/prompts/ios/redirect-based.txt b/docs/content/getting-started/connect-your-application/prompts/ios/redirect-based.txt new file mode 100644 index 0000000000..755f828913 --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/ios/redirect-based.txt @@ -0,0 +1,34 @@ +# Integrate {{productName}} Authentication in iOS Application + +## Context +I have an iOS application (Swift, SwiftUI) and I want to integrate {{productName}}'s authentication system using the {{productName}} iOS SDK, with sign-in and sign-up forms rendered natively in the app (app-native authentication). + +## Requirements +- Use the `ThunderIDSwiftUI` Swift package for authentication +- Use app-native authentication through the Flow Execution API (no OAuth 2.0 client ID, no redirect URI) +- Implement sign-in and sign-up with the prebuilt `SignIn`/`SignUp` views +- Guard content with the `SignedIn`/`SignedOut` views +- Display the signed-in user's profile information +- Implement sign-out with the prebuilt `SignOutButton` view + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: ThunderIDSwiftUI (Swift Package Manager) or ThunderIDSwiftUI CocoaPod + +## IMPORTANT Configuration Rules +- Apply the `.thunderIDProvider(config:)` modifier to the root view in your app's entry point (the type conforming to `App`), configured with `ThunderIDConfig(baseUrl, scopes, applicationId)` +- Access authentication state via an injected `ThunderIDState` `@EnvironmentObject`, not a separate hook or singleton +- Required scopes: at minimum `"openid"` +- Use `applicationId`, NOT a client ID or redirect URI — this SDK only supports app-native authentication + +## Implementation Steps +1. Create a new Xcode project using the iOS > App template with SwiftUI and Swift +2. Add the `ThunderIDSwiftUI` package via File > Add Package Dependencies (or the `ThunderIDSwiftUI` CocoaPod) +3. Apply `.thunderIDProvider(config:)` to your root view, configured with `baseUrl`, `scopes`, and `applicationId` +4. Build a root view that checks `state.isInitialized` and renders a `SignedIn`/fallback split +5. Build an auth view using the `SignIn` view, passing the `applicationId` +6. Build a home view that reads `state.user` to display the signed-in user's name and email, with a `SignOutButton` +7. Run the app on an iOS 16+ simulator or device + +Please provide complete, working code with proper configuration for {{productName}} authentication using the {{productName}} iOS SDK for SwiftUI. diff --git a/docs/content/getting-started/connect-your-application/prompts/nextjs/embedded.txt b/docs/content/getting-started/connect-your-application/prompts/nextjs/embedded.txt new file mode 100644 index 0000000000..cb7860e1cc --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/nextjs/embedded.txt @@ -0,0 +1,41 @@ +# Integrate {{productName}} Authentication in Next.js Application (Custom UI Mode) + +## Context +I have a Next.js application (App Router) and I want to integrate {{productName}}'s authentication system using the {{productName}} Next.js SDK with a custom sign-in UI instead of {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/nextjs SDK for authentication +- Render sign-in on a custom route (not a redirect to {{productName}}-hosted pages) +- Use the App Router (not Pages Router) +- Implement sign-in and sign-out with the SDK's own components +- Add middleware for route protection and automatic token refresh +- Display signed-in user's profile information + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **Sign In Route**: /signin +- **SDK**: @thunderid/nextjs + +## IMPORTANT Configuration Rules +- Use environment variables for configuration (NOT props on the provider) +- Required env vars: NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_APPLICATION_ID, THUNDERID_SECRET, NODE_TLS_REJECT_UNAUTHORIZED=0 +- Import ThunderIDProvider from '@thunderid/nextjs/server' (NOT from '@thunderid/nextjs') +- Import middleware utilities from '@thunderid/nextjs/server' +- Import UI components (`SignedIn`, `SignedOut`, `SignIn`, `SignOutButton`, `UserDropdown`) from '@thunderid/nextjs' +- The `` component drives the Flow Execution API directly (not an OAuth redirect) — render it on your custom `app/signin/page.tsx` +- `` accepts an `onSuccess` callback (e.g. `router.push('/')`) and an `onError` callback +- NEVER redirect to a {{productName}}-hosted URL for sign-in in this mode + +## Implementation Steps +1. Create a Next.js app: npx create-next-app@latest nextjs-demo +2. Navigate into the project: cd nextjs-demo +3. Install @thunderid/nextjs: npm install @thunderid/nextjs +4. Create .env.local with NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_APPLICATION_ID, THUNDERID_SECRET, NODE_TLS_REJECT_UNAUTHORIZED=0 +5. Wrap root layout with from '@thunderid/nextjs/server' +6. Create app/signin/page.tsx rendering ` router.push('/')} />` and `` for the already-authenticated case +7. Create proxy.ts with thunderIDProxy and createRouteMatcher from '@thunderid/nextjs/server' for route protection +8. Add SignedIn, UserDropdown, SignedOut, SignOutButton components to other pages +9. Run: npm run dev + +Please provide complete, working code for {{productName}} authentication using the {{productName}} Next.js SDK, with a custom sign-in page rendering `` instead of a hosted-page redirect. diff --git a/docs/content/getting-started/connect-your-application/prompts/nextjs/redirect-based.txt b/docs/content/getting-started/connect-your-application/prompts/nextjs/redirect-based.txt new file mode 100644 index 0000000000..4b5badc202 --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/nextjs/redirect-based.txt @@ -0,0 +1,37 @@ +# Integrate {{productName}} Authentication in Next.js Application + +## Context +I have a Next.js application (App Router) and I want to integrate {{productName}}'s authentication system using the {{productName}} Next.js SDK with {{productName}}-hosted login pages. + +## Requirements +- Use @thunderid/nextjs SDK for authentication +- Configure {{productName}}-hosted login pages (not custom/embedded) +- Use the App Router (not Pages Router) +- Implement sign-in and sign-out with prebuilt components +- Add middleware for route protection and automatic token refresh +- Display signed-in user's profile information + +## Configuration +- **Client ID**: {{clientId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: @thunderid/nextjs + +## IMPORTANT Configuration Rules +- Use environment variables for configuration (NOT props on the provider) +- Required env vars: NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SECRET, NODE_TLS_REJECT_UNAUTHORIZED=0 +- Import ThunderIDProvider from '@thunderid/nextjs/server' (NOT from '@thunderid/nextjs') +- Import middleware utilities from '@thunderid/nextjs/server' +- Import UI components from '@thunderid/nextjs' +- The ThunderIDProvider handles the OAuth callback automatically — no manual callback route is needed + +## Implementation Steps +1. Create a Next.js app: npx create-next-app@latest nextjs-demo +2. Navigate into the project: cd nextjs-demo +3. Install @thunderid/nextjs: npm install @thunderid/nextjs +4. Create .env.local with NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SECRET, NODE_TLS_REJECT_UNAUTHORIZED=0 +5. Wrap root layout with from '@thunderid/nextjs/server' +6. Create proxy.ts with thunderIDProxy and createRouteMatcher from '@thunderid/nextjs/server' for route protection +7. Add SignedIn, UserDropdown, SignedOut, SignInButton components to pages +8. Run: npm run dev + +Please provide complete, working code with proper configuration for {{productName}} authentication using the {{productName}} Next.js SDK. diff --git a/docs/content/getting-started/connect-your-application/prompts/node/embedded.txt b/docs/content/getting-started/connect-your-application/prompts/node/embedded.txt new file mode 100644 index 0000000000..5b4a8e0c31 --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/node/embedded.txt @@ -0,0 +1,46 @@ +# Integrate {{productName}} Authentication in a Node.js Application (Custom UI / Embedded Mode) + +## Context +I have a Node.js application and I want to integrate {{productName}} authentication using the @thunderid/node SDK's embedded flow functions and the built-in http module — no framework required — rendering my own sign-in form instead of redirecting to {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/node SDK with the built-in Node.js http module +- Use `executeEmbeddedSignInFlow` (re-exported by @thunderid/node from the core @thunderid/javascript client) to drive the sign-in flow directly, instead of `ThunderIDNodeClient.signIn()` (which is redirect-only) +- Implement a `/login` route that serves a minimal HTML form +- Implement a `/flow/sign-in` route that calls `executeEmbeddedSignInFlow` on each POST and returns/renders whatever `components` the flow response asks for next +- Implement a `/logout` route to sign out and clear the session cookie +- Protect the `/profile` route using `isSignedIn()` and display user info with `getUser()` +- Manage sessions using a session ID stored in an HttpOnly cookie +- Keep code minimal and fully runnable with CommonJS `require()` + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **App URL**: http://localhost:3000 +- **Sign-In Page**: /login +- **Flow Route**: /flow/sign-in +- **SDK**: @thunderid/node + +## Important Rules +- Use CommonJS syntax (require) in index.js +- `ThunderIDNodeClient` itself only implements the redirect flow — for embedded mode, call `executeEmbeddedSignInFlow({applicationId, flowType: 'AUTHENTICATION', ...})` directly, imported from `@thunderid/node` (or `@thunderid/javascript`) +- @thunderid/node has NO bundled sign-in UI — write the HTML/JS form yourself, rendering whatever fields the flow response's `components` array specifies +- Each flow step returns an `executionId`; POST the next step's user input back to `/flow/sign-in` along with that `executionId` until the flow completes +- Store the session ID in a cookie named 'tid_session' with HttpOnly and SameSite=Lax flags +- Use randomUUID() from the built-in 'crypto' module to generate session IDs +- Use isSignedIn(sessionId) to guard protected routes +- Use getUser(sessionId) to retrieve the authenticated user profile +- Use signOut(sessionId) to clear the local session + +## Implementation Steps +1. Create a new project: mkdir my-node-app && cd my-node-app && npm init -y +2. Install @thunderid/node: npm install @thunderid/node +3. Create index.js importing `executeEmbeddedSignInFlow` from @thunderid/node +4. Add /login route serving a minimal HTML form +5. Add /flow/sign-in route: call executeEmbeddedSignInFlow with the applicationId and the user's submitted inputs, and set the session cookie once the flow completes +6. Add /logout route: clear the session cookie +7. Add / route: show sign-in or profile link based on isSignedIn() +8. Add /profile route: guard with isSignedIn(), display user info from getUser() +9. Start the server: node index.js + +Please provide a complete, working index.js file with all routes, driven by the embedded flow functions (not a redirect). diff --git a/docs/content/getting-started/connect-your-application/prompts/node/redirect-based.txt b/docs/content/getting-started/connect-your-application/prompts/node/redirect-based.txt new file mode 100644 index 0000000000..2e178b0b9a --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/node/redirect-based.txt @@ -0,0 +1,45 @@ +# Integrate {{productName}} Authentication in a Node.js Application + +## Context +I have a Node.js application and I want to integrate {{productName}} authentication using the @thunderid/node SDK and the built-in http module — no framework required. + +## Requirements +- Use @thunderid/node SDK with the built-in Node.js http module +- Initialize ThunderIDNodeClient with clientId, clientSecret, baseUrl, afterSignInUrl, afterSignOutUrl +- Implement /login route to start the sign-in flow (redirects to {{productName}}) +- Implement /callback route to handle the OAuth authorization code exchange +- Implement /logout route to sign out and clear the session cookie +- Protect the /profile route using isSignedIn() and display user info with getUser() +- Manage sessions using a session ID stored in an HttpOnly cookie +- Keep code minimal and fully runnable with CommonJS require() + +## Configuration +- **Client ID**: {{clientId}} +- **Client Secret**: `` +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **App URL**: http://localhost:3000 +- **Callback Route**: /callback +- **SDK**: @thunderid/node + +## Important Rules +- Use CommonJS syntax (require) in index.js +- Initialize ThunderIDNodeClient with auth.initialize({...}) before starting the server +- signIn() works in two phases: first call redirects the user (authUrlCallback), second call (with code+state) exchanges the token +- Store the session ID in a cookie named 'tid_session' with HttpOnly and SameSite=Lax flags +- Use randomUUID() from the built-in 'crypto' module to generate session IDs +- Use isSignedIn(sessionId) to guard protected routes +- Use getUser(sessionId) to retrieve the authenticated user profile +- Use signOut(sessionId) to get the OIDC end-session URL, then clear the local cookie and redirect + +## Implementation Steps +1. Create a new project: mkdir my-node-app && cd my-node-app && npm init -y +2. Install @thunderid/node: npm install @thunderid/node +3. Create index.js with ThunderIDNodeClient initialization +4. Add /login route: generate session ID cookie and redirect to {{productName}} auth URL +5. Add /callback route: exchange authorization code for tokens using signIn() +6. Add /logout route: call signOut() to get end-session URL, clear cookie, redirect +7. Add / route: show sign-in or profile link based on isSignedIn() +8. Add /profile route: guard with isSignedIn(), display user info from getUser() +9. Start the server: node index.js + +Please provide a complete, working index.js file with all routes and the ThunderIDNodeClient wired up correctly. diff --git a/docs/content/getting-started/connect-your-application/prompts/nuxt/embedded.txt b/docs/content/getting-started/connect-your-application/prompts/nuxt/embedded.txt new file mode 100644 index 0000000000..2ef19c0dae --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/nuxt/embedded.txt @@ -0,0 +1,44 @@ +# Integrate {{productName}} Authentication in Nuxt 3 Application (Custom UI Mode) + +## Context +I have a Nuxt 3 application and I want to integrate {{productName}}'s authentication system using the @thunderid/nuxt module with a custom sign-in UI instead of {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/nuxt module for authentication +- Register the module in nuxt.config.ts +- Configure via environment variables (no inline config) +- Wrap app.vue content with +- Render sign-in on a custom page using the module's own SignIn component (not a redirect to {{productName}}-hosted pages) +- Display signed-in user's profile information +- Optionally protect pages with the built-in thunderIDMiddleware + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **Sign In Page**: /signin (custom page) +- **SDK**: @thunderid/nuxt + +## IMPORTANT Configuration Rules +- Add '@thunderid/nuxt' to the modules array in nuxt.config.ts — no other config needed there +- All configuration is read from environment variables with NUXT_PUBLIC_ prefix for public values +- Required env vars: NUXT_PUBLIC_THUNDERID_BASE_URL, NUXT_PUBLIC_THUNDERID_APPLICATION_ID, THUNDERID_SESSION_SECRET (without the NUXT_PUBLIC_ prefix) +- Wrap with in app.vue +- All components (`SignedIn`, `SignedOut`, `SignIn`, `SignOutButton`, `User`) and composables are auto-imported — no manual import needed +- The `` component mirrors the Vue SDK's `SignIn`/`BaseSignIn` and drives the embedded (app-native) Flow Execution API directly — it replaces `window.location` navigation with Nuxt's `navigateTo` internally, so no manual redirect handling is needed +- Render `` inside `` on pages/signin.vue +- Protect pages by adding definePageMeta({ middleware: ['thunderIDMiddleware'] }) +- NEVER redirect to a {{productName}}-hosted URL for sign-in in this mode + +## Implementation Steps +1. Create a Nuxt 3 app: npx nuxi@latest init my-nuxt-app +2. Navigate into the project: cd my-nuxt-app && npm install +3. Install @thunderid/nuxt: npm install @thunderid/nuxt +4. Add '@thunderid/nuxt' to modules in nuxt.config.ts +5. Create .env with NUXT_PUBLIC_THUNDERID_BASE_URL, NUXT_PUBLIC_THUNDERID_APPLICATION_ID, THUNDERID_SESSION_SECRET +6. Wrap with in app.vue +7. Create pages/signin.vue rendering `` +8. Create pages/index.vue with SignedIn, SignOutButton, and User components +9. Optionally add definePageMeta({ middleware: ['thunderIDMiddleware'] }) to protected pages +10. Run: npm run dev + +Please provide complete, working code for {{productName}} authentication using the @thunderid/nuxt module, with a custom sign-in page rendering `` instead of a hosted-page redirect. diff --git a/docs/content/getting-started/connect-your-application/prompts/nuxt/redirect-based.txt b/docs/content/getting-started/connect-your-application/prompts/nuxt/redirect-based.txt new file mode 100644 index 0000000000..dff4a6bed7 --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/nuxt/redirect-based.txt @@ -0,0 +1,42 @@ +# Integrate {{productName}} Authentication in Nuxt 3 Application + +## Context +I have a Nuxt 3 application and I want to integrate {{productName}}'s authentication system using the @thunderid/nuxt module with {{productName}}-hosted login pages. + +## Requirements +- Use @thunderid/nuxt module for authentication +- Register the module in nuxt.config.ts +- Configure via environment variables (no inline config) +- Wrap app.vue content with +- Implement sign-in and sign-out with auto-imported components +- Display signed-in user's profile information +- Optionally protect pages with the built-in thunderIDMiddleware + +## Configuration +- **Client ID**: {{clientId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **Callback URL**: http://localhost:3000/api/auth/callback (auto-registered by the module) +- **SDK**: @thunderid/nuxt + +## IMPORTANT Configuration Rules +- Add '@thunderid/nuxt' to the modules array in nuxt.config.ts — no other config needed there +- All configuration is read from environment variables with NUXT_PUBLIC_ prefix for public values +- Required env vars: NUXT_PUBLIC_THUNDERID_BASE_URL, NUXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SESSION_SECRET +- THUNDERID_CLIENT_SECRET and THUNDERID_SESSION_SECRET must NOT have the NUXT_PUBLIC_ prefix +- The /api/auth/callback route is auto-registered by the module — do NOT create it manually +- Wrap with in app.vue +- All components (SignedIn, SignedOut, SignInButton, SignOutButton, User) and composables are auto-imported +- Protect pages by adding definePageMeta({ middleware: ['thunderIDMiddleware'] }) + +## Implementation Steps +1. Create a Nuxt 3 app: npx nuxi@latest init my-nuxt-app +2. Navigate into the project: cd my-nuxt-app && npm install +3. Install @thunderid/nuxt: npm install @thunderid/nuxt +4. Add '@thunderid/nuxt' to modules in nuxt.config.ts +5. Create .env with NUXT_PUBLIC_THUNDERID_BASE_URL, NUXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SESSION_SECRET +6. Wrap with in app.vue +7. Create pages/index.vue with SignInButton, SignOutButton, SignedIn, SignedOut, and User components +8. Optionally add definePageMeta({ middleware: ['thunderIDMiddleware'] }) to protected pages +9. Run: npm run dev + +Please provide complete, working code with proper configuration for {{productName}} authentication using the @thunderid/nuxt module. diff --git a/docs/content/getting-started/connect-your-application/prompts/react/embedded.txt b/docs/content/getting-started/connect-your-application/prompts/react/embedded.txt new file mode 100644 index 0000000000..af5c0ed6d2 --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/react/embedded.txt @@ -0,0 +1,45 @@ +# Integrate {{productName}} Authentication in React Application (Custom UI Mode) + +## Context +I have a React application and I want to integrate {{productName}}'s authentication system using the ThunderID React SDK with a custom sign-in UI instead of {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/react SDK for authentication +- Render sign-in on a custom route (not a redirect to {{productName}}-hosted pages) +- Use react-router for routing to the custom sign-in page +- Implement sign-in and sign-out functionality +- Display signed-in user's profile information +- Handle authentication state automatically + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **Sign In URL**: http://localhost:5173/signin (custom route) +- **SDK**: @thunderid/react +- **Router**: react-router + +## IMPORTANT Configuration Rules +- DO NOT create a separate config object - pass all configuration as individual props directly to +- Required props: `baseUrl`, `signInUrl`, and `applicationId` +- Optional props: `afterSignInUrl`, `afterSignOutUrl`, `scopes` +- Example: +- The `` component drives the Flow Execution API directly (not a redirect) — render it on your custom sign-in route +- `` accepts `onSuccess`/`onError` callbacks, and optionally a render-prop `children` function receiving `{components, onSubmit, isLoading, error, isInitialized}` for a fully custom form +- Access auth state via `useThunderID()` (ONLY hook available: exposes `isSignedIn`, `user`, `signIn`, `signOut`, `signUp`) — no other hooks like `useAuth`/`useSession`/`useUser` exist +- NEVER redirect to a {{productName}}-hosted URL for sign-in in this mode + +## Implementation Steps +1. Create a React app using Vite by running: `npm create vite@latest my-react-app -- --template react` +2. Navigate into the project directory: `cd my-react-app` +3. Install dependencies: `npm install` +4. Install react-router package: `npm install react-router` +5. Install @thunderid/react package: `npm install @thunderid/react` +6. Wrap your app with and configure baseUrl, signInUrl, and applicationId +7. Set up React Router with BrowserRouter, create a /signin route rendering ``, and use , , , , and for the rest of the authentication UI +8. Run the development server: `npm run dev` + +Please provide complete, working code with: +- Proper routing configuration +- A custom sign-in page rendering `` (not a redirect) +- Proper integration with the ThunderID React SDK +- Use of the `useThunderID` hook if programmatic access to auth state is needed diff --git a/docs/content/getting-started/connect-your-application/prompts/react/redirect-based.txt b/docs/content/getting-started/connect-your-application/prompts/react/redirect-based.txt new file mode 100644 index 0000000000..8c4958ff6a --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/react/redirect-based.txt @@ -0,0 +1,34 @@ +# Integrate {{productName}} Authentication in React Application (Inbuilt Mode) + +## Context +I have a React application and I want to integrate {{productName}}'s authentication system using the ThunderID React SDK with {{productName}}-hosted login pages. + +## Requirements +- Use @thunderid/react SDK for authentication +- Configure {{productName}}-hosted login, registration, and account management UIs +- Implement sign-in and sign-out functionality using prebuilt components +- Display signed-in user's profile information +- Handle authentication state automatically + +## Configuration +- **Client ID**: {{clientId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: @thunderid/react + +## IMPORTANT Configuration Rules +- DO NOT create a separate config object - pass all configuration as individual props directly to +- Required props: `clientId` and `baseUrl` +- Optional props: `afterSignInUrl`, `afterSignOutUrl`, `scopes` (string array) +- NEVER use property names like `signInRedirectURL` or `signOutRedirectURL` +- Example: + +## Implementation Steps +1. Create a React app using Vite by running: `npm create vite@latest my-react-app -- --template react` +2. Navigate into the project directory: `cd my-react-app` +3. Install dependencies: `npm install` +4. Install @thunderid/react package: `npm install @thunderid/react` +5. Wrap your app with and configure clientId and baseUrl +6. Build with ThunderID components: use , , , and to control what signed-in and signed-out users see +7. Run the development server: `npm run dev` + +Please provide complete, working code with proper configuration for {{productName}} authentication using the ThunderID React SDK. diff --git a/docs/content/getting-started/connect-your-application/prompts/vue/embedded.txt b/docs/content/getting-started/connect-your-application/prompts/vue/embedded.txt new file mode 100644 index 0000000000..17e76530ec --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/vue/embedded.txt @@ -0,0 +1,40 @@ +# Integrate {{productName}} Authentication in Vue 3 Application (Custom UI Mode) + +## Context +I have a Vue 3 application and I want to integrate {{productName}}'s authentication system using the @thunderid/vue SDK with a custom sign-in UI instead of {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/vue SDK for authentication +- Register the ThunderIDPlugin in main.js +- Render sign-in on a custom route using the SDK's own SignIn component (not a redirect to {{productName}}-hosted pages) +- Use vue-router for routing to the custom sign-in page +- Implement sign-in and sign-out functionality +- Display signed-in user's profile information + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **Sign In Route**: /signin (custom route) +- **SDK**: @thunderid/vue + +## IMPORTANT Configuration Rules +- Register ThunderIDPlugin via `app.use(ThunderIDPlugin)` in src/main.js +- Import ThunderIDPlugin from '@thunderid/vue' +- Wrap the app root in App.vue with `` +- Pass configuration as kebab-case attributes: `application-id` and `base-url` +- The `` component drives the Flow Execution API directly (not a redirect) — render it on your custom /signin route, inside `` +- `` emits `@success`/`@error` events +- Access auth state via the `useThunderID()` composable (`isSignedIn`, `user`, `signIn`, `signOut`, `signUp`) +- NEVER redirect to a {{productName}}-hosted URL for sign-in in this mode + +## Implementation Steps +1. Create a Vue 3 app: npm create vite@latest my-vue-app -- --template vue +2. Navigate into the project: cd my-vue-app && npm install +3. Install @thunderid/vue and vue-router: npm install @thunderid/vue vue-router +4. Register ThunderIDPlugin in src/main.js with app.use(ThunderIDPlugin) +5. Wrap app content with in src/App.vue +6. Set up vue-router with a /signin route rendering `` +7. Use , , and for the rest of the authentication UI +8. Run: npm run dev + +Please provide complete, working code with proper routing, a custom sign-in page rendering `` (not a redirect), and use of the `useThunderID()` composable where programmatic access to auth state is needed. diff --git a/docs/content/getting-started/connect-your-application/prompts/vue/redirect-based.txt b/docs/content/getting-started/connect-your-application/prompts/vue/redirect-based.txt new file mode 100644 index 0000000000..80600c78e2 --- /dev/null +++ b/docs/content/getting-started/connect-your-application/prompts/vue/redirect-based.txt @@ -0,0 +1,35 @@ +# Integrate {{productName}} Authentication in Vue 3 Application + +## Context +I have a Vue 3 application and I want to integrate {{productName}}'s authentication system using the @thunderid/vue SDK with {{productName}}-hosted login pages. + +## Requirements +- Use @thunderid/vue SDK for authentication +- Register the ThunderIDPlugin in main.js +- Wrap the app with ThunderIDProvider in App.vue +- Implement sign-in and sign-out with prebuilt components +- Display signed-in user's profile using the UserDropdown component + +## Configuration +- **Client ID**: {{clientId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: @thunderid/vue + +## IMPORTANT Configuration Rules +- Register ThunderIDPlugin via app.use(ThunderIDPlugin) in src/main.js +- Import ThunderIDPlugin from '@thunderid/vue' +- Wrap the app root in App.vue with +- Pass configuration as kebab-case attributes: `client-id` and `base-url` +- Import UI components (SignInButton, SignOutButton, SignedIn, SignedOut, UserDropdown) from '@thunderid/vue' +- Use inside to display the signed-in user's profile and sign-out option + +## Implementation Steps +1. Create a Vue 3 app: npm create vite@latest my-vue-app -- --template vue +2. Navigate into the project: cd my-vue-app && npm install +3. Install @thunderid/vue: npm install @thunderid/vue +4. Register ThunderIDPlugin in src/main.js with app.use(ThunderIDPlugin) +5. Wrap app content with in src/App.vue +6. Add SignInButton inside SignedOut and UserDropdown inside SignedIn conditional wrappers +7. Run: npm run dev + +Please provide complete, working code with proper configuration for {{productName}} authentication using the @thunderid/vue SDK. diff --git a/docs/package.json b/docs/package.json index 30055aafb5..9a1d1edf65 100644 --- a/docs/package.json +++ b/docs/package.json @@ -31,6 +31,7 @@ "generate:changelog": "node ./scripts/generate-changelog.mjs", "generate:contributors": "node ./scripts/generate-contributors.mjs", "generate:postman-collections": "node ./scripts/generate-postman-collections.mjs", + "generate:prompts": "node ./scripts/generate-prompts.mjs", "generate:sdk-releases": "node ./scripts/generate-sdk-releases.mjs", "setup": "node ./scripts/prebuild.mjs", "lint": "eslint .", @@ -51,10 +52,17 @@ "@docusaurus/theme-mermaid": "3.9.2", "@mdx-js/react": "3.0.0", "@scalar/api-reference-react": "0.9.38", + "@tanstack/react-query": "catalog:", + "@thunderid/components": "workspace:^", + "@thunderid/contexts": "workspace:^", "@thunderid/design": "workspace:^", "@thunderid/eslint-plugin": "workspace:^", + "@thunderid/hooks": "workspace:^", + "@thunderid/i18n": "workspace:^", "@thunderid/logger": "workspace:^", "@thunderid/prettier-config": "workspace:^", + "@thunderid/react": "catalog:", + "@thunderid/utils": "workspace:^", "@wso2/oxygen-ui": "catalog:", "@wso2/oxygen-ui-icons-react": "catalog:", "clsx": "2.1.1", @@ -63,7 +71,9 @@ "prism-react-renderer": "2.3.0", "react": "19.2.3", "react-dom": "19.2.3", - "react-github-btn": "1.4.0" + "react-github-btn": "1.4.0", + "react-i18next": "catalog:", + "react-router": "catalog:" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.9.2", diff --git a/docs/scripts/generate-prompts.mjs b/docs/scripts/generate-prompts.mjs new file mode 100644 index 0000000000..d699dbb889 --- /dev/null +++ b/docs/scripts/generate-prompts.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import {existsSync, mkdirSync, readdirSync, copyFileSync} from 'fs'; +import {join, dirname, relative, sep} from 'path'; +import {fileURLToPath} from 'url'; +import {createLogger} from '@thunderid/logger'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const logger = createLogger('generate-prompts'); + +// LLM prompt .txt files live alongside the docs page they belong to +// (e.g. content/getting-started/connect-your-application/prompts/react/redirect-based.txt) +// so they stay versioned with the docs. They're mirrored here into static/docs//... +// so Docusaurus serves them as plain, CORS-fetchable .txt files (the console fetches them +// directly), matching the URL scheme used for every other doc-relative link in config.js. +const PROMPT_ROOT_SEGMENT = 'prompts'; + +const VERSION_ROOTS = [ + {sourceDir: join(__dirname, '..', 'content'), versionPath: 'next'}, + {sourceDir: join(__dirname, '..', 'versioned_docs', 'version-v1.0.x'), versionPath: 'v1.0.x'}, +]; + +const STATIC_DOCS_DIR = join(__dirname, '..', 'static', 'docs'); + +/** Recursively collect every file living under a `prompts/` directory. */ +function findPromptFiles(dir, baseDir = dir) { + const files = []; + if (!existsSync(dir)) return files; + + for (const entry of readdirSync(dir, {withFileTypes: true})) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...findPromptFiles(fullPath, baseDir)); + } else if (entry.isFile()) { + const relativePath = relative(baseDir, fullPath); + if (relativePath.split(sep).includes(PROMPT_ROOT_SEGMENT)) { + files.push({fullPath, relativePath}); + } + } + } + return files; +} + +function generatePrompts() { + logger.info('🔄 Copying LLM prompt files into static/docs/...'); + + let written = 0; + + for (const {sourceDir, versionPath} of VERSION_ROOTS) { + for (const {fullPath, relativePath} of findPromptFiles(sourceDir)) { + const outputPath = join(STATIC_DOCS_DIR, versionPath, relativePath); + mkdirSync(dirname(outputPath), {recursive: true}); + copyFileSync(fullPath, outputPath); + written++; + } + } + + logger.info(`✅ Copied ${written} prompt file(s) into static/docs/{${VERSION_ROOTS.map((v) => v.versionPath).join(',')}}`); +} + +try { + generatePrompts(); +} catch (error) { + logger.error('❌ Error copying prompt files:', error); + process.exit(1); +} diff --git a/docs/scripts/prebuild.mjs b/docs/scripts/prebuild.mjs index ee71d62f9c..4b3d6b97cf 100644 --- a/docs/scripts/prebuild.mjs +++ b/docs/scripts/prebuild.mjs @@ -65,6 +65,9 @@ async function generateDocs() { // Generate SDK release data executeScript('SDK Releases Generator', join(__dirname, 'generate-sdk-releases.mjs')); + // Copy LLM prompt files into static/docs/ + executeScript('Prompts Generator', join(__dirname, 'generate-prompts.mjs')); + logger.info('🎉 All documentation artifacts generated successfully!\n'); } diff --git a/docs/src/components/DeveloperShortcut.tsx b/docs/src/components/DeveloperShortcut.tsx index 8d4bdbabd4..26d9cd3d8a 100644 --- a/docs/src/components/DeveloperShortcut.tsx +++ b/docs/src/components/DeveloperShortcut.tsx @@ -3,12 +3,11 @@ import Link from '@docusaurus/Link'; import {useWindowSize} from '@docusaurus/theme-common'; +import {AndroidLogo, FlutterLogo} from '@thunderid/components'; import {Box, Chip, Typography} from '@wso2/oxygen-ui'; import {AppWindow, Bot, Check, Download, MonitorSmartphone, Server, Zap} from '@wso2/oxygen-ui-icons-react'; import React, {useCallback} from 'react'; -import AndroidLogo from './icons/AndroidLogo'; import ExpressLogo from './icons/ExpressLogo'; -import FlutterLogo from './icons/FlutterLogo'; import IOSLogo from './icons/IOSLogo'; import JavaScriptLogo from './icons/JavaScriptLogo'; import LangChainLogo from './icons/LangChainLogo'; diff --git a/docs/src/components/Ecosystem/data.ts b/docs/src/components/Ecosystem/data.ts index c30e64e8de..b5393cf699 100644 --- a/docs/src/components/Ecosystem/data.ts +++ b/docs/src/components/Ecosystem/data.ts @@ -1,8 +1,8 @@ // Copyright 2026 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 +import {AndroidLogo, FlutterLogo} from '@thunderid/components'; import {ComponentType} from 'react'; -import AndroidLogo from '../icons/AndroidLogo'; import AngularLogo from '../icons/AngularLogo'; import AuthJsLogo from '../icons/AuthJsLogo'; import BetterAuthLogo from '../icons/BetterAuthLogo'; @@ -10,7 +10,6 @@ import BrowserLogo from '../icons/BrowserLogo'; import ClaudeLogo from '../icons/ClaudeLogo'; import CodexLogo from '../icons/CodexLogo'; import ExpressLogo from '../icons/ExpressLogo'; -import FlutterLogo from '../icons/FlutterLogo'; import GoLogo from '../icons/GoLogo'; import IOSLogo from '../icons/IOSLogo'; import JavaScriptLogo from '../icons/JavaScriptLogo'; diff --git a/docs/src/components/FloatingLogosBackground.tsx b/docs/src/components/FloatingLogosBackground.tsx index 0a6ce727d1..a1adbb538c 100644 --- a/docs/src/components/FloatingLogosBackground.tsx +++ b/docs/src/components/FloatingLogosBackground.tsx @@ -1,13 +1,12 @@ // Copyright 2026 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 +import {AndroidLogo, FlutterLogo} from '@thunderid/components'; import {Box} from '@wso2/oxygen-ui'; import {JSX} from 'react'; -import AndroidLogo from './icons/AndroidLogo'; import AngularLogo from './icons/AngularLogo'; import BrowserLogo from './icons/BrowserLogo'; import ExpressLogo from './icons/ExpressLogo'; -import FlutterLogo from './icons/FlutterLogo'; import GoLogo from './icons/GoLogo'; import IOSLogo from './icons/IOSLogo'; import NextLogo from './icons/NextLogo'; diff --git a/docs/src/components/HomePage/SDKShowcaseSection.tsx b/docs/src/components/HomePage/SDKShowcaseSection.tsx index 31e99a2990..da460553fc 100644 --- a/docs/src/components/HomePage/SDKShowcaseSection.tsx +++ b/docs/src/components/HomePage/SDKShowcaseSection.tsx @@ -2,11 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import Link from '@docusaurus/Link'; +import {AndroidLogo, FlutterLogo} from '@thunderid/components'; import {Box, Container, Typography} from '@wso2/oxygen-ui'; import {JSX, useState} from 'react'; -import AndroidLogo from '../icons/AndroidLogo'; import ExpressLogo from '../icons/ExpressLogo'; -import FlutterLogo from '../icons/FlutterLogo'; import IOSLogo from '../icons/IOSLogo'; import JavaScriptLogo from '../icons/JavaScriptLogo'; import NextLogo from '../icons/NextLogo'; diff --git a/docs/src/theme/DocSidebarItem/Link/index.tsx b/docs/src/theme/DocSidebarItem/Link/index.tsx index b90635331a..175a4259d7 100644 --- a/docs/src/theme/DocSidebarItem/Link/index.tsx +++ b/docs/src/theme/DocSidebarItem/Link/index.tsx @@ -4,10 +4,9 @@ import Link from '@docusaurus/Link'; import {usePluginData} from '@docusaurus/useGlobalData'; import OriginalDocSidebarItemLink from '@theme-original/DocSidebarItem/Link'; +import {AndroidLogo, FlutterLogo} from '@thunderid/components'; import React from 'react'; -import AndroidLogo from '@site/src/components/icons/AndroidLogo'; import ExpressLogo from '@site/src/components/icons/ExpressLogo'; -import FlutterLogo from '@site/src/components/icons/FlutterLogo'; import IOSLogo from '@site/src/components/icons/IOSLogo'; import JavaScriptLogo from '@site/src/components/icons/JavaScriptLogo'; import LangChainLogo from '@site/src/components/icons/LangChainLogo'; diff --git a/docs/src/theme/MDXComponents.tsx b/docs/src/theme/MDXComponents.tsx index 5936477487..65c703ab09 100644 --- a/docs/src/theme/MDXComponents.tsx +++ b/docs/src/theme/MDXComponents.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import MDXComponents from '@theme-original/MDXComponents'; +import {AndroidLogo, FlutterLogo} from '@thunderid/components'; import { Box, Card, @@ -40,7 +41,6 @@ import DocsGetStarted from '@site/src/components/DocsGetStarted'; import FloatingLogosBackground from '@site/src/components/FloatingLogosBackground'; import {BuildAFlowDiagram, FlowNodeTypesRoadmap, FlowBuildingBlocksRoadmap} from '@site/src/components/FlowConcepts'; import GettingStartedJourney from '@site/src/components/GettingStartedJourney'; -import AndroidLogo from '@site/src/components/icons/AndroidLogo'; import AngularLogo from '@site/src/components/icons/AngularLogo'; import BrowserLogo from '@site/src/components/icons/BrowserLogo'; import ClaudeLogo from '@site/src/components/icons/ClaudeLogo'; @@ -48,7 +48,6 @@ import CliLogo from '@site/src/components/icons/CliLogo'; import CodexLogo from '@site/src/components/icons/CodexLogo'; import DockerLogo from '@site/src/components/icons/DockerLogo'; import ExpressLogo from '@site/src/components/icons/ExpressLogo'; -import FlutterLogo from '@site/src/components/icons/FlutterLogo'; import GoLogo from '@site/src/components/icons/GoLogo'; import Html5Logo from '@site/src/components/icons/Html5Logo'; import IOSLogo from '@site/src/components/icons/IOSLogo'; diff --git a/docs/static/data/contributors.json b/docs/static/data/contributors.json index c4d5c43cc1..e74d9d4037 100644 --- a/docs/static/data/contributors.json +++ b/docs/static/data/contributors.json @@ -2,55 +2,61 @@ "contributors": [ { "avatarUrl": "https://avatars.githubusercontent.com/u/35653110?v=4", - "contributions": 886, + "contributions": 1009, "htmlUrl": "https://github.com/ThaminduDilshan", "login": "ThaminduDilshan" }, - { - "avatarUrl": "https://avatars.githubusercontent.com/u/1617810?v=4", - "contributions": 463, - "htmlUrl": "https://github.com/darshanasbg", - "login": "darshanasbg" - }, { "avatarUrl": "https://avatars.githubusercontent.com/u/25959096?v=4", - "contributions": 418, + "contributions": 492, "htmlUrl": "https://github.com/brionmario", "login": "brionmario" }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/1617810?v=4", + "contributions": 464, + "htmlUrl": "https://github.com/darshanasbg", + "login": "darshanasbg" + }, { "avatarUrl": "https://avatars.githubusercontent.com/u/42619922?v=4", - "contributions": 367, + "contributions": 431, "htmlUrl": "https://github.com/DonOmalVindula", "login": "DonOmalVindula" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/114133267?v=4", - "contributions": 301, + "contributions": 360, "htmlUrl": "https://github.com/thiva-k", "login": "thiva-k" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/32898873?v=4", - "contributions": 239, + "contributions": 338, "htmlUrl": "https://github.com/rajithacharith", "login": "rajithacharith" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/62582918?v=4", - "contributions": 205, + "contributions": 207, "htmlUrl": "https://github.com/KaveeshaPiumini", "login": "KaveeshaPiumini" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/11584828?v=4", - "contributions": 181, + "contributions": 204, "htmlUrl": "https://github.com/senthalan", "login": "senthalan" }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/32576163?v=4", + "contributions": 129, + "htmlUrl": "https://github.com/sahandilshan", + "login": "sahandilshan" + }, { "avatarUrl": "https://avatars.githubusercontent.com/u/7569427?v=4", - "contributions": 108, + "contributions": 122, "htmlUrl": "https://github.com/jeradrutnam", "login": "jeradrutnam" }, @@ -61,76 +67,112 @@ "login": "JeethJJ" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/32576163?v=4", - "contributions": 87, - "htmlUrl": "https://github.com/sahandilshan", - "login": "sahandilshan" + "avatarUrl": "https://avatars.githubusercontent.com/u/79596630?v=4", + "contributions": 105, + "htmlUrl": "https://github.com/Malith-19", + "login": "Malith-19" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/31583807?v=4", - "contributions": 76, + "contributions": 101, "htmlUrl": "https://github.com/himeshsiriwardana", "login": "himeshsiriwardana" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/42399179?v=4", - "contributions": 73, + "contributions": 91, "htmlUrl": "https://github.com/ThumulaPerera", "login": "ThumulaPerera" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/79596630?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/37938529?v=4", + "contributions": 86, + "htmlUrl": "https://github.com/JayaShakthi97", + "login": "JayaShakthi97" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/77233788?v=4", "contributions": 60, - "htmlUrl": "https://github.com/Malith-19", - "login": "Malith-19" + "htmlUrl": "https://github.com/Dilusha-Madushan", + "login": "Dilusha-Madushan" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/35717390?v=4", - "contributions": 49, + "contributions": 58, "htmlUrl": "https://github.com/sadilchamishka", "login": "sadilchamishka" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/46132469?v=4", - "contributions": 42, + "contributions": 43, "htmlUrl": "https://github.com/hwupathum", "login": "hwupathum" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/77233788?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/1332991?v=4", + "contributions": 42, + "htmlUrl": "https://github.com/madurangasiriwardena", + "login": "madurangasiriwardena" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/36252606?v=4", "contributions": 41, - "htmlUrl": "https://github.com/Dilusha-Madushan", - "login": "Dilusha-Madushan" + "htmlUrl": "https://github.com/UdeshAthukorala", + "login": "UdeshAthukorala" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/37938529?v=4", - "contributions": 39, - "htmlUrl": "https://github.com/JayaShakthi97", - "login": "JayaShakthi97" + "avatarUrl": "https://avatars.githubusercontent.com/u/119397108?v=4", + "contributions": 37, + "htmlUrl": "https://github.com/NutharaNR", + "login": "NutharaNR" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/154749626?v=4", - "contributions": 21, + "contributions": 25, "htmlUrl": "https://github.com/RandithaK", "login": "RandithaK" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/119397108?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/51480027?v=4", + "contributions": 24, + "htmlUrl": "https://github.com/ayeshajay", + "login": "ayeshajay" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/270975584?v=4", + "contributions": 23, + "htmlUrl": "https://github.com/anushasunkada", + "login": "anushasunkada" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/4128062?v=4", + "contributions": 23, + "htmlUrl": "https://github.com/indeewari", + "login": "indeewari" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/194389180?v=4", + "contributions": 20, + "htmlUrl": "https://github.com/Yathusiga27", + "login": "Yathusiga27" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/124599924?v=4", "contributions": 18, - "htmlUrl": "https://github.com/NutharaNR", - "login": "NutharaNR" + "htmlUrl": "https://github.com/Osara-B", + "login": "Osara-B" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/51480027?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/118417912?v=4", "contributions": 17, - "htmlUrl": "https://github.com/ayeshajay", - "login": "ayeshajay" + "htmlUrl": "https://github.com/ImalshaD", + "login": "ImalshaD" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/36252606?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/86454991?v=4", "contributions": 16, - "htmlUrl": "https://github.com/UdeshAthukorala", - "login": "UdeshAthukorala" + "htmlUrl": "https://github.com/KD23243", + "login": "KD23243" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/42939752?v=4", @@ -139,10 +181,10 @@ "login": "RushanNanayakkara" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/1332991?v=4", - "contributions": 11, - "htmlUrl": "https://github.com/madurangasiriwardena", - "login": "madurangasiriwardena" + "avatarUrl": "https://avatars.githubusercontent.com/u/74812270?v=4", + "contributions": 12, + "htmlUrl": "https://github.com/Sadeesha-Sath", + "login": "Sadeesha-Sath" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/45028707?v=4", @@ -150,21 +192,21 @@ "htmlUrl": "https://github.com/ThaminduR", "login": "ThaminduR" }, - { - "avatarUrl": "https://avatars.githubusercontent.com/u/86454991?v=4", - "contributions": 10, - "htmlUrl": "https://github.com/KD23243", - "login": "KD23243" - }, { "avatarUrl": "https://avatars.githubusercontent.com/u/17597293?v=4", - "contributions": 8, + "contributions": 10, "htmlUrl": "https://github.com/piraveena", "login": "piraveena" }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/739892?v=4", + "contributions": 10, + "htmlUrl": "https://github.com/sagara-gunathunga", + "login": "sagara-gunathunga" + }, { "avatarUrl": "https://avatars.githubusercontent.com/u/183357006?v=4", - "contributions": 8, + "contributions": 9, "htmlUrl": "https://github.com/sacrana0", "login": "sacrana0" }, @@ -181,16 +223,22 @@ "login": "nandhu-kumar" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/270975584?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/50801227?v=4", "contributions": 7, - "htmlUrl": "https://github.com/anushasunkada", - "login": "anushasunkada" + "htmlUrl": "https://github.com/ZiyamSanthosh", + "login": "ZiyamSanthosh" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/739892?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/25483865?v=4", + "contributions": 7, + "htmlUrl": "https://github.com/AnuradhaSK", + "login": "AnuradhaSK" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/61885844?v=4", "contributions": 6, - "htmlUrl": "https://github.com/sagara-gunathunga", - "login": "sagara-gunathunga" + "htmlUrl": "https://github.com/PasinduYeshan", + "login": "PasinduYeshan" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/39120228?v=4", @@ -211,10 +259,22 @@ "login": "Sithumli" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/124599924?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/30111554?v=4", "contributions": 5, - "htmlUrl": "https://github.com/Osara-B", - "login": "Osara-B" + "htmlUrl": "https://github.com/NipuniBhagya", + "login": "NipuniBhagya" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/150885396?v=4", + "contributions": 5, + "htmlUrl": "https://github.com/samadhisakunika", + "login": "samadhisakunika" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/150885192?v=4", + "contributions": 5, + "htmlUrl": "https://github.com/ravindu439", + "login": "ravindu439" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/146766148?v=4", @@ -222,12 +282,6 @@ "htmlUrl": "https://github.com/kavindadimuthu", "login": "kavindadimuthu" }, - { - "avatarUrl": "https://avatars.githubusercontent.com/u/194389180?v=4", - "contributions": 4, - "htmlUrl": "https://github.com/Yathusiga27", - "login": "Yathusiga27" - }, { "avatarUrl": "https://avatars.githubusercontent.com/u/110180949?v=4", "contributions": 4, @@ -235,10 +289,10 @@ "login": "ranuka-laksika" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/150885396?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/9883427?v=4", "contributions": 4, - "htmlUrl": "https://github.com/samadhisakunika", - "login": "samadhisakunika" + "htmlUrl": "https://github.com/omindu", + "login": "omindu" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/150146619?v=4", @@ -247,28 +301,16 @@ "login": "DharshanSR" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/9883427?v=4", - "contributions": 4, - "htmlUrl": "https://github.com/omindu", - "login": "omindu" - }, - { - "avatarUrl": "https://avatars.githubusercontent.com/u/6869769?v=4", - "contributions": 3, - "htmlUrl": "https://github.com/maheshika", - "login": "maheshika" - }, - { - "avatarUrl": "https://avatars.githubusercontent.com/u/4128062?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/130324291?v=4", "contributions": 3, - "htmlUrl": "https://github.com/indeewari", - "login": "indeewari" + "htmlUrl": "https://github.com/HesandaLiyanage", + "login": "HesandaLiyanage" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/25483865?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/8939174?v=4", "contributions": 3, - "htmlUrl": "https://github.com/AnuradhaSK", - "login": "AnuradhaSK" + "htmlUrl": "https://github.com/JKAUSHALYA", + "login": "JKAUSHALYA" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/145958660?v=4", @@ -277,16 +319,10 @@ "login": "warnakulasuriya-fds-e23" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/8939174?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/6869769?v=4", "contributions": 3, - "htmlUrl": "https://github.com/JKAUSHALYA", - "login": "JKAUSHALYA" - }, - { - "avatarUrl": "https://avatars.githubusercontent.com/u/17524063?v=4", - "contributions": 2, - "htmlUrl": "https://github.com/VajiraPrabuddhaka", - "login": "VajiraPrabuddhaka" + "htmlUrl": "https://github.com/maheshika", + "login": "maheshika" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/25488962?v=4", @@ -295,22 +331,28 @@ "login": "Thumimku" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/150885192?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/17524063?v=4", "contributions": 2, - "htmlUrl": "https://github.com/ravindu439", - "login": "ravindu439" + "htmlUrl": "https://github.com/VajiraPrabuddhaka", + "login": "VajiraPrabuddhaka" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/75576423?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/5061791?v=4", "contributions": 2, - "htmlUrl": "https://github.com/BimsaraBodaragama", - "login": "BimsaraBodaragama" + "htmlUrl": "https://github.com/ayshsandu", + "login": "ayshsandu" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/130324291?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/2276412?v=4", "contributions": 2, - "htmlUrl": "https://github.com/HesandaLiyanage", - "login": "HesandaLiyanage" + "htmlUrl": "https://github.com/kavix", + "login": "kavix" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/123491619?v=4", + "contributions": 2, + "htmlUrl": "https://github.com/janithjay", + "login": "janithjay" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/175306990?v=4", @@ -331,34 +373,46 @@ "login": "malinthaprasan" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/29277992?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/75576423?v=4", + "contributions": 2, + "htmlUrl": "https://github.com/BimsaraBodaragama", + "login": "BimsaraBodaragama" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/67315176?v=4", "contributions": 1, - "htmlUrl": "https://github.com/isala404", - "login": "isala404" + "htmlUrl": "https://github.com/savindi7", + "login": "savindi7" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/6144466?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/79456372?v=4", "contributions": 1, - "htmlUrl": "https://github.com/madhuramendis", - "login": "madhuramendis" + "htmlUrl": "https://github.com/prdai", + "login": "prdai" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/140165177?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/152188411?v=4", "contributions": 1, - "htmlUrl": "https://github.com/nishagii", - "login": "nishagii" + "htmlUrl": "https://github.com/iff-sal", + "login": "iff-sal" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/34201061?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/142331771?v=4", "contributions": 1, - "htmlUrl": "https://github.com/PasinduRavimal", - "login": "PasinduRavimal" + "htmlUrl": "https://github.com/garuka-satharasinghe", + "login": "garuka-satharasinghe" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/62681950?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/49331487?v=4", "contributions": 1, - "htmlUrl": "https://github.com/priyanshoon", - "login": "priyanshoon" + "htmlUrl": "https://github.com/drsamitha", + "login": "drsamitha" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/55852668?v=4", + "contributions": 1, + "htmlUrl": "https://github.com/littleKitchen", + "login": "littleKitchen" }, { "avatarUrl": "https://avatars.githubusercontent.com/u/74367192?v=4", @@ -367,49 +421,55 @@ "login": "th33k" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/65800949?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/62681950?v=4", "contributions": 1, - "htmlUrl": "https://github.com/HasiniSama", - "login": "HasiniSama" + "htmlUrl": "https://github.com/priyanshoon", + "login": "priyanshoon" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/55852668?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/34201061?v=4", "contributions": 1, - "htmlUrl": "https://github.com/littleKitchen", - "login": "littleKitchen" + "htmlUrl": "https://github.com/PasinduRavimal", + "login": "PasinduRavimal" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/49331487?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/140165177?v=4", "contributions": 1, - "htmlUrl": "https://github.com/drsamitha", - "login": "drsamitha" + "htmlUrl": "https://github.com/nishagii", + "login": "nishagii" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/142331771?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/6144466?v=4", "contributions": 1, - "htmlUrl": "https://github.com/garuka-satharasinghe", - "login": "garuka-satharasinghe" + "htmlUrl": "https://github.com/madhuramendis", + "login": "madhuramendis" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/152188411?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/29277992?v=4", "contributions": 1, - "htmlUrl": "https://github.com/iff-sal", - "login": "iff-sal" + "htmlUrl": "https://github.com/isala404", + "login": "isala404" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/2276412?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/65800949?v=4", "contributions": 1, - "htmlUrl": "https://github.com/kavix", - "login": "kavix" + "htmlUrl": "https://github.com/HasiniSama", + "login": "HasiniSama" }, { - "avatarUrl": "https://avatars.githubusercontent.com/u/67315176?v=4", + "avatarUrl": "https://avatars.githubusercontent.com/u/77677724?v=4", "contributions": 1, - "htmlUrl": "https://github.com/savindi7", - "login": "savindi7" + "htmlUrl": "https://github.com/KashiwalHarsh", + "login": "KashiwalHarsh" + }, + { + "avatarUrl": "https://avatars.githubusercontent.com/u/101007824?v=4", + "contributions": 1, + "htmlUrl": "https://github.com/chamals3n4", + "login": "chamals3n4" } ], - "generatedAt": "2026-06-23T17:39:55.706Z", - "totalCommits": 3993, - "totalContributors": 68 + "generatedAt": "2026-08-05T08:29:36.074Z", + "totalCommits": 4886, + "totalContributors": 78 } diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/android.mdx b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/android.mdx index d269027256..3fcc7fab5d 100644 --- a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/android.mdx +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/android.mdx @@ -64,7 +64,11 @@ Once it's running, the console is available at [https://localhost:8090/console]( 3. Under **Application Type**, select **Mobile App**. 4. Enter a name (e.g. `My Android App`) and create an application. The rest of the settings can stay at their defaults. -5. Copy the **Client ID** from the **General** tab. You'll add the redirect URI in the next step after configuring your URL scheme. +5. Copy the **Application ID** from the **General** tab, under **Quick Copy**. + +:::info App-Native Authentication +This quickstart uses app-native authentication through the Flow Execution API. The Android SDK renders the sign-in and sign-up forms natively in your app, so you only need the **Application ID**, no OAuth 2.0 Client ID and no redirect URI. +::: ## Create an Android App @@ -102,48 +106,6 @@ implementation 'dev.thunderid:compose:0.1.0' ``` ::: -## Configure a Callback URL Scheme - - redirects back to your app after sign-in and sign-out using a custom URL scheme. You need to register this scheme in two places. - -**1. Register the scheme in your app** - -Open `AndroidManifest.xml` and add an intent filter with your scheme to your launcher activity. Use `singleTask` launch mode so the redirect reuses the existing activity instance instead of creating a new one. - -```xml title="AndroidManifest.xml" showLineNumbers - - - - - - - - - - - - - - -``` - -**2. Register the redirect URI in the console** - -In the console, open your registered application and add the following as an **Allowed Redirect URI**: - -```text -dev.thunderid.quickstart://callback -``` - -Add the same value as an **Allowed Post-Logout Redirect URI**: - -```text -dev.thunderid.quickstart://logout -``` - ## Initialize the SDK Open your `MainActivity` and wrap the content you pass to `setContent` with the `ThunderIDProvider` composable. This provides `ThunderIDState` to all child composables through `LocalThunderID`. @@ -167,10 +129,7 @@ class MainActivity : ComponentActivity() { ThunderIDProvider( config = ThunderIDConfig( baseUrl = "https://localhost:8090", - clientId = "", scopes = listOf("openid", "profile", "email"), - afterSignInUrl = "dev.thunderid.quickstart://callback", - afterSignOutUrl = "dev.thunderid.quickstart://logout", applicationId = "" ) ) { @@ -183,7 +142,7 @@ class MainActivity : ComponentActivity() { ``` :::warning Configuration -Replace `` with the **Client ID** and `` with the **Application ID** from your application settings. +Replace `` with the **Application ID** from your application settings. ::: ### Configuration Parameters @@ -191,11 +150,8 @@ Replace `` with the **Client ID** and `` wi | Parameter | Description | |-----------|-------------| | `baseUrl` | Your instance URL. Must use HTTPS. | -| `clientId` | The Client ID from your application | | `scopes` | OAuth 2.0 scopes to request. Include `"openid"` at minimum. | -| `afterSignInUrl` | The redirect URI to return to after sign-in | -| `afterSignOutUrl` | The redirect URI to return to after sign-out | -| `applicationId` | The Application ID used for embedded (app-native) sign-in flows | +| `applicationId` | The Application ID used for the app-native sign-in and sign-up flows | ## Add Sign-In and Sign-Out diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/flutter.mdx b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/flutter.mdx index 162fee924a..590ece4a16 100644 --- a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/flutter.mdx +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/flutter.mdx @@ -65,7 +65,11 @@ Once it's running, the console is available at [https://localhost:8090/console]( 3. Under **Application Type**, select **Mobile App**. 4. Enter a name (e.g. `My Flutter App`) and create an application. The rest of the settings can stay at their defaults. -5. Copy the **Client ID** from the **General** tab. You'll add the redirect URI in the next step after configuring your URL scheme. +5. Copy the **Application ID** from the **General** tab, under **Quick Copy**. + +:::info App-Native Authentication +This quickstart uses app-native authentication through the Flow Execution API. The Flutter SDK renders the sign-in and sign-up forms natively in your app, so you only need the **Application ID**, no OAuth 2.0 Client ID and no redirect URI. +::: ## Create a Flutter App @@ -95,55 +99,6 @@ Then install it: flutter pub get ``` -## Configure a Callback URL Scheme - - redirects back to your app after sign-in and sign-out using a custom URL scheme. You need to register this scheme on both platforms. - -### iOS - -Open `ios/Runner/Info.plist` and add a URL scheme under **URL Types**: - -```xml title="ios/Runner/Info.plist" -CFBundleURLTypes - - - CFBundleURLSchemes - - dev.thunderid.app - - - -``` - -### Android - -Open `android/app/src/main/AndroidManifest.xml` and add an intent filter inside your `` tag: - -```xml title="android/app/src/main/AndroidManifest.xml" - - - - - - -``` - -### Register the URIs in the console - -In the console, open your registered application and add the following as an **Allowed Redirect URI**: - -```text -dev.thunderid.app://callback -``` - -Add the same value as an **Allowed Post-Logout Redirect URI**: - -```text -dev.thunderid.app://logout -``` - ## Initialize the SDK Wrap your root widget with `ThunderIDProvider` in `lib/main.dart`: @@ -157,10 +112,7 @@ void main() { ThunderIDProvider( config: ThunderIDConfig( baseUrl: 'https://localhost:8090', - clientId: '', scopes: const ['openid', 'profile', 'email'], - afterSignInUrl: 'dev.thunderid.app://callback', - afterSignOutUrl: 'dev.thunderid.app://logout', applicationId: '', ), child: const MyApp(), @@ -181,7 +133,7 @@ class MyApp extends StatelessWidget { ``` :::warning Configuration -Replace `` with the **Client ID** and `` with the **Application ID** from your application settings. +Replace `` with the **Application ID** from your application settings. ::: ### Configuration Parameters @@ -189,11 +141,8 @@ Replace `` with the **Client ID** and `` wi | Parameter | Description | |-----------|-------------| | `baseUrl` | Your instance URL. Must use HTTPS. | -| `clientId` | The Client ID from your application | | `scopes` | OAuth 2.0 scopes to request. Include `'openid'` at minimum. | -| `afterSignInUrl` | The redirect URI to return to after sign-in | -| `afterSignOutUrl` | The redirect URI to return to after sign-out | -| `applicationId` | The Application ID used for embedded (app-native) sign-in flows | +| `applicationId` | The Application ID used for the app-native sign-in and sign-up flows | ## Add Sign-In and Sign-Out diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/ios.mdx b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/ios.mdx index 8518cc6925..29d9f15e4a 100644 --- a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/ios.mdx +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/ios.mdx @@ -65,7 +65,11 @@ Once it's running, the console is available at [https://localhost:8090/console]( 3. Under **Application Type**, select **Mobile App**. 4. Enter a name (e.g. `My iOS App`) and create an application. The rest of the settings can stay at their defaults. -5. Copy the **Client ID** from the **General** tab. You'll add the redirect URI in the next step after configuring your URL scheme. +5. Copy the **Application ID** from the **General** tab, under **Quick Copy**. + +:::info App-Native Authentication +This quickstart uses app-native authentication through the Flow Execution API. The iOS SDK renders the sign-in and sign-up forms natively in your app, so you only need the **Application ID**, no OAuth 2.0 Client ID and no redirect URI. +::: ## Create an iOS App @@ -93,40 +97,6 @@ pod 'ThunderIDSwiftUI' ``` ::: -## Configure a Callback URL Scheme - - redirects back to your app after sign-in and sign-out using a custom URL scheme. You need to register this scheme in two places. - -**1. Register the scheme in your app** - -Open your app's `Info.plist` and add a URL scheme under **URL Types**. For example: - -```xml -CFBundleURLTypes - - - CFBundleURLSchemes - - io.thunderid.b2c - - - -``` - -**2. Register the redirect URI in the console** - -In the console, open your registered application and add the following as an **Allowed Redirect URI**: - -```text -io.thunderid.b2c://callback -``` - -Add the same value as an **Allowed Post-Logout Redirect URI**: - -```text -io.thunderid.b2c://logout -``` - ## Initialize the SDK Open your app's entry point (the file that conforms to `App`) and apply the `.thunderIDProvider(config:)` modifier to your root view. This injects a `ThunderIDState` environment object into all child views. @@ -142,10 +112,7 @@ struct MyApp: App { ContentView() .thunderIDProvider(config: ThunderIDConfig( baseUrl: "https://localhost:8090", - clientId: "", scopes: ["openid", "profile", "email"], - afterSignInUrl: "io.thunderid.b2c://callback", - afterSignOutUrl: "io.thunderid.b2c://logout", applicationId: "" )) } @@ -154,7 +121,7 @@ struct MyApp: App { ``` :::warning Configuration -Replace `` with the **Client ID** and `` with the **Application ID** from your application settings. +Replace `` with the **Application ID** from your application settings. ::: ### Configuration Parameters @@ -162,11 +129,8 @@ Replace `` with the **Client ID** and `` wi | Parameter | Description | |-----------|-------------| | `baseUrl` | Your instance URL. Must use HTTPS. | -| `clientId` | The Client ID from your application | | `scopes` | OAuth 2.0 scopes to request. Include `"openid"` at minimum. | -| `afterSignInUrl` | The redirect URI to return to after sign-in | -| `afterSignOutUrl` | The redirect URI to return to after sign-out | -| `applicationId` | The Application ID used for embedded (app-native) sign-in flows | +| `applicationId` | The Application ID used for the app-native sign-in and sign-up flows | ## Add Sign-In and Sign-Out diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/node.mdx b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/node.mdx index d7100d7911..0875c9a4b9 100644 --- a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/node.mdx +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/node.mdx @@ -4,7 +4,7 @@ title: Node.js Quickstart docType: quickstart sidebar_position: 5 persona: app -description: Add {{ProductName}} authentication to a vanilla Node.js application using the @thunderid/node SDK. +description: Authenticate a Node.js service to {{ProductName}} as itself using the OAuth 2.0 client_credentials grant with the @thunderid/node SDK. --- import { @@ -20,7 +20,7 @@ import { # Node.js Quickstart -Use this guide to add authentication to a vanilla Node.js application using the `@thunderid/node` SDK and the built-in `http` module. No framework required. +Use this guide to authenticate a Node.js service to using the `@thunderid/node` SDK. Unlike the other quickstarts in this section, there's no user and no browser sign-in: the service authenticates as **itself** with the OAuth 2.0 `client_credentials` grant, then uses the resulting access token to call another piece of business logic. Use this pattern for background jobs, cron tasks, or one backend service calling another, where there's no human in the loop to redirect through a login page. @@ -28,18 +28,22 @@ Use this guide to add authentication to a vanilla Node.js applic }>Create a Node.js project }>Install the @thunderid/node package -}>Add working sign-in and sign-out routes -}>Protect routes and display the signed-in user's profile +}>Authenticate a service with the client_credentials grant +}>Use the resulting access token to call business logic ## Prerequisites -}>About 15 minutes +}>About 10 minutes }>Node.js 18+ installed on your system }>npm, yarn, or pnpm }>Your preferred code editor +:::tip Example Source Code +Check out the complete Node.js Quickstart Sample in the repository. +::: + ## Run @@ -50,7 +54,9 @@ Start a local instance. Pick the method that works best for you: Once it's running, the console is available at [https://localhost:8090/console](https://localhost:8090/console). -## Create an Application +## Create an Agent + +Agents are 's machine identities, distinct from user-facing applications. A background service like this one authenticates as an agent, not as an application. 1. Sign in to the Console. @@ -58,12 +64,11 @@ Once it's running, the console is available at [https://localhost:8090/console]( If you used the default setup, sign in to the Console as `admin` with the password generated during setup and printed to the setup output (unless you supplied your own). ::: -2. Navigate to **Applications**, and click **Add Application**. - -3. Under **Technology**, select **Node.js**. -4. Enter a name (e.g. `My Node.js App`) and create an application. The rest of the settings can stay at their defaults. -5. Copy both the **Client ID** and **Client Secret** from the window that pops up. The Client ID can also be found in the **General** tab. -6. Under **General**, add `http://localhost:3000/callback` to the list of **Authorized Redirect URIs**. +2. Navigate to **Agents** and click **Add Agent**. +3. Enter an **Agent name** (e.g. `node-service-quickstart`) and select an **Owner**, then click **Create agent**. +4. displays the agent's **Client Secret** once. Copy it now; it cannot be retrieved again. +5. Open the agent's **Advanced Settings** tab, enable the `client_credentials` grant type, and set the **Client authentication method** to `client_secret_basic`. +6. Copy the **Client ID** from the **General** tab. ## Create a Node.js Project @@ -71,20 +76,20 @@ Initialize a new Node.js project: - mkdir my-node-app - cd my-node-app + mkdir my-node-service + cd my-node-service npm init -y - mkdir my-node-app - cd my-node-app + mkdir my-node-service + cd my-node-service yarn init -y - mkdir my-node-app - cd my-node-app + mkdir my-node-service + cd my-node-service pnpm init @@ -107,309 +112,92 @@ Install the Node.js SDK: -## Initialize the Client +## Authenticate as the Service -Create an `index.js` file and initialize the `ThunderIDNodeClient` with your application credentials: +Create an `index.mjs` file and initialize `ThunderIDNodeClient` with `grantType: 'client_credentials'`. With that set, `getAccessToken()` authenticates as the service itself, no session and no sign-in, and transparently fetches, caches, and refreshes the token. -```js title="index.js" showLineNumbers -const http = require('http'); -const { URL } = require('url'); -const { randomUUID } = require('crypto'); -const { ThunderIDNodeClient } = require('@thunderid/node'); +```js title="index.mjs" showLineNumbers +import { ThunderIDNodeClient } from '@thunderid/node'; -const PORT = 3000; -const SESSION_COOKIE = 'tid_session'; +const client = new ThunderIDNodeClient(); -const auth = new ThunderIDNodeClient(); +await client.initialize({ + baseUrl: 'https://localhost:8090', + clientId: '', + clientSecret: '', + grantType: 'client_credentials', +}); -function getSessionId(req) { - const cookieHeader = req.headers.cookie ?? ''; - for (const part of cookieHeader.split(';')) { - const [name, value] = part.trim().split('='); - if (name === SESSION_COOKIE) return decodeURIComponent(value); - } - return null; -} - -async function main() { - await auth.initialize({ - clientId: '', - clientSecret: '', - baseUrl: 'https://localhost:8090', - afterSignInUrl: 'http://localhost:3000/callback', - afterSignOutUrl: 'http://localhost:3000', - }); - - const server = http.createServer(async (req, res) => { - // routes added in the next step - }); - - server.listen(PORT, () => { - console.log(`Server running on http://localhost:${PORT}`); - }); -} +const accessToken = await client.getAccessToken(); +const { scope } = await client.decodeJwtToken(accessToken); -main(); +console.log(`Authenticated with scope: ${scope}`); ``` :::warning Configuration -Replace `` and `` with the values from your application. Set the authorized redirect URL in your application settings to `http://localhost:3000/callback`. +Replace `` and `` with the **Client ID** and **Client Secret** from your agent. ::: ### Configuration Parameters | Parameter | Description | |-----------|-------------| -| `clientId` | The Client ID from your application | -| `clientSecret` | The Client Secret from your application | | `baseUrl` | Your instance URL (e.g., `https://localhost:8090`) | -| `afterSignInUrl` | The callback URL redirects to after sign-in | -| `afterSignOutUrl` | The URL to redirect to after sign-out | - -## Add Sign-In and Sign-Out Routes - -The `signIn` method works in two phases: it first redirects the user to , then handles the authorization code on the callback. Session state is tied to a session ID stored in a cookie. - -Replace the `// routes added in the next step` comment with: - -```js title="index.js" showLineNumbers - const url = new URL(req.url, `http://localhost:${PORT}`); - - try { - if (url.pathname === '/login') { - let sessionId = getSessionId(req); - const extraHeaders = {}; - if (!sessionId) { - sessionId = randomUUID(); - extraHeaders['Set-Cookie'] = - `${SESSION_COOKIE}=${sessionId}; HttpOnly; SameSite=Lax; Path=/`; - } - await auth.signIn((authUrl) => { - res.writeHead(302, { ...extraHeaders, Location: authUrl }); - res.end(); - }, sessionId); - - } else if (url.pathname === '/callback') { - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - const sessionState = url.searchParams.get('session_state'); - const sessionId = getSessionId(req); - - if (!sessionId || !code || !state) { - res.writeHead(400); - return res.end('Bad request'); - } - - await auth.signIn(() => {}, sessionId, code, sessionState, state); - res.writeHead(302, { Location: '/profile' }); - res.end(); - - } else if (url.pathname === '/logout') { - const sessionId = getSessionId(req); - if (!sessionId) { - res.writeHead(302, { Location: '/' }); - return res.end(); - } - const signOutUrl = await auth.signOut(sessionId); - res.writeHead(302, { - Location: signOutUrl, - 'Set-Cookie': `${SESSION_COOKIE}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`, - }); - res.end(); - - } - } catch { - res.writeHead(500); - res.end('Internal server error'); - } -``` +| `clientId` | The Client ID from your agent | +| `clientSecret` | The Client Secret from your agent | +| `grantType` | Set to `'client_credentials'` to authenticate the service as itself, with no user and no browser redirect | -**How the sign-in flow works:** - -1. `GET /login`: generates a session ID, stores it in a cookie, and calls `signIn` with an `authUrlCallback`. The callback receives the authorization URL and redirects the user's browser there. -2. `GET /callback`: redirects back with `code` and `state` query parameters. Calling `signIn` again with those values exchanges the code for tokens and stores the session. -3. `GET /logout`: calls `signOut` to get the OIDC end-session URL, clears the local cookie, then redirects the browser to complete the logout at . - -## Protect a Route and Display User Info - -Use `isSignedIn` to guard routes and `getUser` to retrieve the authenticated user's profile. Add these inside the same `try` block, before the closing `}`: - -```js title="index.js" showLineNumbers - if (url.pathname === '/') { - const sessionId = getSessionId(req); - const signedIn = sessionId && (await auth.isSignedIn(sessionId)); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(signedIn - ? 'View profile | Sign out' - : 'Sign in' - ); - - } else if (url.pathname === '/profile') { - const sessionId = getSessionId(req); - if (!sessionId || !(await auth.isSignedIn(sessionId))) { - res.writeHead(302, { Location: '/login' }); - return res.end(); - } - const user = await auth.getUser(sessionId); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(` -

Welcome, ${user.name || user.username}!

-

Email: ${user.email}

-

First name: ${user.given_name}

-

Last name: ${user.family_name}

- Sign out - `); - - } else if (url.pathname === '/login') { -``` +## Call Business Logic With the Token -## Complete index.js +A real backend keeps authentication in its own module instead of scattering it across business logic. Update `index.mjs` so the rest of the app asks for a token only when it needs one, instead of handling `getAccessToken()` and the `Authorization` header directly: -Here is the full file for reference: +```js title="index.mjs" showLineNumbers +import { ThunderIDNodeClient } from '@thunderid/node'; -```js title="index.js" showLineNumbers -const http = require('http'); -const { URL } = require('url'); -const { randomUUID } = require('crypto'); -const { ThunderIDNodeClient } = require('@thunderid/node'); +const client = new ThunderIDNodeClient(); -const PORT = 3000; -const SESSION_COOKIE = 'tid_session'; +await client.initialize({ + baseUrl: 'https://localhost:8090', + clientId: '', + clientSecret: '', + grantType: 'client_credentials', +}); -const auth = new ThunderIDNodeClient(); +async function getStock(sku) { + const accessToken = await client.getAccessToken(); -function getSessionId(req) { - const cookieHeader = req.headers.cookie ?? ''; - for (const part of cookieHeader.split(';')) { - const [name, value] = part.trim().split('='); - if (name === SESSION_COOKIE) return decodeURIComponent(value); - } - return null; -} + // A real backend would attach this as `Authorization: Bearer ${accessToken}` + // on a request to a separate inventory service. This quickstart just proves + // the token was obtained before answering. + console.log(`Requesting ${sku} with a valid access token`); -async function main() { - await auth.initialize({ - clientId: '', - clientSecret: '', - baseUrl: 'https://localhost:8090', - afterSignInUrl: 'http://localhost:3000/callback', - afterSignOutUrl: 'http://localhost:3000', - }); - - const server = http.createServer(async (req, res) => { - const url = new URL(req.url, `http://localhost:${PORT}`); - - try { - if (url.pathname === '/') { - const sessionId = getSessionId(req); - const signedIn = sessionId && (await auth.isSignedIn(sessionId)); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(signedIn - ? 'View profile | Sign out' - : 'Sign in' - ); - - } else if (url.pathname === '/profile') { - const sessionId = getSessionId(req); - if (!sessionId || !(await auth.isSignedIn(sessionId))) { - res.writeHead(302, { Location: '/login' }); - return res.end(); - } - const user = await auth.getUser(sessionId); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(` -

Welcome, ${user.name || user.username}!

-

Email: ${user.email}

-

First name: ${user.given_name}

-

Last name: ${user.family_name}

- Sign out - `); - - } else if (url.pathname === '/login') { - let sessionId = getSessionId(req); - const extraHeaders = {}; - if (!sessionId) { - sessionId = randomUUID(); - extraHeaders['Set-Cookie'] = - `${SESSION_COOKIE}=${sessionId}; HttpOnly; SameSite=Lax; Path=/`; - } - await auth.signIn((authUrl) => { - res.writeHead(302, { ...extraHeaders, Location: authUrl }); - res.end(); - }, sessionId); - - } else if (url.pathname === '/callback') { - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - const sessionState = url.searchParams.get('session_state'); - const sessionId = getSessionId(req); - - if (!sessionId || !code || !state) { - res.writeHead(400); - return res.end('Bad request'); - } - - await auth.signIn(() => {}, sessionId, code, sessionState, state); - res.writeHead(302, { Location: '/profile' }); - res.end(); - - } else if (url.pathname === '/logout') { - const sessionId = getSessionId(req); - if (!sessionId) { - res.writeHead(302, { Location: '/' }); - return res.end(); - } - const signOutUrl = await auth.signOut(sessionId); - res.writeHead(302, { - Location: signOutUrl, - 'Set-Cookie': `${SESSION_COOKIE}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`, - }); - res.end(); - - } else { - res.writeHead(404); - res.end('Not found'); - } - } catch { - res.writeHead(500); - res.end('Internal server error'); - } - }); - - server.listen(PORT, () => { - console.log(`Server running on http://localhost:${PORT}`); - }); + return { sku, inStock: true }; } -main(); +const item = await getStock('SKU-100'); +console.log(item); ``` -## Run Your App +## Run Your Service -Start the server: +Start the script: - node index.js + node index.mjs - yarn node index.js + yarn node index.mjs - pnpm node index.js + pnpm node index.mjs -Open [http://localhost:3000](http://localhost:3000). - -:::note Test credentials -You'll need a user to sign in with. If you haven't created one yet, open , navigate to **Users**, and add a test user with an email and password. -::: - :::tip Success -You should see the sign-in link. Click it to be redirected to the -hosted sign-in page. After authenticating with your test user, you'll return to the `/profile` route with your user profile displayed. +The script authenticates once, prints the scope it received, then calls `getStock()` and prints the result before exiting. :::
@@ -418,11 +206,11 @@ You should see the sign-in link. Click it to be redirected to the - +
-
Upgrade the sign-in flow
-
Add MFA, passkeys, or social login to your flow without changing your route wiring.
-
Flow Designer →
+
Agent Authentication
+
Request specific scopes, narrow tokens to a resource server, and troubleshoot the token endpoint directly.
+
Read the Guide →
diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/android/redirect-based.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/android/redirect-based.txt new file mode 100644 index 0000000000..2affef1349 --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/android/redirect-based.txt @@ -0,0 +1,36 @@ +# Integrate {{productName}} Authentication in Android Application + +## Context +I have an Android application (Kotlin, Jetpack Compose) and I want to integrate {{productName}}'s authentication system using the {{productName}} Android SDK, with sign-in and sign-up forms rendered natively in the app (app-native authentication). + +## Requirements +- Use the `dev.thunderid:compose` Gradle dependency for authentication +- Use app-native authentication through the Flow Execution API (no OAuth 2.0 client ID, no redirect URI) +- Implement sign-in and sign-up with the prebuilt `SignIn`/`SignUp` composables +- Guard content with the `SignedIn`/`SignedOut` composables +- Display the signed-in user's profile information +- Implement sign-out with the prebuilt `SignOutButton` composable + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: dev.thunderid:compose + +## IMPORTANT Configuration Rules +- Add the {{productName}} Maven repository (`https://maven.thunderid.dev/releases`) to `settings.gradle.kts` before adding the dependency +- Wrap the content passed to `setContent` with the `ThunderIDProvider` composable, configured with `ThunderIDConfig(baseUrl, scopes, applicationId)` +- Access authentication state via `LocalThunderID.current`, not a separate hook or singleton +- Required scopes: at minimum `"openid"` +- Use `applicationId`, NOT a client ID or redirect URI — this SDK only supports app-native authentication + +## Implementation Steps +1. Create a new Android Studio project using the Empty Activity template with Kotlin and Jetpack Compose +2. Add the {{productName}} Maven repository to `settings.gradle.kts` +3. Add the `dev.thunderid:compose` dependency to `build.gradle.kts` +4. Wrap your `setContent` content with `ThunderIDProvider`, configured with `baseUrl`, `scopes`, and `applicationId` +5. Build a root composable that checks `thunder.isInitialized` and renders a `SignedIn`/fallback split +6. Build an auth screen using the `SignIn` composable, passing the `applicationId` +7. Build a home screen that reads `LocalThunderID.current.user` to display the signed-in user's name and email, with a `SignOutButton` +8. Run the app on an API 24+ emulator or device + +Please provide complete, working code with proper configuration for {{productName}} authentication using the {{productName}} Android SDK for Jetpack Compose. diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/browser/embedded.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/browser/embedded.txt new file mode 100644 index 0000000000..b3a5777548 --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/browser/embedded.txt @@ -0,0 +1,36 @@ +# Integrate {{productName}} Authentication in Vanilla JavaScript Application (Custom UI Mode) + +## Context +I have a vanilla JavaScript application and I want to integrate {{productName}}'s authentication system using the ThunderID Browser SDK's embedded flow functions, rendering my own sign-in form instead of redirecting to {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/browser SDK for authentication +- Use `executeEmbeddedSignInFlow` (re-exported by @thunderid/browser from the core @thunderid/javascript client) to drive the sign-in flow directly, instead of `ThunderIDBrowserClient.signIn()` (which is redirect-only) +- Render your own sign-in form driven by the flow response's `components` array +- Display signed-in user's profile information +- Handle authentication state + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: @thunderid/browser + +## IMPORTANT Configuration Rules +- `@thunderid/browser` has NO bundled ``-style DOM component — it is framework-agnostic with no rendering layer. Build the form yourself from the flow response +- Call `executeEmbeddedSignInFlow({applicationId, baseUrl, flowType: 'AUTHENTICATION'})` to start the flow +- Each step returns `components` describing the fields to render next (e.g. identifier, password, OTP); render inputs for those fields, then call `executeEmbeddedSignInFlow` again with the same `executionId` and the user's submitted `inputs` to advance the flow +- Repeat until the flow response indicates completion (tokens/session issued) +- NEVER call `ThunderIDBrowserClient.signIn()` or navigate to a {{productName}}-hosted URL in this mode — that redirects the browser instead of using the embedded flow +- Still use `ThunderIDBrowserClient.initialize(config)` for session/token management (`isSignedIn()`, `getUser()`, `getAccessToken()`, `signOut()`) once the embedded flow completes + +## Implementation Steps +1. Create a vanilla JS app using Vite by running: `npm create vite@latest js-demo -- --template vanilla` +2. Navigate into the project directory: `cd js-demo` +3. Install dependencies: `npm install` +4. Install @thunderid/browser package: `npm install @thunderid/browser` +5. Create src/auth.js to initialize ThunderIDBrowserClient with applicationId and baseUrl +6. Create a sign-in form in src/main.js; on submit, call `executeEmbeddedSignInFlow` and render whatever `components` the response specifies for the next step +7. Once the flow completes, use `isSignedIn()`/`getUser()` to show the signed-in profile, and `signOut()` for sign-out +8. Run the development server: `npm run dev` + +Please provide complete, working code for {{productName}} authentication using the ThunderID Browser SDK's embedded flow functions, with a custom sign-in form (not a redirect). diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/browser/redirect-based.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/browser/redirect-based.txt new file mode 100644 index 0000000000..f5022077b9 --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/browser/redirect-based.txt @@ -0,0 +1,57 @@ +# Integrate {{productName}} Authentication in Vanilla JavaScript Application + +## Context +I have a vanilla JavaScript application and I want to integrate {{productName}}'s authentication system using the ThunderID Browser SDK with {{productName}}-hosted login pages. + +## Requirements +- Use @thunderid/browser SDK for authentication +- Configure {{productName}}-hosted login pages +- Implement sign-in and sign-out functionality +- Display signed-in user's profile information +- Handle authentication state + +## Configuration +- **Client ID**: {{clientId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: @thunderid/browser + +## IMPORTANT Configuration Rules +- Create a ThunderIDBrowserClient instance and call initialize() with a config object +- Required config properties: `clientId` and `baseUrl` +- Optional config properties: `afterSignInUrl`, `afterSignOutUrl`, `scopes` (string array), `storage` +- Storage options: 'sessionStorage' (default), 'localStorage', 'browserMemory' +- NEVER use property names like `signInRedirectURL` or `signOutRedirectURL` + +## Available SDK APIs + +### ThunderIDBrowserClient +- `initialize(config)` - Initialize the client with configuration +- `signIn()` - Redirect to {{productName}} sign-in page +- `signOut()` - Sign out and clear session +- `isSignedIn()` - Check if user is signed in (returns Promise) +- `getUser()` - Get authenticated user profile (returns Promise) +- `getAccessToken()` - Get current access token +- `getIdToken()` - Get ID token +- `getDecodedIdToken()` - Get decoded ID token claims +- `httpRequest(config)` - Make authenticated HTTP request +- `on(hook, callback)` - Register event callbacks (Hooks.SignIn, Hooks.SignOut, etc.) + +### User Object Properties +- `displayName` - User's display name +- `username` - Username +- `email` - Email address +- `given_name` - First name +- `family_name` - Last name +- `picture` - Profile picture URL + +## Implementation Steps +1. Create a vanilla JS app using Vite by running: `npm create vite@latest js-demo -- --template vanilla` +2. Navigate into the project directory: `cd js-demo` +3. Install dependencies: `npm install` +4. Install @thunderid/browser package: `npm install @thunderid/browser` +5. Create src/auth.js to initialize ThunderIDBrowserClient with clientId and baseUrl +6. Update src/main.js to check isSignedIn(), show sign-in button or user profile +7. Add event listeners for sign-in and sign-out buttons +8. Run the development server: `npm run dev` + +Please provide complete, working code with proper configuration for {{productName}} authentication using the ThunderID Browser SDK. diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/express/embedded.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/express/embedded.txt new file mode 100644 index 0000000000..9ca1b63f34 --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/express/embedded.txt @@ -0,0 +1,47 @@ +# Integrate {{productName}} Authentication in an Express Application (Custom UI / Embedded Mode) + +## Context +I have an Express application and I want to integrate {{productName}} authentication using the ThunderID Express SDK in embedded (app-native) mode, rendering my own sign-in form instead of redirecting to {{productName}}-hosted pages. + +## Requirements +- Use `@thunderid/express` in a Node.js + Express application +- Use `cookie-parser` and `express.json()` middleware +- Configure ThunderID middleware with `baseUrl`, `applicationId`, and `mode: 'embedded'` +- Add a `/flow/sign-in` route using `handleFlow()` that proxies each step of the flow to the Flow Execution API +- Serve a `/login` page with a plain HTML form that POSTs to `/flow/sign-in` and re-renders whatever fields the flow response asks for next +- Add `/logout` with `handleSignOut()` +- Protect a route (`/protected`) using `protect((res) => res.redirect('/login'))` +- Add a `/me` route that returns the authenticated user profile as JSON +- Keep the code minimal, production-lean, and fully runnable + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **App URL**: http://localhost:3000 +- **Sign-In Page**: `/login` +- **Flow Route**: `/flow/sign-in` +- **Logout Callback Route**: `/logout` +- **SDK**: @thunderid/express + +## Important Rules +- Use CommonJS syntax (`require`) in `index.js` +- `@thunderid/express` has NO bundled sign-in UI component — this is a server-side SDK. `mode: 'embedded'` only changes what `handleFlow()` proxies to; you must write the actual HTML/JS form yourself +- Set `mode: 'embedded'` in the `thunderID({...})` middleware config (defaults to `'redirect'` if omitted) +- `handleFlow()` calls the Flow Execution API on each POST and returns the flow's JSON step (`executionId`, `challengeToken`, `authId`, `components`) — render whatever `components` the response contains as form fields, and POST the user's input back to the same route with the returned `executionId` +- Do not invent unsupported SDK APIs or custom wrappers +- Do not use `clientId`/`clientSecret` in this mode — embedded mode is driven by `applicationId`, not an OAuth2 client + +## Implementation Steps +1. Create a new project and install `express` and `cookie-parser` +2. Install `@thunderid/express` +3. Add `index.js` with `thunderID({ applicationId, baseUrl, mode: 'embedded' })` middleware +4. Add `/login` route serving a minimal HTML form, and `/flow/sign-in` using `handleFlow()` +5. Add `/logout`, `/protected`, and `/me` routes +6. Start the server with `node index.js` +7. Validate the flow by opening `/login`, submitting the form, then `/protected` and `/me` + +Please provide: +- The exact terminal commands +- A complete `index.js` file +- A minimal HTML sign-in form driven by the flow's `components` response (not a redirect) +- A short verification checklist for sign-in, sign-out, and protected route behavior diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/express/redirect-based.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/express/redirect-based.txt new file mode 100644 index 0000000000..e2aeaea4d6 --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/express/redirect-based.txt @@ -0,0 +1,42 @@ +# Integrate {{productName}} Authentication in an Express Application (Inbuilt Mode) + +## Context +I have an Express application and I want to integrate {{productName}} authentication using the ThunderID Express SDK with {{productName}}-hosted sign-in pages. + +## Requirements +- Use `@thunderid/express` in a Node.js + Express application +- Use `cookie-parser` and `express.json()` middleware +- Configure ThunderID middleware with `baseUrl`, `clientId`, `clientSecret`, `afterSignInUrl`, and `afterSignOutUrl` +- Implement `/login` with `handleSignIn()` and `/logout` with `handleSignOut()` +- Protect a route (`/protected`) using `protect((res) => res.redirect('/login'))` +- Add a `/me` route that returns the authenticated user profile as JSON +- Keep the code minimal, production-lean, and fully runnable + +## Configuration +- **Client ID**: {{clientId}} +- **Client Secret**: `` +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **App URL**: http://localhost:3000 +- **Login Callback Route**: `/login` +- **Logout Callback Route**: `/logout` +- **SDK**: @thunderid/express + +## Important Rules +- Use CommonJS syntax (`require`) in `index.js` +- Use these SDK imports exactly: `thunderID`, `handleSignIn`, `handleSignOut`, `protect` +- Ensure redirect URL alignment: the app callback URL must be `http://localhost:3000/login`, and the post-logout redirect URL must be `http://localhost:3000/logout` +- Do not invent unsupported SDK APIs or custom wrappers +- Keep route names and behavior exactly as specified + +## Implementation Steps +1. Create a new project and install `express` and `cookie-parser` +2. Install `@thunderid/express` +3. Add `index.js` with ThunderID middleware and auth routes +4. Add `/protected` and `/me` routes with `protect()` +5. Start the server with `node index.js` +6. Validate the flow by opening `/protected`, then `/me` + +Please provide: +- The exact terminal commands +- A complete `index.js` file +- A short verification checklist for sign-in, sign-out, and protected route behavior diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/flutter/redirect-based.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/flutter/redirect-based.txt new file mode 100644 index 0000000000..bb4da78d3e --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/flutter/redirect-based.txt @@ -0,0 +1,34 @@ +# Integrate {{productName}} Authentication in Flutter Application + +## Context +I have a Flutter application (Dart) and I want to integrate {{productName}}'s authentication system using the `thunderid_flutter` package, with sign-in and sign-up forms rendered natively in the app (app-native authentication). + +## Requirements +- Use the `thunderid_flutter` package for authentication +- Use app-native authentication through the Flow Execution API (no OAuth 2.0 client ID, no redirect URI) +- Implement sign-in and sign-up with the prebuilt `SignIn`/`SignUp` widgets +- Route between an auth screen and a home screen based on `thunder.isSignedIn` +- Display the signed-in user's profile information +- Implement sign-out with the prebuilt `SignOutButton` widget + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: thunderid_flutter + +## IMPORTANT Configuration Rules +- Wrap your root widget with `ThunderIDProvider`, configured with `ThunderIDConfig(baseUrl, scopes, applicationId)` +- Access authentication state via `ThunderIDProvider.of(context)`, not a separate hook or singleton +- Required scopes: at minimum `'openid'` +- Use `applicationId`, NOT a client ID or redirect URI — this SDK only supports app-native authentication + +## Implementation Steps +1. Create a new Flutter project: `flutter create my_app` +2. Add `thunderid_flutter` to `pubspec.yaml` and run `flutter pub get` +3. Wrap your root widget with `ThunderIDProvider` in `lib/main.dart`, configured with `baseUrl`, `scopes`, and `applicationId` +4. Build a root screen that checks `thunder.initialized`/`thunder.isLoading` and routes to an auth or home screen based on `thunder.isSignedIn` +5. Build an auth screen using the `SignIn`/`SignUp` widgets, passing the `applicationId` +6. Build a home screen that reads `thunder.user` to display the signed-in user's name and email, with a `SignOutButton` +7. Run the app on an iOS simulator or Android device: `flutter run` + +Please provide complete, working code with proper configuration for {{productName}} authentication using the `thunderid_flutter` package. diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/ios/redirect-based.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/ios/redirect-based.txt new file mode 100644 index 0000000000..755f828913 --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/ios/redirect-based.txt @@ -0,0 +1,34 @@ +# Integrate {{productName}} Authentication in iOS Application + +## Context +I have an iOS application (Swift, SwiftUI) and I want to integrate {{productName}}'s authentication system using the {{productName}} iOS SDK, with sign-in and sign-up forms rendered natively in the app (app-native authentication). + +## Requirements +- Use the `ThunderIDSwiftUI` Swift package for authentication +- Use app-native authentication through the Flow Execution API (no OAuth 2.0 client ID, no redirect URI) +- Implement sign-in and sign-up with the prebuilt `SignIn`/`SignUp` views +- Guard content with the `SignedIn`/`SignedOut` views +- Display the signed-in user's profile information +- Implement sign-out with the prebuilt `SignOutButton` view + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: ThunderIDSwiftUI (Swift Package Manager) or ThunderIDSwiftUI CocoaPod + +## IMPORTANT Configuration Rules +- Apply the `.thunderIDProvider(config:)` modifier to the root view in your app's entry point (the type conforming to `App`), configured with `ThunderIDConfig(baseUrl, scopes, applicationId)` +- Access authentication state via an injected `ThunderIDState` `@EnvironmentObject`, not a separate hook or singleton +- Required scopes: at minimum `"openid"` +- Use `applicationId`, NOT a client ID or redirect URI — this SDK only supports app-native authentication + +## Implementation Steps +1. Create a new Xcode project using the iOS > App template with SwiftUI and Swift +2. Add the `ThunderIDSwiftUI` package via File > Add Package Dependencies (or the `ThunderIDSwiftUI` CocoaPod) +3. Apply `.thunderIDProvider(config:)` to your root view, configured with `baseUrl`, `scopes`, and `applicationId` +4. Build a root view that checks `state.isInitialized` and renders a `SignedIn`/fallback split +5. Build an auth view using the `SignIn` view, passing the `applicationId` +6. Build a home view that reads `state.user` to display the signed-in user's name and email, with a `SignOutButton` +7. Run the app on an iOS 16+ simulator or device + +Please provide complete, working code with proper configuration for {{productName}} authentication using the {{productName}} iOS SDK for SwiftUI. diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nextjs/embedded.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nextjs/embedded.txt new file mode 100644 index 0000000000..cb7860e1cc --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nextjs/embedded.txt @@ -0,0 +1,41 @@ +# Integrate {{productName}} Authentication in Next.js Application (Custom UI Mode) + +## Context +I have a Next.js application (App Router) and I want to integrate {{productName}}'s authentication system using the {{productName}} Next.js SDK with a custom sign-in UI instead of {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/nextjs SDK for authentication +- Render sign-in on a custom route (not a redirect to {{productName}}-hosted pages) +- Use the App Router (not Pages Router) +- Implement sign-in and sign-out with the SDK's own components +- Add middleware for route protection and automatic token refresh +- Display signed-in user's profile information + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **Sign In Route**: /signin +- **SDK**: @thunderid/nextjs + +## IMPORTANT Configuration Rules +- Use environment variables for configuration (NOT props on the provider) +- Required env vars: NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_APPLICATION_ID, THUNDERID_SECRET, NODE_TLS_REJECT_UNAUTHORIZED=0 +- Import ThunderIDProvider from '@thunderid/nextjs/server' (NOT from '@thunderid/nextjs') +- Import middleware utilities from '@thunderid/nextjs/server' +- Import UI components (`SignedIn`, `SignedOut`, `SignIn`, `SignOutButton`, `UserDropdown`) from '@thunderid/nextjs' +- The `` component drives the Flow Execution API directly (not an OAuth redirect) — render it on your custom `app/signin/page.tsx` +- `` accepts an `onSuccess` callback (e.g. `router.push('/')`) and an `onError` callback +- NEVER redirect to a {{productName}}-hosted URL for sign-in in this mode + +## Implementation Steps +1. Create a Next.js app: npx create-next-app@latest nextjs-demo +2. Navigate into the project: cd nextjs-demo +3. Install @thunderid/nextjs: npm install @thunderid/nextjs +4. Create .env.local with NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_APPLICATION_ID, THUNDERID_SECRET, NODE_TLS_REJECT_UNAUTHORIZED=0 +5. Wrap root layout with from '@thunderid/nextjs/server' +6. Create app/signin/page.tsx rendering ` router.push('/')} />` and `` for the already-authenticated case +7. Create proxy.ts with thunderIDProxy and createRouteMatcher from '@thunderid/nextjs/server' for route protection +8. Add SignedIn, UserDropdown, SignedOut, SignOutButton components to other pages +9. Run: npm run dev + +Please provide complete, working code for {{productName}} authentication using the {{productName}} Next.js SDK, with a custom sign-in page rendering `` instead of a hosted-page redirect. diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nextjs/redirect-based.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nextjs/redirect-based.txt new file mode 100644 index 0000000000..4b5badc202 --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nextjs/redirect-based.txt @@ -0,0 +1,37 @@ +# Integrate {{productName}} Authentication in Next.js Application + +## Context +I have a Next.js application (App Router) and I want to integrate {{productName}}'s authentication system using the {{productName}} Next.js SDK with {{productName}}-hosted login pages. + +## Requirements +- Use @thunderid/nextjs SDK for authentication +- Configure {{productName}}-hosted login pages (not custom/embedded) +- Use the App Router (not Pages Router) +- Implement sign-in and sign-out with prebuilt components +- Add middleware for route protection and automatic token refresh +- Display signed-in user's profile information + +## Configuration +- **Client ID**: {{clientId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: @thunderid/nextjs + +## IMPORTANT Configuration Rules +- Use environment variables for configuration (NOT props on the provider) +- Required env vars: NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SECRET, NODE_TLS_REJECT_UNAUTHORIZED=0 +- Import ThunderIDProvider from '@thunderid/nextjs/server' (NOT from '@thunderid/nextjs') +- Import middleware utilities from '@thunderid/nextjs/server' +- Import UI components from '@thunderid/nextjs' +- The ThunderIDProvider handles the OAuth callback automatically — no manual callback route is needed + +## Implementation Steps +1. Create a Next.js app: npx create-next-app@latest nextjs-demo +2. Navigate into the project: cd nextjs-demo +3. Install @thunderid/nextjs: npm install @thunderid/nextjs +4. Create .env.local with NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SECRET, NODE_TLS_REJECT_UNAUTHORIZED=0 +5. Wrap root layout with from '@thunderid/nextjs/server' +6. Create proxy.ts with thunderIDProxy and createRouteMatcher from '@thunderid/nextjs/server' for route protection +7. Add SignedIn, UserDropdown, SignedOut, SignInButton components to pages +8. Run: npm run dev + +Please provide complete, working code with proper configuration for {{productName}} authentication using the {{productName}} Next.js SDK. diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/node/embedded.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/node/embedded.txt new file mode 100644 index 0000000000..5b4a8e0c31 --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/node/embedded.txt @@ -0,0 +1,46 @@ +# Integrate {{productName}} Authentication in a Node.js Application (Custom UI / Embedded Mode) + +## Context +I have a Node.js application and I want to integrate {{productName}} authentication using the @thunderid/node SDK's embedded flow functions and the built-in http module — no framework required — rendering my own sign-in form instead of redirecting to {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/node SDK with the built-in Node.js http module +- Use `executeEmbeddedSignInFlow` (re-exported by @thunderid/node from the core @thunderid/javascript client) to drive the sign-in flow directly, instead of `ThunderIDNodeClient.signIn()` (which is redirect-only) +- Implement a `/login` route that serves a minimal HTML form +- Implement a `/flow/sign-in` route that calls `executeEmbeddedSignInFlow` on each POST and returns/renders whatever `components` the flow response asks for next +- Implement a `/logout` route to sign out and clear the session cookie +- Protect the `/profile` route using `isSignedIn()` and display user info with `getUser()` +- Manage sessions using a session ID stored in an HttpOnly cookie +- Keep code minimal and fully runnable with CommonJS `require()` + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **App URL**: http://localhost:3000 +- **Sign-In Page**: /login +- **Flow Route**: /flow/sign-in +- **SDK**: @thunderid/node + +## Important Rules +- Use CommonJS syntax (require) in index.js +- `ThunderIDNodeClient` itself only implements the redirect flow — for embedded mode, call `executeEmbeddedSignInFlow({applicationId, flowType: 'AUTHENTICATION', ...})` directly, imported from `@thunderid/node` (or `@thunderid/javascript`) +- @thunderid/node has NO bundled sign-in UI — write the HTML/JS form yourself, rendering whatever fields the flow response's `components` array specifies +- Each flow step returns an `executionId`; POST the next step's user input back to `/flow/sign-in` along with that `executionId` until the flow completes +- Store the session ID in a cookie named 'tid_session' with HttpOnly and SameSite=Lax flags +- Use randomUUID() from the built-in 'crypto' module to generate session IDs +- Use isSignedIn(sessionId) to guard protected routes +- Use getUser(sessionId) to retrieve the authenticated user profile +- Use signOut(sessionId) to clear the local session + +## Implementation Steps +1. Create a new project: mkdir my-node-app && cd my-node-app && npm init -y +2. Install @thunderid/node: npm install @thunderid/node +3. Create index.js importing `executeEmbeddedSignInFlow` from @thunderid/node +4. Add /login route serving a minimal HTML form +5. Add /flow/sign-in route: call executeEmbeddedSignInFlow with the applicationId and the user's submitted inputs, and set the session cookie once the flow completes +6. Add /logout route: clear the session cookie +7. Add / route: show sign-in or profile link based on isSignedIn() +8. Add /profile route: guard with isSignedIn(), display user info from getUser() +9. Start the server: node index.js + +Please provide a complete, working index.js file with all routes, driven by the embedded flow functions (not a redirect). diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/node/redirect-based.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/node/redirect-based.txt new file mode 100644 index 0000000000..2e178b0b9a --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/node/redirect-based.txt @@ -0,0 +1,45 @@ +# Integrate {{productName}} Authentication in a Node.js Application + +## Context +I have a Node.js application and I want to integrate {{productName}} authentication using the @thunderid/node SDK and the built-in http module — no framework required. + +## Requirements +- Use @thunderid/node SDK with the built-in Node.js http module +- Initialize ThunderIDNodeClient with clientId, clientSecret, baseUrl, afterSignInUrl, afterSignOutUrl +- Implement /login route to start the sign-in flow (redirects to {{productName}}) +- Implement /callback route to handle the OAuth authorization code exchange +- Implement /logout route to sign out and clear the session cookie +- Protect the /profile route using isSignedIn() and display user info with getUser() +- Manage sessions using a session ID stored in an HttpOnly cookie +- Keep code minimal and fully runnable with CommonJS require() + +## Configuration +- **Client ID**: {{clientId}} +- **Client Secret**: `` +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **App URL**: http://localhost:3000 +- **Callback Route**: /callback +- **SDK**: @thunderid/node + +## Important Rules +- Use CommonJS syntax (require) in index.js +- Initialize ThunderIDNodeClient with auth.initialize({...}) before starting the server +- signIn() works in two phases: first call redirects the user (authUrlCallback), second call (with code+state) exchanges the token +- Store the session ID in a cookie named 'tid_session' with HttpOnly and SameSite=Lax flags +- Use randomUUID() from the built-in 'crypto' module to generate session IDs +- Use isSignedIn(sessionId) to guard protected routes +- Use getUser(sessionId) to retrieve the authenticated user profile +- Use signOut(sessionId) to get the OIDC end-session URL, then clear the local cookie and redirect + +## Implementation Steps +1. Create a new project: mkdir my-node-app && cd my-node-app && npm init -y +2. Install @thunderid/node: npm install @thunderid/node +3. Create index.js with ThunderIDNodeClient initialization +4. Add /login route: generate session ID cookie and redirect to {{productName}} auth URL +5. Add /callback route: exchange authorization code for tokens using signIn() +6. Add /logout route: call signOut() to get end-session URL, clear cookie, redirect +7. Add / route: show sign-in or profile link based on isSignedIn() +8. Add /profile route: guard with isSignedIn(), display user info from getUser() +9. Start the server: node index.js + +Please provide a complete, working index.js file with all routes and the ThunderIDNodeClient wired up correctly. diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nuxt/embedded.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nuxt/embedded.txt new file mode 100644 index 0000000000..2ef19c0dae --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nuxt/embedded.txt @@ -0,0 +1,44 @@ +# Integrate {{productName}} Authentication in Nuxt 3 Application (Custom UI Mode) + +## Context +I have a Nuxt 3 application and I want to integrate {{productName}}'s authentication system using the @thunderid/nuxt module with a custom sign-in UI instead of {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/nuxt module for authentication +- Register the module in nuxt.config.ts +- Configure via environment variables (no inline config) +- Wrap app.vue content with +- Render sign-in on a custom page using the module's own SignIn component (not a redirect to {{productName}}-hosted pages) +- Display signed-in user's profile information +- Optionally protect pages with the built-in thunderIDMiddleware + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **Sign In Page**: /signin (custom page) +- **SDK**: @thunderid/nuxt + +## IMPORTANT Configuration Rules +- Add '@thunderid/nuxt' to the modules array in nuxt.config.ts — no other config needed there +- All configuration is read from environment variables with NUXT_PUBLIC_ prefix for public values +- Required env vars: NUXT_PUBLIC_THUNDERID_BASE_URL, NUXT_PUBLIC_THUNDERID_APPLICATION_ID, THUNDERID_SESSION_SECRET (without the NUXT_PUBLIC_ prefix) +- Wrap with in app.vue +- All components (`SignedIn`, `SignedOut`, `SignIn`, `SignOutButton`, `User`) and composables are auto-imported — no manual import needed +- The `` component mirrors the Vue SDK's `SignIn`/`BaseSignIn` and drives the embedded (app-native) Flow Execution API directly — it replaces `window.location` navigation with Nuxt's `navigateTo` internally, so no manual redirect handling is needed +- Render `` inside `` on pages/signin.vue +- Protect pages by adding definePageMeta({ middleware: ['thunderIDMiddleware'] }) +- NEVER redirect to a {{productName}}-hosted URL for sign-in in this mode + +## Implementation Steps +1. Create a Nuxt 3 app: npx nuxi@latest init my-nuxt-app +2. Navigate into the project: cd my-nuxt-app && npm install +3. Install @thunderid/nuxt: npm install @thunderid/nuxt +4. Add '@thunderid/nuxt' to modules in nuxt.config.ts +5. Create .env with NUXT_PUBLIC_THUNDERID_BASE_URL, NUXT_PUBLIC_THUNDERID_APPLICATION_ID, THUNDERID_SESSION_SECRET +6. Wrap with in app.vue +7. Create pages/signin.vue rendering `` +8. Create pages/index.vue with SignedIn, SignOutButton, and User components +9. Optionally add definePageMeta({ middleware: ['thunderIDMiddleware'] }) to protected pages +10. Run: npm run dev + +Please provide complete, working code for {{productName}} authentication using the @thunderid/nuxt module, with a custom sign-in page rendering `` instead of a hosted-page redirect. diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nuxt/redirect-based.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nuxt/redirect-based.txt new file mode 100644 index 0000000000..dff4a6bed7 --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/nuxt/redirect-based.txt @@ -0,0 +1,42 @@ +# Integrate {{productName}} Authentication in Nuxt 3 Application + +## Context +I have a Nuxt 3 application and I want to integrate {{productName}}'s authentication system using the @thunderid/nuxt module with {{productName}}-hosted login pages. + +## Requirements +- Use @thunderid/nuxt module for authentication +- Register the module in nuxt.config.ts +- Configure via environment variables (no inline config) +- Wrap app.vue content with +- Implement sign-in and sign-out with auto-imported components +- Display signed-in user's profile information +- Optionally protect pages with the built-in thunderIDMiddleware + +## Configuration +- **Client ID**: {{clientId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **Callback URL**: http://localhost:3000/api/auth/callback (auto-registered by the module) +- **SDK**: @thunderid/nuxt + +## IMPORTANT Configuration Rules +- Add '@thunderid/nuxt' to the modules array in nuxt.config.ts — no other config needed there +- All configuration is read from environment variables with NUXT_PUBLIC_ prefix for public values +- Required env vars: NUXT_PUBLIC_THUNDERID_BASE_URL, NUXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SESSION_SECRET +- THUNDERID_CLIENT_SECRET and THUNDERID_SESSION_SECRET must NOT have the NUXT_PUBLIC_ prefix +- The /api/auth/callback route is auto-registered by the module — do NOT create it manually +- Wrap with in app.vue +- All components (SignedIn, SignedOut, SignInButton, SignOutButton, User) and composables are auto-imported +- Protect pages by adding definePageMeta({ middleware: ['thunderIDMiddleware'] }) + +## Implementation Steps +1. Create a Nuxt 3 app: npx nuxi@latest init my-nuxt-app +2. Navigate into the project: cd my-nuxt-app && npm install +3. Install @thunderid/nuxt: npm install @thunderid/nuxt +4. Add '@thunderid/nuxt' to modules in nuxt.config.ts +5. Create .env with NUXT_PUBLIC_THUNDERID_BASE_URL, NUXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SESSION_SECRET +6. Wrap with in app.vue +7. Create pages/index.vue with SignInButton, SignOutButton, SignedIn, SignedOut, and User components +8. Optionally add definePageMeta({ middleware: ['thunderIDMiddleware'] }) to protected pages +9. Run: npm run dev + +Please provide complete, working code with proper configuration for {{productName}} authentication using the @thunderid/nuxt module. diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/react/embedded.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/react/embedded.txt new file mode 100644 index 0000000000..af5c0ed6d2 --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/react/embedded.txt @@ -0,0 +1,45 @@ +# Integrate {{productName}} Authentication in React Application (Custom UI Mode) + +## Context +I have a React application and I want to integrate {{productName}}'s authentication system using the ThunderID React SDK with a custom sign-in UI instead of {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/react SDK for authentication +- Render sign-in on a custom route (not a redirect to {{productName}}-hosted pages) +- Use react-router for routing to the custom sign-in page +- Implement sign-in and sign-out functionality +- Display signed-in user's profile information +- Handle authentication state automatically + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **Sign In URL**: http://localhost:5173/signin (custom route) +- **SDK**: @thunderid/react +- **Router**: react-router + +## IMPORTANT Configuration Rules +- DO NOT create a separate config object - pass all configuration as individual props directly to +- Required props: `baseUrl`, `signInUrl`, and `applicationId` +- Optional props: `afterSignInUrl`, `afterSignOutUrl`, `scopes` +- Example: +- The `` component drives the Flow Execution API directly (not a redirect) — render it on your custom sign-in route +- `` accepts `onSuccess`/`onError` callbacks, and optionally a render-prop `children` function receiving `{components, onSubmit, isLoading, error, isInitialized}` for a fully custom form +- Access auth state via `useThunderID()` (ONLY hook available: exposes `isSignedIn`, `user`, `signIn`, `signOut`, `signUp`) — no other hooks like `useAuth`/`useSession`/`useUser` exist +- NEVER redirect to a {{productName}}-hosted URL for sign-in in this mode + +## Implementation Steps +1. Create a React app using Vite by running: `npm create vite@latest my-react-app -- --template react` +2. Navigate into the project directory: `cd my-react-app` +3. Install dependencies: `npm install` +4. Install react-router package: `npm install react-router` +5. Install @thunderid/react package: `npm install @thunderid/react` +6. Wrap your app with and configure baseUrl, signInUrl, and applicationId +7. Set up React Router with BrowserRouter, create a /signin route rendering ``, and use , , , , and for the rest of the authentication UI +8. Run the development server: `npm run dev` + +Please provide complete, working code with: +- Proper routing configuration +- A custom sign-in page rendering `` (not a redirect) +- Proper integration with the ThunderID React SDK +- Use of the `useThunderID` hook if programmatic access to auth state is needed diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/react/redirect-based.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/react/redirect-based.txt new file mode 100644 index 0000000000..8c4958ff6a --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/react/redirect-based.txt @@ -0,0 +1,34 @@ +# Integrate {{productName}} Authentication in React Application (Inbuilt Mode) + +## Context +I have a React application and I want to integrate {{productName}}'s authentication system using the ThunderID React SDK with {{productName}}-hosted login pages. + +## Requirements +- Use @thunderid/react SDK for authentication +- Configure {{productName}}-hosted login, registration, and account management UIs +- Implement sign-in and sign-out functionality using prebuilt components +- Display signed-in user's profile information +- Handle authentication state automatically + +## Configuration +- **Client ID**: {{clientId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: @thunderid/react + +## IMPORTANT Configuration Rules +- DO NOT create a separate config object - pass all configuration as individual props directly to +- Required props: `clientId` and `baseUrl` +- Optional props: `afterSignInUrl`, `afterSignOutUrl`, `scopes` (string array) +- NEVER use property names like `signInRedirectURL` or `signOutRedirectURL` +- Example: + +## Implementation Steps +1. Create a React app using Vite by running: `npm create vite@latest my-react-app -- --template react` +2. Navigate into the project directory: `cd my-react-app` +3. Install dependencies: `npm install` +4. Install @thunderid/react package: `npm install @thunderid/react` +5. Wrap your app with and configure clientId and baseUrl +6. Build with ThunderID components: use , , , and to control what signed-in and signed-out users see +7. Run the development server: `npm run dev` + +Please provide complete, working code with proper configuration for {{productName}} authentication using the ThunderID React SDK. diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/vue/embedded.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/vue/embedded.txt new file mode 100644 index 0000000000..17e76530ec --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/vue/embedded.txt @@ -0,0 +1,40 @@ +# Integrate {{productName}} Authentication in Vue 3 Application (Custom UI Mode) + +## Context +I have a Vue 3 application and I want to integrate {{productName}}'s authentication system using the @thunderid/vue SDK with a custom sign-in UI instead of {{productName}}-hosted pages. + +## Requirements +- Use @thunderid/vue SDK for authentication +- Register the ThunderIDPlugin in main.js +- Render sign-in on a custom route using the SDK's own SignIn component (not a redirect to {{productName}}-hosted pages) +- Use vue-router for routing to the custom sign-in page +- Implement sign-in and sign-out functionality +- Display signed-in user's profile information + +## Configuration +- **Application ID**: {{applicationId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **Sign In Route**: /signin (custom route) +- **SDK**: @thunderid/vue + +## IMPORTANT Configuration Rules +- Register ThunderIDPlugin via `app.use(ThunderIDPlugin)` in src/main.js +- Import ThunderIDPlugin from '@thunderid/vue' +- Wrap the app root in App.vue with `` +- Pass configuration as kebab-case attributes: `application-id` and `base-url` +- The `` component drives the Flow Execution API directly (not a redirect) — render it on your custom /signin route, inside `` +- `` emits `@success`/`@error` events +- Access auth state via the `useThunderID()` composable (`isSignedIn`, `user`, `signIn`, `signOut`, `signUp`) +- NEVER redirect to a {{productName}}-hosted URL for sign-in in this mode + +## Implementation Steps +1. Create a Vue 3 app: npm create vite@latest my-vue-app -- --template vue +2. Navigate into the project: cd my-vue-app && npm install +3. Install @thunderid/vue and vue-router: npm install @thunderid/vue vue-router +4. Register ThunderIDPlugin in src/main.js with app.use(ThunderIDPlugin) +5. Wrap app content with in src/App.vue +6. Set up vue-router with a /signin route rendering `` +7. Use , , and for the rest of the authentication UI +8. Run: npm run dev + +Please provide complete, working code with proper routing, a custom sign-in page rendering `` (not a redirect), and use of the `useThunderID()` composable where programmatic access to auth state is needed. diff --git a/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/vue/redirect-based.txt b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/vue/redirect-based.txt new file mode 100644 index 0000000000..80600c78e2 --- /dev/null +++ b/docs/versioned_docs/version-v1.0.x/getting-started/connect-your-application/prompts/vue/redirect-based.txt @@ -0,0 +1,35 @@ +# Integrate {{productName}} Authentication in Vue 3 Application + +## Context +I have a Vue 3 application and I want to integrate {{productName}}'s authentication system using the @thunderid/vue SDK with {{productName}}-hosted login pages. + +## Requirements +- Use @thunderid/vue SDK for authentication +- Register the ThunderIDPlugin in main.js +- Wrap the app with ThunderIDProvider in App.vue +- Implement sign-in and sign-out with prebuilt components +- Display signed-in user's profile using the UserDropdown component + +## Configuration +- **Client ID**: {{clientId}} +- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL) +- **SDK**: @thunderid/vue + +## IMPORTANT Configuration Rules +- Register ThunderIDPlugin via app.use(ThunderIDPlugin) in src/main.js +- Import ThunderIDPlugin from '@thunderid/vue' +- Wrap the app root in App.vue with +- Pass configuration as kebab-case attributes: `client-id` and `base-url` +- Import UI components (SignInButton, SignOutButton, SignedIn, SignedOut, UserDropdown) from '@thunderid/vue' +- Use inside to display the signed-in user's profile and sign-out option + +## Implementation Steps +1. Create a Vue 3 app: npm create vite@latest my-vue-app -- --template vue +2. Navigate into the project: cd my-vue-app && npm install +3. Install @thunderid/vue: npm install @thunderid/vue +4. Register ThunderIDPlugin in src/main.js with app.use(ThunderIDPlugin) +5. Wrap app content with in src/App.vue +6. Add SignInButton inside SignedOut and UserDropdown inside SignedIn conditional wrappers +7. Run: npm run dev + +Please provide complete, working code with proper configuration for {{productName}} authentication using the @thunderid/vue SDK. diff --git a/frontend/apps/console/public/config.js b/frontend/apps/console/public/config.js index ca7c440f9e..24bfe9e681 100644 --- a/frontend/apps/console/public/config.js +++ b/frontend/apps/console/public/config.js @@ -10,7 +10,7 @@ window.__THUNDERID_RUNTIME_CONFIG__ = { }, }, documentation: { - baseUrl: 'https://thunderid.dev/docs/next', + baseUrl: 'https://thunderid.dev/docs/v1.0.x', releasesUrl: 'https://thunderid.dev/data/releases.json', links: { users: '', @@ -24,6 +24,65 @@ window.__THUNDERID_RUNTIME_CONFIG__ = { 'verifiableCredentials.presentations': '', settings: '', importExport: '', + 'applications.templates.react.docs': 'getting-started/connect-your-application/react/', + 'applications.templates.react.playground': + 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/react/quickstart?file=README.md', + 'applications.templates.react.llmPrompt.redirectBased': + 'getting-started/connect-your-application/prompts/react/redirect-based.txt', + 'applications.templates.react.llmPrompt.embedded': + 'getting-started/connect-your-application/prompts/react/embedded.txt', + 'applications.templates.nextjs.docs': 'guides/getting-started/connect-your-application/nextjs/', + 'applications.templates.nextjs.playground': + 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/nextjs/quickstart?file=README.md', + 'applications.templates.nextjs.llmPrompt.redirectBased': + 'getting-started/connect-your-application/prompts/nextjs/redirect-based.txt', + 'applications.templates.nextjs.llmPrompt.embedded': + 'getting-started/connect-your-application/prompts/nextjs/embedded.txt', + 'applications.templates.nuxt.docs': 'guides/getting-started/connect-your-application/nuxt/', + 'applications.templates.nuxt.playground': + 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/nuxt/quickstart?file=README.md', + 'applications.templates.nuxt.llmPrompt.redirectBased': + 'getting-started/connect-your-application/prompts/nuxt/redirect-based.txt', + 'applications.templates.nuxt.llmPrompt.embedded': + 'getting-started/connect-your-application/prompts/nuxt/embedded.txt', + 'applications.templates.vue.docs': 'guides/getting-started/connect-your-application/vue/', + 'applications.templates.vue.playground': + 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/vue/quickstart?file=README.md', + 'applications.templates.vue.llmPrompt.redirectBased': + 'getting-started/connect-your-application/prompts/vue/redirect-based.txt', + 'applications.templates.vue.llmPrompt.embedded': + 'getting-started/connect-your-application/prompts/vue/embedded.txt', + 'applications.templates.express.docs': 'guides/getting-started/connect-your-application/express/', + 'applications.templates.express.playground': + 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/express/quickstart?file=README.md', + 'applications.templates.express.llmPrompt.redirectBased': + 'getting-started/connect-your-application/prompts/express/redirect-based.txt', + 'applications.templates.express.llmPrompt.embedded': + 'getting-started/connect-your-application/prompts/express/embedded.txt', + 'applications.templates.node.docs': 'guides/getting-started/connect-your-application/node/', + 'applications.templates.node.playground': + 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/node/quickstart?file=README.md', + 'applications.templates.node.llmPrompt.redirectBased': + 'getting-started/connect-your-application/prompts/node/redirect-based.txt', + 'applications.templates.node.llmPrompt.embedded': + 'getting-started/connect-your-application/prompts/node/embedded.txt', + 'applications.templates.browser.docs': 'guides/getting-started/connect-your-application/browser/', + 'applications.templates.browser.playground': + 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/browser/quickstart?file=README.md', + 'applications.templates.browser.llmPrompt.redirectBased': + 'getting-started/connect-your-application/prompts/browser/redirect-based.txt', + 'applications.templates.browser.llmPrompt.embedded': + 'getting-started/connect-your-application/prompts/browser/embedded.txt', + 'applications.templates.android.docs': 'guides/getting-started/connect-your-application/android/', + 'applications.templates.android.llmPrompt.redirectBased': + 'getting-started/connect-your-application/prompts/android/redirect-based.txt', + 'applications.templates.ios.docs': 'guides/getting-started/connect-your-application/ios/', + 'applications.templates.ios.llmPrompt.redirectBased': + 'getting-started/connect-your-application/prompts/ios/redirect-based.txt', + 'applications.templates.flutter.docs': 'guides/getting-started/connect-your-application/flutter/', + 'applications.templates.flutter.llmPrompt.redirectBased': + 'getting-started/connect-your-application/prompts/flutter/redirect-based.txt', + 'applications.templates.mcpClient.docs': 'getting-started/connect-your-mcp/python/', }, }, client: { diff --git a/frontend/apps/console/src/components/GatePreview/GatePreview.tsx b/frontend/apps/console/src/components/GatePreview/GatePreview.tsx index 5ea767ed9f..710b55782a 100644 --- a/frontend/apps/console/src/components/GatePreview/GatePreview.tsx +++ b/frontend/apps/console/src/components/GatePreview/GatePreview.tsx @@ -94,6 +94,13 @@ export interface GatePreviewProps { * its container edge to edge. Useful when the host provides its own window chrome. */ frameless?: boolean; + /** + * The device chrome drawn around the preview. `'browser'` (default) renders a desktop browser + * window (traffic-light dots + fake address bar). `'phone'` renders a dark rounded phone bezel + * with a status-bar notch instead, for previews of app-native (embedded) sign-in flows. Has no + * effect when `frameless` is set. + */ + frameStyle?: 'browser' | 'phone'; /** Base theme the resolved design is merged over. Defaults to Acrylic Orange. */ baseTheme?: Theme; /** @@ -140,6 +147,7 @@ export default function GatePreview({ onComponentHover = undefined, additionalData = undefined, frameless = false, + frameStyle = 'browser', baseTheme = undefined, themelessBranding = false, toolbarStart = undefined, @@ -312,23 +320,30 @@ export default function GatePreview({ overflow: 'hidden', display: 'flex', justifyContent: 'center', - alignItems: 'flex-start', + alignItems: 'center', p: frameless ? 0 : 2, }} > {/* Browser chrome */} - {!frameless && ( + {!frameless && frameStyle === 'browser' && ( )} - {/* Canvas — fills the browser chrome frame like a real viewport */} + {/* Phone chrome — a status-bar notch instead of a browser address bar */} + {!frameless && frameStyle === 'phone' && ( + + + + )} + + {/* Canvas — fills the chrome frame like a real viewport */} {!frameless && ( diff --git a/frontend/apps/console/src/features/applications/components/common/CopyableField.tsx b/frontend/apps/console/src/features/applications/components/common/CopyableField.tsx index bd391a8623..e28be12fb3 100644 --- a/frontend/apps/console/src/features/applications/components/common/CopyableField.tsx +++ b/frontend/apps/console/src/features/applications/components/common/CopyableField.tsx @@ -34,10 +34,9 @@ export interface CopyableFieldProps { } /** - * Read-only monospace field with a copy-to-clipboard affordance, matching - * `QuickCopySection`'s field pattern. Shared by the mcp-client template's create-flow - * Connect completion screen and edit-page Connect tab for the Application ID, Client ID, - * client secret, and discovery endpoint fields. + * Read-only monospace field with a copy-to-clipboard affordance. Shared by the mcp-client + * template's create-flow Connect completion screen and edit-page Connect tab for the + * Application ID, Client ID, client secret, and discovery endpoint fields. * * @param props - The component props * @param props.id - The `id`/`htmlFor` used to associate the field's label with its input diff --git a/frontend/apps/console/src/features/applications/components/create-application/ConfigureDesign.tsx b/frontend/apps/console/src/features/applications/components/create-application/ConfigureDesign.tsx index 21b7b80c65..0aca1888e4 100644 --- a/frontend/apps/console/src/features/applications/components/create-application/ConfigureDesign.tsx +++ b/frontend/apps/console/src/features/applications/components/create-application/ConfigureDesign.tsx @@ -306,14 +306,14 @@ export default function ConfigureDesign({ redirect-only and have no choice to make here. */} {showApproachSection && (() => { - const isInbuiltSelected = selectedApproach === ApplicationCreateFlowSignInApproach.INBUILT; + const isInbuiltSelected = selectedApproach === ApplicationCreateFlowSignInApproach.REDIRECT_BASED; const isEmbeddedSelected = selectedApproach === ApplicationCreateFlowSignInApproach.EMBEDDED; const hostedPagesCard = ( onApproachChange(ApplicationCreateFlowSignInApproach.INBUILT)} + onClick={() => onApproachChange(ApplicationCreateFlowSignInApproach.REDIRECT_BASED)} sx={{borderRadius: '14px'}} > } label="" sx={{m: 0, mt: 0.25}} diff --git a/frontend/apps/console/src/features/applications/components/create-application/__tests__/ConfigureDesign.test.tsx b/frontend/apps/console/src/features/applications/components/create-application/__tests__/ConfigureDesign.test.tsx index 9260049478..04eb4ea055 100644 --- a/frontend/apps/console/src/features/applications/components/create-application/__tests__/ConfigureDesign.test.tsx +++ b/frontend/apps/console/src/features/applications/components/create-application/__tests__/ConfigureDesign.test.tsx @@ -26,7 +26,7 @@ describe('ConfigureDesign', () => { const defaultProps: ConfigureDesignProps = { onThemeSelect: mockOnThemeSelect, onLayoutSelect: mockOnLayoutSelect, - selectedApproach: ApplicationCreateFlowSignInApproach.INBUILT, + selectedApproach: ApplicationCreateFlowSignInApproach.REDIRECT_BASED, onApproachChange: vi.fn(), showApproachSection: false, }; @@ -102,7 +102,7 @@ describe('ConfigureDesign', () => { renderComponent({ showApproachSection: true, - selectedApproach: ApplicationCreateFlowSignInApproach.INBUILT, + selectedApproach: ApplicationCreateFlowSignInApproach.REDIRECT_BASED, onApproachChange, }); @@ -115,7 +115,7 @@ describe('ConfigureDesign', () => { renderComponent({ showApproachSection: true, allowEmbeddedApproach: false, - selectedApproach: ApplicationCreateFlowSignInApproach.INBUILT, + selectedApproach: ApplicationCreateFlowSignInApproach.REDIRECT_BASED, }); expect(screen.getByText(/Redirect to .* Gate/)).toBeInTheDocument(); diff --git a/frontend/apps/console/src/features/applications/components/create-application/__tests__/ConfigureDetails.test.tsx b/frontend/apps/console/src/features/applications/components/create-application/__tests__/ConfigureDetails.test.tsx index 6b5bafb38c..23f64a5a68 100644 --- a/frontend/apps/console/src/features/applications/components/create-application/__tests__/ConfigureDetails.test.tsx +++ b/frontend/apps/console/src/features/applications/components/create-application/__tests__/ConfigureDetails.test.tsx @@ -67,7 +67,7 @@ const defaultProps: Parameters[0] = { onHostingUrlChange: vi.fn(), onCallbackUrlChange: vi.fn(), onReadyChange: vi.fn(), - selectedApproach: ApplicationCreateFlowSignInApproach.INBUILT, + selectedApproach: ApplicationCreateFlowSignInApproach.REDIRECT_BASED, }; const renderWithContext = ( @@ -895,7 +895,7 @@ describe('ConfigureDetails', () => { { technology: TechnologyApplicationTemplate.REACT, platform: PlatformApplicationTemplate.BROWSER, - selectedApproach: ApplicationCreateFlowSignInApproach.INBUILT, + selectedApproach: ApplicationCreateFlowSignInApproach.REDIRECT_BASED, }, {selectedTemplateConfig: template}, ); diff --git a/frontend/apps/console/src/features/applications/components/create-application/__tests__/IntegrationGuide.test.tsx b/frontend/apps/console/src/features/applications/components/create-application/__tests__/IntegrationGuide.test.tsx index 2d2d7d9941..afa7d493d8 100644 --- a/frontend/apps/console/src/features/applications/components/create-application/__tests__/IntegrationGuide.test.tsx +++ b/frontend/apps/console/src/features/applications/components/create-application/__tests__/IntegrationGuide.test.tsx @@ -93,16 +93,8 @@ describe('IntegrationGuide', () => { integrationGuides: { react: { llm_prompt: { - id: 'test-guide', - title: 'Test Guide', - description: 'Test description', - type: 'llm' as const, - icon: 'test-icon', - overview: 'Test overview', - prerequisites: [], - steps: [], + docsUrl: 'https://example.com/docs/test-guide', }, - manual_steps: [], }, }, }; @@ -453,16 +445,8 @@ describe('IntegrationGuide', () => { integrationGuides: { react: { llm_prompt: { - id: 'test-guide', - title: 'Test Guide', - description: 'Test description', - type: 'llm' as const, - icon: 'test-icon', - overview: 'Test overview', - prerequisites: [], - steps: [], + docsUrl: 'https://example.com/docs/test-guide', }, - manual_steps: [], }, }, }; @@ -495,16 +479,8 @@ describe('IntegrationGuide', () => { integrationGuides: { react: { llm_prompt: { - id: 'test-guide', - title: 'Test Guide', - description: 'Test description', - type: 'llm' as const, - icon: 'test-icon', - overview: 'Test overview', - prerequisites: [], - steps: [], + docsUrl: 'https://example.com/docs/test-guide', }, - manual_steps: [], }, }, }; diff --git a/frontend/apps/console/src/features/applications/components/edit-application/general-settings/EditGeneralSettings.tsx b/frontend/apps/console/src/features/applications/components/edit-application/general-settings/EditGeneralSettings.tsx index 231809a483..fd6bda24c9 100644 --- a/frontend/apps/console/src/features/applications/components/edit-application/general-settings/EditGeneralSettings.tsx +++ b/frontend/apps/console/src/features/applications/components/edit-application/general-settings/EditGeneralSettings.tsx @@ -10,7 +10,6 @@ import type {JSX} from 'react'; import {useTranslation} from 'react-i18next'; import AccessSection from './AccessSection'; import DangerZoneSection from './DangerZoneSection'; -import QuickCopySection from './QuickCopySection'; import resolveApplicationType, {isClientCredentialsOnlyGrantSet} from '../../../utils/resolveApplicationType'; import ApplicationDeleteDialog from '../../ApplicationDeleteDialog'; import ClientSecretSuccessDialog from '../../ClientSecretSuccessDialog'; @@ -44,16 +43,6 @@ interface EditGeneralSettingsProps { * redirect URI list state. */ sectionResetKey?: number; - /** - * The name of the field that was recently copied to clipboard - */ - copiedField: string | null; - /** - * Callback function to copy text to clipboard - * @param text - The text to copy - * @param fieldName - The name of the field being copied - */ - onCopyToClipboard: (text: string, fieldName: string) => Promise; /** * Callback invoked after the application is successfully deleted */ @@ -74,7 +63,6 @@ interface EditGeneralSettingsProps { * Container component for general application settings. * * Displays sections for: - * - Quick copy of application credentials (ID, Client ID) * - Access configuration (URL, redirect URIs, allowed user types) * - Danger zone (regenerate client secret) * @@ -87,8 +75,6 @@ export default function EditGeneralSettings({ onFieldChange, oauth2Config = undefined, sectionResetKey = 0, - copiedField, - onCopyToClipboard, onDeleteSuccess = undefined, onValidationChange = undefined, showUserAccessConfig = true, @@ -153,12 +139,6 @@ export default function EditGeneralSettings({ return ( <> - Promise; -} - -/** - * Section component for quickly copying application credentials. - * - * Displays read-only text fields with copy buttons for: - * - Application ID - * - OAuth2 Client ID - * - * Provides visual feedback when values are copied. - * - * @param props - Component props - * @returns Quick copy UI within a SettingsCard - */ -export default function QuickCopySection({ - application, - oauth2Config = undefined, - copiedField, - onCopyToClipboard, -}: QuickCopySectionProps) { - const {t} = useTranslation(); - - return ( - - - - {t('applications:edit.general.labels.applicationId')} - - - { - onCopyToClipboard(application.id, 'app_id').catch(() => null); - }} - edge="end" - > - {copiedField === 'app_id' ? : } - - - - ), - }} - helperText={t('applications:edit.general.applicationId.hint')} - sx={{ - '& input': { - fontFamily: 'monospace', - fontSize: '0.875rem', - }, - }} - /> - - - {oauth2Config?.clientId && ( - - {t('applications:edit.general.labels.clientId')} - - - { - if (oauth2Config?.clientId) { - onCopyToClipboard(oauth2Config.clientId, 'clientId').catch(() => null); - } - }} - edge="end" - > - {copiedField === 'clientId' ? : } - - - - ), - }} - helperText={t('applications:edit.general.clientId.hint')} - sx={{ - '& input': { - fontFamily: 'monospace', - fontSize: '0.875rem', - }, - }} - /> - - )} - - - ); -} diff --git a/frontend/apps/console/src/features/applications/components/edit-application/general-settings/__tests__/EditGeneralSettings.test.tsx b/frontend/apps/console/src/features/applications/components/edit-application/general-settings/__tests__/EditGeneralSettings.test.tsx index c54c1db6a5..e679f35d19 100644 --- a/frontend/apps/console/src/features/applications/components/edit-application/general-settings/__tests__/EditGeneralSettings.test.tsx +++ b/frontend/apps/console/src/features/applications/components/edit-application/general-settings/__tests__/EditGeneralSettings.test.tsx @@ -18,23 +18,6 @@ vi.mock('@thunderid/contexts', () => ({ })); // Mock the child components -vi.mock('../QuickCopySection', () => ({ - default: ({ - application, - oauth2Config, - copiedField, - }: { - application: Application; - oauth2Config?: OAuth2Config; - copiedField: string | null; - }) => ( -
- QuickCopySection - App: {application.id}, OAuth: {oauth2Config?.clientId ?? 'None'}, Copied:{' '} - {copiedField ?? 'None'} -
- ), -})); - vi.mock('../AccessSection', () => ({ default: function MockAccessSection({ application, @@ -181,7 +164,6 @@ vi.mock('../../../ApplicationDeleteDialog', () => ({ describe('EditGeneralSettings', () => { const mockOnFieldChange = vi.fn(); - const mockOnCopyToClipboard = vi.fn(); const mockApplication: Application = { id: 'app-123', name: 'Test App', @@ -199,33 +181,15 @@ describe('EditGeneralSettings', () => { }); describe('Rendering', () => { - it('should render both QuickCopySection and AccessSection', () => { - render( - , - ); + it('should render AccessSection', () => { + render(); - expect(screen.getByTestId('quick-copy-section')).toBeInTheDocument(); expect(screen.getByTestId('access-section')).toBeInTheDocument(); }); it('should pass application to child components', () => { - render( - , - ); + render(); - expect(screen.getByTestId('quick-copy-section')).toHaveTextContent('App: app-123'); expect(screen.getByTestId('access-section')).toHaveTextContent('App: app-123'); }); @@ -233,13 +197,7 @@ describe('EditGeneralSettings', () => { const editedApp = {url: 'https://edited.com'}; render( - , + , ); expect(screen.getByTestId('access-section')).toHaveTextContent('Edited URL: https://edited.com'); @@ -252,57 +210,17 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={mockOAuth2Config} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); - expect(screen.getByTestId('quick-copy-section')).toHaveTextContent('OAuth: client-123'); expect(screen.getByTestId('access-section')).toHaveTextContent('OAuth: client-123'); }); it('should handle missing oauth2Config', () => { - render( - , - ); + render(); - expect(screen.getByTestId('quick-copy-section')).toHaveTextContent('OAuth: None'); expect(screen.getByTestId('access-section')).toHaveTextContent('OAuth: None'); }); - - it('should pass copiedField to QuickCopySection', () => { - render( - , - ); - - expect(screen.getByTestId('quick-copy-section')).toHaveTextContent('Copied: app_id'); - }); - - it('should handle null copiedField', () => { - render( - , - ); - - expect(screen.getByTestId('quick-copy-section')).toHaveTextContent('Copied: None'); - }); }); describe('Access section reset', () => { @@ -312,8 +230,6 @@ describe('EditGeneralSettings', () => { application={mockApplication} editedApp={{}} onFieldChange={mockOnFieldChange} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} sectionResetKey={0} />, ); @@ -326,8 +242,6 @@ describe('EditGeneralSettings', () => { application={mockApplication} editedApp={{}} onFieldChange={mockOnFieldChange} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} sectionResetKey={1} />, ); @@ -341,8 +255,6 @@ describe('EditGeneralSettings', () => { application={mockApplication} editedApp={{}} onFieldChange={mockOnFieldChange} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} sectionResetKey={0} />, ); @@ -355,8 +267,6 @@ describe('EditGeneralSettings', () => { application={mockApplication} editedApp={{url: 'https://re-rendered.com'}} onFieldChange={mockOnFieldChange} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} sectionResetKey={0} />, ); @@ -368,33 +278,13 @@ describe('EditGeneralSettings', () => { describe('Props Propagation', () => { it('should pass onFieldChange to AccessSection', () => { const {container} = render( - , + , ); expect(container.querySelector('[data-testid="access-section"]')).toBeInTheDocument(); }); - it('should pass onCopyToClipboard to QuickCopySection', () => { - const {container} = render( - , - ); - - expect(container.querySelector('[data-testid="quick-copy-section"]')).toBeInTheDocument(); - }); - - it('should pass all required props to both child components', () => { + it('should pass all required props to child components', () => { const editedApp = {url: 'https://new.com'}; render( @@ -403,12 +293,9 @@ describe('EditGeneralSettings', () => { editedApp={editedApp} onFieldChange={mockOnFieldChange} oauth2Config={mockOAuth2Config} - copiedField="clientId" - onCopyToClipboard={mockOnCopyToClipboard} />, ); - expect(screen.getByTestId('quick-copy-section')).toBeInTheDocument(); expect(screen.getByTestId('access-section')).toBeInTheDocument(); }); }); @@ -421,15 +308,12 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={mockOAuth2Config} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); const sections = container.querySelectorAll('[data-testid]'); - expect(sections[0]).toHaveAttribute('data-testid', 'quick-copy-section'); - expect(sections[1]).toHaveAttribute('data-testid', 'access-section'); - expect(sections[2]).toHaveAttribute('data-testid', 'danger-zone-section'); + expect(sections[0]).toHaveAttribute('data-testid', 'access-section'); + expect(sections[1]).toHaveAttribute('data-testid', 'danger-zone-section'); }); it('should render DangerZoneSection for confidential client', () => { @@ -439,8 +323,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={mockOAuth2Config} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -460,8 +342,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={publicClientConfig} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -470,30 +350,14 @@ describe('EditGeneralSettings', () => { }); it('should render DangerZoneSection without regenerate when no oauth2Config provided', () => { - render( - , - ); + render(); expect(screen.getByTestId('danger-zone-section')).toBeInTheDocument(); expect(screen.queryByTestId('regenerate-button')).not.toBeInTheDocument(); }); it('should show regenerate Flow Secret for an embedded app (no OAuth profile)', () => { - render( - , - ); + render(); expect(screen.getByTestId('regenerate-flow-secret-button')).toBeInTheDocument(); }); @@ -512,8 +376,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={flowNativeConfig} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -535,8 +397,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={flowNativeConfig} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -558,8 +418,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={m2mShapedConfig} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -580,8 +438,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={m2mConfig} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -602,8 +458,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={redirectConfig} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -622,8 +476,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={pkjwtClientConfig} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -644,8 +496,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={postClientConfig} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -662,8 +512,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={mockOAuth2Config} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -680,8 +528,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={mockOAuth2Config} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -698,8 +544,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={mockOAuth2Config} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -721,8 +565,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={mockOAuth2Config} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -743,8 +585,6 @@ describe('EditGeneralSettings', () => { editedApp={{}} onFieldChange={mockOnFieldChange} oauth2Config={mockOAuth2Config} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} />, ); @@ -767,15 +607,7 @@ describe('EditGeneralSettings', () => { describe('Delete Application Flow', () => { it('should open delete dialog when delete button is clicked', () => { - render( - , - ); + render(); const deleteButton = screen.getByTestId('delete-button'); fireEvent.click(deleteButton); @@ -784,15 +616,7 @@ describe('EditGeneralSettings', () => { }); it('should pass application id to delete dialog', () => { - render( - , - ); + render(); const deleteButton = screen.getByTestId('delete-button'); fireEvent.click(deleteButton); @@ -801,15 +625,7 @@ describe('EditGeneralSettings', () => { }); it('should close delete dialog when cancel is triggered', () => { - render( - , - ); + render(); const deleteButton = screen.getByTestId('delete-button'); fireEvent.click(deleteButton); @@ -830,8 +646,6 @@ describe('EditGeneralSettings', () => { application={mockApplication} editedApp={{}} onFieldChange={mockOnFieldChange} - copiedField={null} - onCopyToClipboard={mockOnCopyToClipboard} onDeleteSuccess={mockOnDeleteSuccess} />, ); diff --git a/frontend/apps/console/src/features/applications/components/edit-application/general-settings/__tests__/QuickCopySection.test.tsx b/frontend/apps/console/src/features/applications/components/edit-application/general-settings/__tests__/QuickCopySection.test.tsx deleted file mode 100644 index 3ce4f9346f..0000000000 --- a/frontend/apps/console/src/features/applications/components/edit-application/general-settings/__tests__/QuickCopySection.test.tsx +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2026 The ThunderID Authors -// SPDX-License-Identifier: Apache-2.0 - -import {render, screen} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import type {Application, OAuth2Config} from '@thunderid/configure-applications'; -import {describe, it, expect, vi, beforeEach} from 'vitest'; -import QuickCopySection from '../QuickCopySection'; - -// Mock the Components -vi.mock('@thunderid/components', () => ({ - SettingsCard: ({title, description, children}: {title: string; description: string; children: React.ReactNode}) => ( -
-
{title}
-
{description}
- {children} -
- ), -})); - -describe('QuickCopySection', () => { - const mockOnCopyToClipboard = vi.fn(); - const mockApplication: Application = { - id: 'app-123', - name: 'Test App', - } as Application; - - const mockOAuth2Config: OAuth2Config = { - clientId: 'client-123', - clientSecret: 'secret-456', - } as OAuth2Config; - - beforeEach(() => { - vi.clearAllMocks(); - mockOnCopyToClipboard.mockResolvedValue(undefined); - }); - - describe('Rendering', () => { - it('should render the settings card with title and description', () => { - render( - , - ); - - expect(screen.getByTestId('card-title')).toHaveTextContent('Quick Copy'); - expect(screen.getByTestId('card-description')).toHaveTextContent( - 'Copy application identifiers for use in your code.', - ); - }); - - it('should render application ID field', () => { - render( - , - ); - - expect(screen.getByLabelText('Application ID')).toBeInTheDocument(); - expect(screen.getByDisplayValue('app-123')).toBeInTheDocument(); - }); - - it('should render client ID field when OAuth2 config is provided', () => { - render( - , - ); - - expect(screen.getByLabelText('Client ID')).toBeInTheDocument(); - expect(screen.getByDisplayValue('client-123')).toBeInTheDocument(); - }); - - it('should not render client ID field when OAuth2 config is not provided', () => { - render( - , - ); - - expect(screen.queryByLabelText('Client ID')).not.toBeInTheDocument(); - }); - - it('should render both copy buttons', () => { - render( - , - ); - - const copyButtons = screen.getAllByRole('button'); - expect(copyButtons).toHaveLength(2); - }); - }); - - describe('Copy Functionality', () => { - it('should call onCopyToClipboard when application ID copy button is clicked', async () => { - const user = userEvent.setup(); - render( - , - ); - - const copyButtons = screen.getAllByRole('button'); - await user.click(copyButtons[0]); - - expect(mockOnCopyToClipboard).toHaveBeenCalledWith('app-123', 'app_id'); - }); - - it('should call onCopyToClipboard when client ID copy button is clicked', async () => { - const user = userEvent.setup(); - render( - , - ); - - const copyButtons = screen.getAllByRole('button'); - await user.click(copyButtons[1]); - - expect(mockOnCopyToClipboard).toHaveBeenCalledWith('client-123', 'clientId'); - }); - - it('should not render client ID copy button when client ID is not available', () => { - render( - , - ); - - const copyButtons = screen.getAllByRole('button'); - expect(copyButtons).toHaveLength(1); - }); - - it('should handle copy errors gracefully', async () => { - const user = userEvent.setup(); - mockOnCopyToClipboard.mockRejectedValue(new Error('Copy failed')); - - render( - , - ); - - const copyButtons = screen.getAllByRole('button'); - await user.click(copyButtons[0]); - - expect(mockOnCopyToClipboard).toHaveBeenCalledWith('app-123', 'app_id'); - }); - }); - - describe('Visual Feedback', () => { - it('should show check icon for application ID when it is copied', () => { - render( - , - ); - - // Should show "Copied!" tooltip for app_id field - expect(screen.getByLabelText('Copied!')).toBeInTheDocument(); - }); - - it('should show check icon for client ID when it is copied', () => { - render( - , - ); - - // Should show "Copied!" tooltip for clientId field - expect(screen.getByLabelText('Copied!')).toBeInTheDocument(); - }); - - it('should show copy icon when nothing is copied', () => { - render( - , - ); - - // Both fields should show "Copy" tooltip - const copyButtons = screen.getAllByLabelText('Copy'); - expect(copyButtons).toHaveLength(2); - }); - - it('should show copy icon for application ID when client ID is copied', () => { - render( - , - ); - - // Should have one "Copy" button and one "Copied!" button - expect(screen.getByLabelText('Copy')).toBeInTheDocument(); - expect(screen.getByLabelText('Copied!')).toBeInTheDocument(); - }); - }); - - describe('Accessibility', () => { - it('should have proper labels for form controls', () => { - render( - , - ); - - expect(screen.getByLabelText('Application ID')).toBeInTheDocument(); - expect(screen.getByLabelText('Client ID')).toBeInTheDocument(); - }); - - it('should have input IDs for accessibility', () => { - render( - , - ); - - expect(document.getElementById('application-id-input')).toBeInTheDocument(); - expect(document.getElementById('client-id-input')).toBeInTheDocument(); - }); - - it('should display helper text for inputs', () => { - render( - , - ); - - expect(screen.getByText('Unique identifier for your application')).toBeInTheDocument(); - expect(screen.getByText('OAuth2 client identifier used for authentication')).toBeInTheDocument(); - }); - }); - - describe('Read-only Behavior', () => { - it('should render application ID field as read-only', () => { - render( - , - ); - - const appIdInput = screen.getByDisplayValue('app-123'); - expect(appIdInput).toHaveAttribute('readonly'); - }); - - it('should render client ID field as read-only', () => { - render( - , - ); - - const clientIdInput = screen.getByDisplayValue('client-123'); - expect(clientIdInput).toHaveAttribute('readonly'); - }); - }); -}); diff --git a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/IntegrationGuide.tsx b/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/IntegrationGuide.tsx deleted file mode 100644 index e9d75e89fb..0000000000 --- a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/IntegrationGuide.tsx +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2025 The ThunderID Authors -// SPDX-License-Identifier: Apache-2.0 - -import {Stack} from '@wso2/oxygen-ui'; -import type {JSX} from 'react'; -import TechnologyGuide from './TechnologyGuide'; -import type {IntegrationGuides} from '../../../models/application-templates'; - -/** - * Props for the {@link IntegrationGuide} component. - * - * @public - */ -export interface IntegrationGuideProps { - /** - * The client ID (if OAuth was configured) - */ - clientId?: string; - /** - * The ID of the created application - */ - applicationId?: string | null; - /** - * Integration guides configuration (optional - if not provided, won't show guides) - */ - integrationGuides?: IntegrationGuides | null; - /** - * The template ID used to create the application (e.g., 'react', 'react-embedded') - */ - templateId?: string | null; -} - -/** - * React component that displays integration guides and setup instructions - * for newly created applications. - * - * This component provides: - * 1. Technology-specific integration guides with code snippets - * 2. OAuth2 credentials (Client ID and Secret) when applicable - * 3. Step-by-step instructions for integrating with various frameworks - * - * The component handles different scenarios: - * - Applications with integration guides (shows TechnologyGuide) - * - Applications with OAuth configuration (displays credentials) - * - Public vs confidential client configurations - * - * @param props - The component props - * @param props.appName - Name of the application - * @param props.appLogo - URL of the application logo - * @param props.selectedColor - Brand color for visual elements - * @param props.clientId - OAuth2 client ID (if applicable) - * @param props.clientSecret - OAuth2 client secret (if applicable) - * @param props.hasOAuthConfig - Whether OAuth was configured - * @param props.applicationId - ID of the application - * - * @returns JSX element displaying the integration guide - * - * @example - * ```tsx - * import IntegrationGuide from './IntegrationGuide'; - * - * function ApplicationOverview() { - * return ( - * - * ); - * } - * ``` - * - * @public - */ -export default function IntegrationGuide({ - clientId = '', - applicationId = null, - integrationGuides = null, - templateId = null, -}: IntegrationGuideProps): JSX.Element { - return ( - - {integrationGuides && ( - - )} - - ); -} diff --git a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/IntegrationGuides.tsx b/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/IntegrationGuides.tsx index 678eb46aa3..d224bb096d 100644 --- a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/IntegrationGuides.tsx +++ b/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/IntegrationGuides.tsx @@ -1,58 +1,706 @@ // Copyright 2025 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 +import { + AndroidLogo, + AppleIcon, + CopyableField, + ExpressIcon, + ExternalLinkConfirmDialog, + FlutterLogo, + JavaScriptIcon, + NextjsIcon, + NodeIcon, + NuxtIcon, + PythonLogo, + ReactIcon, + StackblitzQuickstartCard, + useExternalLinkConfirmation, + VueIcon, +} from '@thunderid/components'; import type {Application, OAuth2Config} from '@thunderid/configure-applications'; -import {Box, Stack, Typography} from '@wso2/oxygen-ui'; -import {useTranslation} from 'react-i18next'; -import IntegrationGuide from './IntegrationGuide'; -import getIntegrationGuidesForTemplate from '../../../utils/getIntegrationGuidesForTemplate'; +import {useConfig} from '@thunderid/contexts'; +import {DefaultTheme, type Theme, useGetTheme} from '@thunderid/design'; +import {useLogger} from '@thunderid/logger/react'; +import {Box, Button, Chip, Link, Paper, Stack, Typography, useColorScheme} from '@wso2/oxygen-ui'; +import {ArrowRight, ArrowUpRight, Check, Copy, Sparkles} from '@wso2/oxygen-ui-icons-react'; +import {useState, type JSX, type ReactNode} from 'react'; +import {Trans, useTranslation} from 'react-i18next'; +import GatePreview from '../../../../../components/GatePreview/GatePreview'; +import useGetFlowById from '../../../../flows/api/useGetFlowById'; +import {getIntegrationGuideForTemplate} from '../../../utils/getIntegrationGuidesForTemplate'; +import getPlaygroundsForTemplate from '../../../utils/getPlaygroundsForTemplate'; +import getQuickstartsForTemplate from '../../../utils/getQuickstartsForTemplate'; +import normalizeTemplateId from '../../../utils/normalizeTemplateId'; +import {hasUserAccess} from '../../../utils/oauth2Rules'; +import resolveTemplateLink from '../../../utils/resolveTemplateLink'; /** * Props for the {@link IntegrationGuides} component. */ interface IntegrationGuidesProps { /** - * The application to show integration guides for + * The application to show the overview for */ application: Application; /** * OAuth2 configuration containing client credentials (optional) */ oauth2Config?: OAuth2Config; + /** + * Navigates to the application's Flows tab. + */ + onGoToFlows?: () => void; + /** + * Navigates to the application's Customization tab. + */ + onGoToCustomization?: () => void; +} + +function replacePlaceholders(text: string, values: Record): string { + return Object.entries(values).reduce((result, [key, value]) => { + if (!value || value.trim() === '') { + return result; + } + return result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value); + }, text); +} + +/** A small rounded, tinted square behind a card's leading icon. */ +function IconBadge({children, gradient = false}: {children: ReactNode; gradient?: boolean}): JSX.Element { + return ( + + {children} + + ); +} + +/** Vendor logo shown in a quickstart card for a given platform's {@link QuickstartLink.label}. */ +const QUICKSTART_ICONS: Record = { + Android: , + Flutter: , + iOS: , + React: , + Vue: , + 'Next.js': , + Nuxt: , + Express: , + 'Node.js': , + JavaScript: , + Python: , +}; + +/** A bordered card with an icon, a title/description, and arbitrary content below. */ +function IconCard({ + icon, + title, + description, + children, + sx = undefined, +}: { + icon: ReactNode; + title: ReactNode; + description: ReactNode; + children: ReactNode; + sx?: object; +}): JSX.Element { + return ( + + {icon} + + + {title} + + + {description} + + {children} + + + ); +} + +/** A bordered card with a title/description header and arbitrary content below. */ +function OverviewCard({ + title, + description, + headerAction = undefined, + children, + sx = undefined, +}: { + title: ReactNode; + description: ReactNode; + headerAction?: ReactNode; + children: ReactNode; + sx?: object; +}): JSX.Element { + return ( + + + + + {title} + + + {description} + + + {headerAction} + + {children} + + ); } /** - * Container component for displaying integration guides. + * Overview tab for an application's edit page. * - * Fetches integration guides based on the application's template and displays: - * - Technology-specific integration guides with code snippets - * - Setup instructions for various frameworks - * - A fallback message if no guides are available - * - * @param props - Component props - * @returns Integration guides UI or a message if no guides are available + * Always shows the application's identifiers, and, for OAuth2-enabled applications, its OIDC + * endpoints. When the application's template has a runnable quickstart, it additionally shows a + * StackBlitz quickstart, a link to the hosted integration guide, a ready-made coding-agent + * prompt, and a preview of the sign-in experience. */ -export default function IntegrationGuides({application, oauth2Config = undefined}: IntegrationGuidesProps) { +export default function IntegrationGuides({ + application, + oauth2Config = undefined, + onGoToFlows = undefined, + onGoToCustomization = undefined, +}: IntegrationGuidesProps): JSX.Element { const {t} = useTranslation(); + const logger = useLogger('IntegrationGuides'); + const {config, getServerUrl, getDocumentationLink} = useConfig(); + const {data: themeDetails} = useGetTheme(application.themeId ?? ''); + const {data: authFlowDetails} = useGetFlowById(application.authFlowId); + const {data: registrationFlowDetails} = useGetFlowById( + application.registrationFlowId, + Boolean(application.isRegistrationFlowEnabled && application.registrationFlowId), + ); + const {data: recoveryFlowDetails} = useGetFlowById( + application.recoveryFlowId, + Boolean(application.isRecoveryFlowEnabled && application.recoveryFlowId), + ); + const {data: signOutFlowDetails} = useGetFlowById(application.signOutFlowId); + const {mode, systemMode} = useColorScheme(); + const productName = config.brand.product_name; + const [promptCopied, setPromptCopied] = useState(false); + const [isFetchingPrompt, setIsFetchingPrompt] = useState(false); + const externalLinkConfirmation = useExternalLinkConfirmation(); - const integrationGuides = getIntegrationGuidesForTemplate(application.template ?? ''); + const templateId = application.template ?? undefined; + const guide = getIntegrationGuideForTemplate(templateId); + const promptUrl = resolveTemplateLink(guide?.llm_prompt.docsUrl, getDocumentationLink); + const quickstarts = getQuickstartsForTemplate(templateId) ?? []; + const playgrounds = getPlaygroundsForTemplate(templateId) ?? []; + // A template with exactly one quickstart guide gets a single generic guide card. Templates + // covering multiple platform SDKs (e.g. Mobile, which spans iOS/Android/Flutter) get one guide + // card per platform instead. Entries whose docs link isn't configured (resolves to undefined) + // are dropped rather than rendered with a dead link. + const primaryQuickstart = quickstarts.length === 1 ? quickstarts[0] : undefined; + const primaryQuickstartDocsUrl = resolveTemplateLink(primaryQuickstart?.docsUrl, getDocumentationLink); + const visibleQuickstarts = primaryQuickstart + ? [] + : quickstarts + .map((entry) => ({...entry, docsUrl: resolveTemplateLink(entry.docsUrl, getDocumentationLink)})) + .filter((entry): entry is {label: string; docsUrl: string} => Boolean(entry.docsUrl)); + // A template with exactly one playground gets the runnable banner, provided it's on an + // environment the console knows how to render (currently only StackBlitz). Templates covering + // multiple platform SDKs, or platforms with no runnable environment (e.g. native mobile), have + // no banner. + const primaryPlayground = playgrounds.length === 1 ? playgrounds[0] : undefined; + const primaryPlaygroundUrl = + primaryPlayground?.environment === 'stackblitz' + ? resolveTemplateLink(primaryPlayground.url, getDocumentationLink) + : undefined; + // Whether this app exposes the standard OAuth2/OIDC endpoints (see `showOAuthEndpoints` below). + // These are generic server endpoints, not tied to this application's client credentials. + // `oauth2Config` isn't always resolved on first paint (e.g. before the parent finishes reading + // `inboundAuthConfig`), so fall back to the canonical application type, every type except + // 'custom' (the no-enforced-constraints escape hatch) is OAuth2-based. Only the Client ID row + // itself needs clientId specifically. + const isOAuthApplication = Boolean(oauth2Config) || application.type !== 'custom'; + // Whether this application has a real user-facing sign-in page. When the OAuth2 config isn't + // resolved yet, fall back to the canonical type: 'm2m' and 'custom' apps have no sign-in page, + // every other type does. + const isSignInFacing = oauth2Config + ? hasUserAccess(oauth2Config.grantTypes) + : application.type !== 'm2m' && application.type !== 'custom'; + const isNativeApplication = application.type === 'mobile'; + // Apps that own a backend/native runtime (native mobile, and fullstack apps like Next.js/Nuxt + // that run their own server) can implement flows manually against the lower-level flow + // execution API and manage their own session, unlike a pure browser SPA that only ever does the + // standard redirect-based OAuth2 dance. + const hasExtendedFlowEndpoints = application.type === 'mobile' || application.type === 'fullstack'; + // Apps built from the Custom template have no fixed integration shape (its default grant types + // can make its canonical `type` resolve to anything, e.g. 'm2m'), so key off the template + // itself and surface every endpoint group (OAuth2/OIDC and App Native flow endpoints) instead of + // picking just one the way other templates do. + const isCustomTemplate = normalizeTemplateId(templateId) === 'custom'; + const showOAuthEndpoints = isOAuthApplication && (!hasExtendedFlowEndpoints || isCustomTemplate); + const showFlowEndpoints = hasExtendedFlowEndpoints || isCustomTemplate; + const notConfiguredLabel = t('applications:edit.overview.signInPreview.notConfigured', 'Not configured'); + // One row per flow type this application can have, shown in the preview card. Sign-in and + // sign-out apply to every sign-in-facing app; sign-up and recovery are opt-in per application. + const flowRows: {label: string; value: string}[] = [ + { + label: t('applications:edit.overview.signInPreview.flows.signIn', 'Sign-in'), + value: authFlowDetails?.name ?? notConfiguredLabel, + }, + { + label: t('applications:edit.overview.signInPreview.flows.signUp', 'Sign-up'), + value: + application.isRegistrationFlowEnabled && application.registrationFlowId + ? (registrationFlowDetails?.name ?? notConfiguredLabel) + : notConfiguredLabel, + }, + { + label: t('applications:edit.overview.signInPreview.flows.recovery', 'Password recovery'), + value: + application.isRecoveryFlowEnabled && application.recoveryFlowId + ? (recoveryFlowDetails?.name ?? notConfiguredLabel) + : notConfiguredLabel, + }, + { + label: t('applications:edit.overview.signInPreview.flows.signOut', 'Sign-out'), + value: signOutFlowDetails?.name ?? notConfiguredLabel, + }, + ]; + + const placeholderValues = { + productName, + clientId: oauth2Config?.clientId, + applicationId: application.id, + }; + + const handleCopyPrompt = async () => { + if (!promptUrl) { + return; + } + + setIsFetchingPrompt(true); + + try { + const response = await fetch(promptUrl); + + if (!response.ok) { + throw new Error(`Failed to fetch prompt: ${response.status}`); + } + + const promptText = await response.text(); + + await navigator.clipboard.writeText(replacePlaceholders(promptText, placeholderValues)); + setPromptCopied(true); + setTimeout(() => setPromptCopied(false), 1500); + } catch { + logger.error('Failed to copy the coding agent prompt to clipboard'); + } finally { + setIsFetchingPrompt(false); + } + }; + + const serverUrl = getServerUrl(); + const flowEndpoints = [ + { + key: 'flowExecute', + label: t('applications:edit.overview.endpoints.flowExecute', 'Flow execution endpoint'), + url: `${serverUrl}/flow/execute`, + }, + { + key: 'flowMeta', + label: t('applications:edit.overview.endpoints.flowMeta', 'Flow metadata endpoint'), + url: `${serverUrl}/flow/meta`, + }, + { + key: 'passkeyRegisterStart', + label: t('applications:edit.overview.endpoints.passkeyRegisterStart', 'Passkey registration (start)'), + url: `${serverUrl}/register/passkey/start`, + }, + { + key: 'passkeyRegisterFinish', + label: t('applications:edit.overview.endpoints.passkeyRegisterFinish', 'Passkey registration (finish)'), + url: `${serverUrl}/register/passkey/finish`, + }, + ]; + const oauthEndpoints = [ + { + key: 'wellknown', + label: t('applications:edit.overview.endpoints.wellknown', 'OpenID configuration'), + url: `${serverUrl}/.well-known/openid-configuration`, + }, + { + key: 'authorization', + label: t('applications:edit.overview.endpoints.authorization', 'Authorization endpoint'), + url: `${serverUrl}/oauth2/authorize`, + }, + { + key: 'token', + label: t('applications:edit.overview.endpoints.token', 'Token endpoint'), + url: `${serverUrl}/oauth2/token`, + }, + { + key: 'userinfo', + label: t('applications:edit.overview.endpoints.userinfo', 'Userinfo endpoint'), + url: `${serverUrl}/oauth2/userinfo`, + }, + { + key: 'jwks', + label: t('applications:edit.overview.endpoints.jwks', 'JWKS URI'), + url: `${serverUrl}/oauth2/jwks`, + }, + ]; + // Apps with their own backend/native runtime ("App Native", per the Flow Execution API) drive + // sign-in/registration step by step against the flow endpoints directly, with a fully custom + // UI, unlike the browser-redirect OAuth2/OIDC endpoints. Custom apps don't commit to either + // shape, so they get both groups. + const endpoints = [...(showOAuthEndpoints ? oauthEndpoints : []), ...(showFlowEndpoints ? flowEndpoints : [])]; + + const colorMode: 'light' | 'dark' = (mode === 'system' ? systemMode : mode) === 'dark' ? 'dark' : 'light'; + const themeSwatchColor = themeDetails?.theme.vars?.palette.primary.main; + const hasVisibleQuickstart = Boolean(primaryQuickstartDocsUrl) || visibleQuickstarts.length > 0; + const endpointsCard = (showOAuthEndpoints || showFlowEndpoints) && ( + + + {endpoints.map((endpoint) => ( + + ))} + + + ); + // Sign-in-facing apps already fill the main column with a quickstart and a sizeable preview + // card, so the endpoints card stays paired with identifiers in the sidebar as before. Apps with + // no preview (Custom, M2M-only) would otherwise leave the main column nearly empty and the + // endpoints card cramped into, or lopsided against, the narrow sidebar, so for those the + // endpoints card moves into the main column instead. + const showEndpointsInSidebar = isSignInFacing; + const hasMainContent = hasVisibleQuickstart || Boolean(promptUrl) || isSignInFacing || Boolean(endpointsCard); + + const detailsCard = ( + + + + {oauth2Config?.clientId && ( + + )} + + + ); return ( - - {integrationGuides ? ( - - ) : ( - - - {t('applications:edit.overview.noGuides')} - - + + {/* Left: main content */} + {hasMainContent && ( + + {primaryPlaygroundUrl && ( + , + }} + /> + } + ctaLabel={t('applications:edit.overview.stackblitz.cta', 'Open on StackBlitz')} + /> + )} + + {(hasVisibleQuickstart || promptUrl) && ( + + {primaryQuickstartDocsUrl && ( + {QUICKSTART_ICONS[primaryQuickstart!.label] ?? } + } + title={t('applications:edit.overview.readGuide.title', 'Read the quickstart guide')} + description={t( + 'applications:edit.overview.readGuide.description', + 'Step-by-step integration guide on the docs site.', + )} + sx={{flex: '1 1 240px'}} + > + externalLinkConfirmation.requestNavigation(primaryQuickstartDocsUrl)} + > + {t('applications:edit.overview.readGuide.action', 'Open quickstart')} + + + + )} + + {visibleQuickstarts.map((entry) => ( + {QUICKSTART_ICONS[entry.label] ?? }} + title={t('applications:edit.overview.readGuide.titleFor', '{{label}} quickstart guide', { + label: entry.label, + })} + description={t( + 'applications:edit.overview.readGuide.descriptionFor', + 'Step-by-step {{label}} integration guide on the docs site.', + {label: entry.label}, + )} + sx={{flex: '1 1 240px'}} + > + externalLinkConfirmation.requestNavigation(entry.docsUrl)} + > + {t('applications:edit.overview.readGuide.action', 'Open quickstart')} + + + + ))} + + {promptUrl && ( + + + + } + title={ + + {t('applications:edit.overview.agentPrompt.title', 'Integrate with a coding agent')} + + + } + description={t( + 'applications:edit.overview.agentPrompt.description', + 'Copy a ready-made prompt for Claude, Cursor, or any agent.', + )} + sx={{flex: '1 1 240px'}} + > + + + )} + + )} + + {isSignInFacing && ( + + + + {isNativeApplication && ( + + + + )} + + + + + + + + + + {t('applications:edit.overview.signInPreview.themeLabel', 'Theme used')} + + {onGoToCustomization && ( + + )} + + + + + {themeDetails?.displayName ?? + t('applications:edit.overview.signInPreview.defaultTheme', 'Default')} + + + + + + + + {t('applications:edit.overview.signInPreview.flowsLabel', 'Flows')} + + {onGoToFlows && ( + + )} + + + {flowRows.map((flow, index) => ( + + + {flow.label} + + + {flow.value} + + + ))} + + + + + + )} + + {!showEndpointsInSidebar && endpointsCard} + )} + + {/* Right: identifiers sidebar. Also holds the endpoints card for sign-in-facing apps, whose + main column is already full without it. */} + + {detailsCard} + {showEndpointsInSidebar && endpointsCard} + + + ); } diff --git a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/TechnologyGuide.tsx b/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/TechnologyGuide.tsx index 0e4d832bf8..2f6fab27ea 100644 --- a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/TechnologyGuide.tsx +++ b/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/TechnologyGuide.tsx @@ -3,15 +3,14 @@ import {useConfig} from '@thunderid/contexts'; import {useLogger} from '@thunderid/logger'; -import {Box, Typography, Stack, Card, CardContent, Divider, Paper, IconButton, Tooltip} from '@wso2/oxygen-ui'; +import {Box, Typography, Stack, Card, CardContent, Tooltip} from '@wso2/oxygen-ui'; import {Sparkles, Copy} from '@wso2/oxygen-ui-icons-react'; import type {JSX, MouseEvent} from 'react'; import {useState} from 'react'; import {useTranslation} from 'react-i18next'; -import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'; -import {vscDarkPlus} from 'react-syntax-highlighter/dist/esm/styles/prism'; -import type {IntegrationGuides, IntegrationStep} from '../../../models/application-templates'; +import type {IntegrationGuides} from '../../../models/application-templates'; import {getIntegrationGuideVariantKey} from '../../../utils/getIntegrationGuidesForTemplate'; +import resolveTemplateLink from '../../../utils/resolveTemplateLink'; import GradientBorderButton from '../../GradientBorderButton'; /** @@ -21,7 +20,7 @@ import GradientBorderButton from '../../GradientBorderButton'; */ export interface TechnologyGuideProps { /** - * Integration guides structure containing LLM prompt and manual steps + * Integration guides structure containing the LLM prompt guide */ guides: IntegrationGuides | null; /** @@ -39,22 +38,19 @@ export interface TechnologyGuideProps { } /** - * React component that displays integration guide options for technology templates. + * React component that displays the integration guide option for technology templates. * - * This component renders: - * 1. LLM prompt option as a clickable card - * 2. Divider with "or" text - * 3. Step-by-step integration guide using custom timeline layout + * This component renders the LLM prompt option as a clickable card. * - * The displayed steps vary based on the template ID: + * The displayed guide varies based on the template ID: * - Templates with '-embedded' suffix (e.g., 'react-embedded'): Shows 'embedded' guide for custom login UI - * - Templates without '-embedded' suffix (e.g., 'react'): Shows 'inbuilt' guide for product's hosted login + * - Templates without '-embedded' suffix (e.g., 'react'): Shows 'redirect_based' guide for product's hosted login * * @param props - The component props * @param props.guides - Integration guides structure * @param props.templateId - The template ID used to create the application * - * @returns JSX element displaying the integration guide options + * @returns JSX element displaying the integration guide option * * @public */ @@ -66,11 +62,11 @@ export default function TechnologyGuide({ }: TechnologyGuideProps): JSX.Element | null { const logger = useLogger('TechnologyGuide'); const {t} = useTranslation(); - const {config} = useConfig(); + const {config, getDocumentationLink} = useConfig(); const productName = config.brand.product_name; - const [copiedStep, setCopiedStep] = useState(null); const [copiedPrompt, setCopiedPrompt] = useState(false); + const [isFetchingPrompt, setIsFetchingPrompt] = useState(false); if (!guides) { return null; @@ -78,14 +74,15 @@ export default function TechnologyGuide({ // Select the guide for the variant determined by the template ID. // Templates with the -embedded suffix (e.g., react-embedded) use the EMBEDDED guide; - // all others use the INBUILT guide. + // all others use the REDIRECT_BASED guide. const selectedGuide = guides[getIntegrationGuideVariantKey(templateId)]; if (!selectedGuide) { return null; } - const {llm_prompt: llmPrompt, manual_steps: manualSteps} = selectedGuide; + const {llm_prompt: llmPrompt} = selectedGuide; + const promptUrl = resolveTemplateLink(llmPrompt.docsUrl, getDocumentationLink); const replacePlaceholders = (text: string): string => { let result = text; @@ -107,21 +104,16 @@ export default function TechnologyGuide({ return result; }; - const handleCopyPrompt = async (e: MouseEvent): Promise => { - e.stopPropagation(); - if (!llmPrompt.content) return; - - const contentWithReplacements = replacePlaceholders(llmPrompt.content); - + const copyText = async (text: string): Promise => { try { - await navigator.clipboard.writeText(contentWithReplacements); + await navigator.clipboard.writeText(text); setCopiedPrompt(true); setTimeout(() => setCopiedPrompt(false), 2000); } catch { // Fallback for older browsers const textArea = document.createElement('textarea'); - textArea.value = contentWithReplacements; + textArea.value = text; textArea.style.position = 'fixed'; textArea.style.opacity = '0'; document.body.appendChild(textArea); @@ -139,135 +131,27 @@ export default function TechnologyGuide({ } }; - const handleCopyCode = async (code: string, stepNumber: number): Promise => { - const codeWithReplacements = replacePlaceholders(code); + const handleCopyPrompt = async (e: MouseEvent): Promise => { + e.stopPropagation(); + if (!promptUrl) return; - try { - await navigator.clipboard.writeText(codeWithReplacements); - setCopiedStep(stepNumber); - setTimeout(() => setCopiedStep(null), 2000); - } catch { - // Fallback for older browsers - const textArea = document.createElement('textarea'); + setIsFetchingPrompt(true); - textArea.value = codeWithReplacements; - textArea.style.position = 'fixed'; - textArea.style.opacity = '0'; - document.body.appendChild(textArea); - textArea.select(); + try { + const response = await fetch(promptUrl); - try { - document.execCommand('copy'); - setCopiedStep(stepNumber); - setTimeout(() => setCopiedStep(null), 2000); - } catch { - logger.error('Failed to copy the code snippet to clipboard.'); + if (!response.ok) { + throw new Error(`Failed to fetch prompt: ${response.status}`); } - document.body.removeChild(textArea); - } - }; + const promptText = await response.text(); - const renderCodeBlock = (step: IntegrationStep): JSX.Element | null => { - if (!step.code) { - return null; + await copyText(replacePlaceholders(promptText)); + } catch (error) { + logger.error('Failed to fetch the integration prompt.', {error}); + } finally { + setIsFetchingPrompt(false); } - - const getLanguage = (lang: string): string => { - const languageMap: Record = { - terminal: 'bash', - '.env': 'properties', - typescript: 'tsx', - }; - return languageMap[lang] || lang; - }; - - const codeWithReplacements = replacePlaceholders(step.code.content); - - return ( - - {step.code.filename && ( - - - {step.code.filename} - - - )} - - - {codeWithReplacements} - - { - handleCopyCode(step.code!.content, step.step).catch(() => { - logger.error('Failed to copy the code snippet to clipboard.'); - }); - }} - sx={{ - position: 'absolute', - top: 8, - right: 8, - color: 'grey.400', - bgcolor: 'grey.800', - '&:hover': { - bgcolor: 'grey.700', - }, - }} - > - - - {copiedStep === step.step && ( - - {t('applications:clientSecret.copied')} - - )} - - - ); }; return ( @@ -292,16 +176,20 @@ export default function TechnologyGuide({
- {replacePlaceholders(llmPrompt.title)} + {t('applications:edit.overview.agentPrompt.title', 'Integrate with a coding agent')} - {replacePlaceholders(llmPrompt.description)} + {t( + 'applications:edit.overview.agentPrompt.description', + 'Copy a ready-made prompt for Claude, Cursor, or any agent.', + )} - {llmPrompt.content && ( + {promptUrl && ( { handleCopyPrompt(e).catch(() => { /* Error already handled */ @@ -317,83 +205,6 @@ export default function TechnologyGuide({ )} - - {/* Divider with "or" */} - {manualSteps && manualSteps.length > 0 && ( - - - {t('applications:onboarding.summary.guides.divider')} - - - )} - - {/* Manual Steps Timeline */} - {manualSteps && manualSteps.length > 0 && ( - - {manualSteps.map((step, index) => ( - - {/* Timeline dot and connector */} - - - - {step.step} - - - {index < manualSteps.length - 1 && ( - - )} - - - {/* Content */} - - - {replacePlaceholders(step.title)} - - - {replacePlaceholders(step.description)} - - {step.subDescription && ( - - {replacePlaceholders(step.subDescription)} - - )} - {step.bullets && step.bullets.length > 0 && ( - - {step.bullets.map((bullet) => ( - - - {replacePlaceholders(bullet)} - - - ))} - - )} - {renderCodeBlock(step)} - - - ))} - - )} ); } diff --git a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/__tests__/IntegrationGuide.test.tsx b/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/__tests__/IntegrationGuide.test.tsx deleted file mode 100644 index 3014464b66..0000000000 --- a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/__tests__/IntegrationGuide.test.tsx +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2026 The ThunderID Authors -// SPDX-License-Identifier: Apache-2.0 - -import {render, screen} from '@testing-library/react'; -import {describe, it, expect, vi, beforeEach} from 'vitest'; -import type {IntegrationGuides} from '../../../../models/application-templates'; -import IntegrationGuide from '../IntegrationGuide'; -import TechnologyGuide from '../TechnologyGuide'; - -// Mock the TechnologyGuide component -vi.mock('../TechnologyGuide', () => ({ - default: vi.fn(() =>
Technology Guide
), -})); - -const mockIntegrationGuides: IntegrationGuides = { - inbuilt: { - llm_prompt: { - id: 'llm-1', - title: 'Use AI to integrate', - description: 'Copy prompt for AI', - type: 'llm' as const, - icon: 'sparkles', - content: 'LLM prompt content', - }, - manual_steps: [ - { - step: 1, - title: 'Install dependencies', - description: 'Install required packages', - code: { - language: 'bash', - content: 'npm install', - }, - }, - ], - }, -}; - -describe('IntegrationGuide', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('Rendering', () => { - it('should render TechnologyGuide when integrationGuides are provided', () => { - render( - , - ); - - expect(screen.getByTestId('technology-guide')).toBeInTheDocument(); - }); - - it('should not render TechnologyGuide when integrationGuides is null', () => { - render( - , - ); - - expect(screen.queryByTestId('technology-guide')).not.toBeInTheDocument(); - }); - - it('should not render TechnologyGuide when integrationGuides is undefined', () => { - render(); - - expect(screen.queryByTestId('technology-guide')).not.toBeInTheDocument(); - }); - }); - - describe('Props Propagation', () => { - it('should pass all props to TechnologyGuide', () => { - render( - , - ); - - expect(TechnologyGuide).toHaveBeenCalledWith( - { - guides: mockIntegrationGuides, - templateId: 'react', - clientId: 'client-123', - applicationId: 'app-123', - }, - undefined, - ); - }); - - it('should pass empty string for clientId when not provided', () => { - render(); - - expect(TechnologyGuide).toHaveBeenCalledWith( - expect.objectContaining({ - clientId: '', - }), - undefined, - ); - }); - - it('should pass null for applicationId when not provided', () => { - render(); - - expect(TechnologyGuide).toHaveBeenCalledWith( - expect.objectContaining({ - applicationId: undefined, - }), - undefined, - ); - }); - - it('should handle undefined templateId', () => { - render( - , - ); - - expect(TechnologyGuide).toHaveBeenCalledWith( - expect.objectContaining({ - templateId: null, - }), - undefined, - ); - }); - }); - - describe('Layout', () => { - it('should render content in a Stack with proper styling', () => { - const {container} = render( - , - ); - - const stack = container.querySelector('.MuiStack-root'); - expect(stack).toBeInTheDocument(); - }); - }); -}); diff --git a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/__tests__/IntegrationGuides.test.tsx b/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/__tests__/IntegrationGuides.test.tsx index 4eec2bb91a..fc011de1ee 100644 --- a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/__tests__/IntegrationGuides.test.tsx +++ b/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/__tests__/IntegrationGuides.test.tsx @@ -1,32 +1,77 @@ // Copyright 2026 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {render, screen} from '@testing-library/react'; +import {render, screen, fireEvent, waitFor} from '@testing-library/react'; import type {Application, OAuth2Config} from '@thunderid/configure-applications'; -import {describe, it, expect, vi, beforeEach} from 'vitest'; -import getIntegrationGuidesForTemplate from '../../../../utils/getIntegrationGuidesForTemplate'; -import IntegrationGuide from '../IntegrationGuide'; +import {LoggerProvider, LogLevel} from '@thunderid/logger'; +import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest'; import IntegrationGuides from '../IntegrationGuides'; -// Mock the integration guide utility -vi.mock('../../../../utils/getIntegrationGuidesForTemplate', () => ({ - default: vi.fn(), +const mockGetServerUrl = vi.fn(() => 'https://localhost:8090'); +const mockGetDocumentationLink = vi.fn((key: string) => documentationLinks[key]); + +const documentationLinks: Record = { + 'applications.templates.react.docs': + 'https://thunderid.dev/docs/next/guides/getting-started/connect-your-application/react/', + 'applications.templates.react.playground': + 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/react/quickstart?file=README.md', + 'applications.templates.react.llmPrompt.redirectBased': 'https://thunderid.dev/prompts/react/redirect-based.txt', + 'applications.templates.nextjs.llmPrompt.redirectBased': 'https://thunderid.dev/prompts/nextjs/redirect-based.txt', + 'applications.templates.ios.docs': + 'https://thunderid.dev/docs/next/guides/getting-started/connect-your-application/ios/', + 'applications.templates.android.docs': + 'https://thunderid.dev/docs/next/guides/getting-started/connect-your-application/android/', + 'applications.templates.android.llmPrompt.redirectBased': 'https://thunderid.dev/prompts/android/redirect-based.txt', + 'applications.templates.flutter.docs': + 'https://thunderid.dev/docs/next/guides/getting-started/connect-your-application/flutter/', +}; + +vi.mock('@thunderid/contexts', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useConfig: () => ({ + config: {brand: {product_name: 'ThunderID'}}, + getServerUrl: mockGetServerUrl, + getDocumentationLink: mockGetDocumentationLink, + }), + }; +}); + +vi.mock('@thunderid/design', () => ({ + useGetTheme: () => ({data: undefined, isLoading: false}), + DefaultTheme: {colorSchemes: {light: {palette: {}}, dark: {palette: {}}}}, })); -// Mock the child component -vi.mock('../IntegrationGuide', () => ({ - default: vi.fn(() =>
Integration Guide
), +vi.mock('../../../../../flows/api/useGetFlowById', () => ({ + default: () => ({data: undefined, isLoading: false}), })); -const mockApplication: Application = { +vi.mock('../../../../../../components/GatePreview/GatePreview', () => ({ + default: () =>
, +})); + +const mockWriteText = vi.fn(); +const mockFetch = vi.fn(); + +const promptFixtures: Record = { + 'https://thunderid.dev/prompts/react/redirect-based.txt': 'Integrate {{productName}} with clientId: {{clientId}}', + 'https://thunderid.dev/prompts/nextjs/redirect-based.txt': 'Integrate {{productName}} with Next.js', +}; + +const renderWithProviders = (component: React.ReactElement) => + render({component}); + +const reactApplication: Application = { id: 'app-123', - name: 'Test Application', + name: 'Bifrost', template: 'react', + type: 'browser', description: 'Test description', allowedUserTypes: ['admin', 'user'], }; -const mockOAuth2Config: OAuth2Config = { +const oauth2Config: OAuth2Config = { clientId: 'client-123', clientSecret: 'secret-456', grantTypes: ['authorization_code'], @@ -36,151 +81,256 @@ const mockOAuth2Config: OAuth2Config = { redirectUris: ['https://example.com/callback'], }; -const mockIntegrationGuides = { - INBUILT: { - llm_prompt: { - id: 'llm-1', - title: 'Use AI to integrate', - description: 'Copy prompt for AI', - type: 'llm' as const, - icon: 'sparkles', - content: 'LLM prompt content', - }, - manual_steps: [ - { - step: 1, - title: 'Install dependencies', - description: 'Install required packages', - code: { - language: 'bash', - content: 'npm install', - }, - }, - ], - }, -}; - describe('IntegrationGuides', () => { + const originalClipboard = navigator.clipboard; + beforeEach(() => { - vi.clearAllMocks(); + vi.useFakeTimers({shouldAdvanceTime: true}); + mockWriteText.mockReset().mockResolvedValue(undefined); + mockGetDocumentationLink.mockImplementation((key: string) => documentationLinks[key]); + mockFetch.mockReset().mockImplementation((url: string) => + Promise.resolve({ + ok: true, + status: 200, + text: () => Promise.resolve(promptFixtures[url] ?? ''), + }), + ); + vi.stubGlobal('fetch', mockFetch); + Object.defineProperty(navigator, 'clipboard', { + value: {writeText: mockWriteText}, + writable: true, + configurable: true, + }); }); - describe('Rendering', () => { - it('should render integration guide when guides are available', () => { - vi.mocked(getIntegrationGuidesForTemplate).mockReturnValue(mockIntegrationGuides); + afterEach(() => { + vi.runOnlyPendingTimers(); + vi.useRealTimers(); + vi.unstubAllGlobals(); + Object.defineProperty(navigator, 'clipboard', {value: originalClipboard, writable: true, configurable: true}); + }); - render(); + it('always shows application details, even for a template with no quickstart', () => { + renderWithProviders(); - expect(screen.getByTestId('integration-guide')).toBeInTheDocument(); - }); + expect(screen.getByText('Application details')).toBeInTheDocument(); + expect(screen.getByText('app-123')).toBeInTheDocument(); + expect(screen.queryByRole('link', {name: /Open on StackBlitz/i})).not.toBeInTheDocument(); + }); - it('should render fallback message when no guides are available', () => { - vi.mocked(getIntegrationGuidesForTemplate).mockReturnValue(null); + it('shows the OIDC endpoints and sign-in preview from the canonical type before the OAuth2 config resolves', () => { + // oauth2Config isn't always resolved on first paint; the canonical application type (always + // present) is enough to know a 'browser' app is OAuth2-based and user-facing. + renderWithProviders(); - render(); + expect(screen.getByText('app-123')).toBeInTheDocument(); + expect(screen.getByText('Useful Endpoints')).toBeInTheDocument(); + expect(screen.getByText('Preview')).toBeInTheDocument(); + expect(screen.getByRole('link', {name: /Open on StackBlitz/i})).toBeInTheDocument(); + }); - expect(screen.getByText('No integration guides available for this application type.')).toBeInTheDocument(); - }); + it('hides the OIDC endpoints and sign-in preview for a custom application with no OAuth2 config', () => { + renderWithProviders(); + + expect(screen.queryByText('OIDC endpoints')).not.toBeInTheDocument(); + expect(screen.queryByText('Preview')).not.toBeInTheDocument(); }); - describe('Props Propagation', () => { - it('should pass clientId from oauth2Config to IntegrationGuide', () => { - vi.mocked(getIntegrationGuidesForTemplate).mockReturnValue(mockIntegrationGuides); + it('hides only the sign-in preview for a machine-to-machine application with no OAuth2 config yet', () => { + renderWithProviders(); - render(); + expect(screen.getByText('Useful Endpoints')).toBeInTheDocument(); + expect(screen.queryByText('Preview')).not.toBeInTheDocument(); + }); - expect(IntegrationGuide).toHaveBeenCalledWith( - { - clientId: 'client-123', - applicationId: 'app-123', - integrationGuides: mockIntegrationGuides, - templateId: 'react', - }, - undefined, - ); - }); + it('shows the OIDC endpoints (but not the Client ID row) when the OAuth2 config has no clientId yet', () => { + renderWithProviders( + , + ); - it('should pass empty clientId when oauth2Config is not provided', () => { - vi.mocked(getIntegrationGuidesForTemplate).mockReturnValue(mockIntegrationGuides); + expect(screen.getByText('Useful Endpoints')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/oauth2/authorize')).toBeInTheDocument(); + expect(screen.queryByText('Client ID')).not.toBeInTheDocument(); + }); - render(); + it('renders the StackBlitz quickstart card for a template with a quickstart sample', () => { + renderWithProviders(); - expect(IntegrationGuide).toHaveBeenCalledWith( - { - clientId: '', - applicationId: 'app-123', - integrationGuides: mockIntegrationGuides, - templateId: 'react', - }, - undefined, - ); - }); + const stackblitzLink = screen.getByRole('link', {name: /Open on StackBlitz/i}); + expect(stackblitzLink).toHaveAttribute( + 'href', + 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/react/quickstart?file=README.md', + ); + }); - it('should pass applicationId to IntegrationGuide', () => { - vi.mocked(getIntegrationGuidesForTemplate).mockReturnValue(mockIntegrationGuides); + it('hides the read-the-quickstart-guide card when the docs link is not configured', () => { + const documentationLinksWithoutDocs = {...documentationLinks}; + delete documentationLinksWithoutDocs['applications.templates.react.docs']; + mockGetDocumentationLink.mockImplementation((key: string) => documentationLinksWithoutDocs[key]); - render(); + renderWithProviders(); - expect(IntegrationGuide).toHaveBeenCalledWith( - { - clientId: '', - applicationId: 'app-123', - integrationGuides: mockIntegrationGuides, - templateId: 'react', - }, - undefined, - ); - }); + expect(screen.queryByText('Read the quickstart guide')).not.toBeInTheDocument(); + expect(screen.getByRole('link', {name: /Open on StackBlitz/i})).toBeInTheDocument(); + }); - it('should pass integrationGuides to IntegrationGuide', () => { - vi.mocked(getIntegrationGuidesForTemplate).mockReturnValue(mockIntegrationGuides); + it('shows the leaving-console confirmation before opening the docs guide', () => { + const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null); + renderWithProviders(); - render(); + fireEvent.click(screen.getByRole('button', {name: /Open quickstart/i})); + expect(screen.getByText('You are leaving ThunderID')).toBeInTheDocument(); - expect(IntegrationGuide).toHaveBeenCalledWith( - { - clientId: '', - applicationId: 'app-123', - integrationGuides: mockIntegrationGuides, - templateId: 'react', - }, - undefined, - ); - }); + fireEvent.click(screen.getByRole('button', {name: 'Continue'})); + expect(openSpy).toHaveBeenCalledWith( + 'https://thunderid.dev/docs/next/guides/getting-started/connect-your-application/react/', + '_blank', + 'noopener,noreferrer', + ); - it('should pass templateId from application to IntegrationGuide', () => { - vi.mocked(getIntegrationGuidesForTemplate).mockReturnValue(mockIntegrationGuides); + openSpy.mockRestore(); + }); - render(); + it('copies the coding agent prompt with placeholders replaced', async () => { + renderWithProviders(); - expect(IntegrationGuide).toHaveBeenCalledWith( - { - clientId: '', - applicationId: 'app-123', - integrationGuides: mockIntegrationGuides, - templateId: 'react', - }, - undefined, - ); + fireEvent.click(screen.getByRole('button', {name: /Copy prompt/i})); + + await waitFor(() => { + expect(mockWriteText).toHaveBeenCalledTimes(1); }); + const copiedText = mockWriteText.mock.calls[0][0] as string; + expect(copiedText).toContain('client-123'); + expect(copiedText).toContain('ThunderID'); + expect(copiedText).not.toContain('{{clientId}}'); + }); + + it('renders application identifiers and OIDC endpoints using the configured server URL', () => { + renderWithProviders(); + + expect(screen.getByText('app-123')).toBeInTheDocument(); + expect(screen.getByText('client-123')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/.well-known/openid-configuration')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/oauth2/authorize')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/oauth2/token')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/oauth2/userinfo')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/oauth2/jwks')).toBeInTheDocument(); + }); + + it('navigates to the Flows and Customization tabs via the sign-in preview links', () => { + const onGoToFlows = vi.fn(); + const onGoToCustomization = vi.fn(); + renderWithProviders( + , + ); + + fireEvent.click(screen.getByRole('button', {name: 'Edit in Flows'})); + expect(onGoToFlows).toHaveBeenCalledTimes(1); + + fireEvent.click(screen.getByRole('button', {name: 'Edit in Customization'})); + expect(onGoToCustomization).toHaveBeenCalledTimes(1); + }); + + it('does not show the sign-in preview for a machine-to-machine (client-credentials only) application', () => { + renderWithProviders( + , + ); + + expect(screen.queryByText('Preview')).not.toBeInTheDocument(); + expect(screen.getByText('Useful Endpoints')).toBeInTheDocument(); }); - describe('Template Utility Integration', () => { - it('should call getIntegrationGuidesForTemplate with template from application', () => { - vi.mocked(getIntegrationGuidesForTemplate).mockReturnValue(mockIntegrationGuides); + it('shows the coding agent prompt for a template whose default EMBEDDED variant has no guide content', () => { + // Android defaults to the EMBEDDED sign-in approach (it's always app-native) but only has + // REDIRECT_BASED guide content authored; every technology template should still get a + // coding-agent prompt card. + renderWithProviders( + , + ); + + expect(screen.getByRole('button', {name: /Copy prompt/i})).toBeInTheDocument(); + }); + + describe('native (mobile) applications', () => { + const mobileApplication: Application = { + ...reactApplication, + template: 'mobile', + type: 'mobile', + }; + + it('shows one quickstart guide card per platform and no StackBlitz banner', () => { + renderWithProviders(); + + expect(screen.queryByRole('link', {name: /Open on StackBlitz/i})).not.toBeInTheDocument(); + expect(screen.getByText('iOS quickstart guide')).toBeInTheDocument(); + expect(screen.getByText('Android quickstart guide')).toBeInTheDocument(); + expect(screen.getByText('Flutter quickstart guide')).toBeInTheDocument(); + }); + + it('shows App Native flow endpoints instead of the OAuth2/OIDC endpoints', () => { + renderWithProviders(); + + expect(screen.getByText('Useful Endpoints')).toBeInTheDocument(); + expect(screen.queryByText('OIDC endpoints')).not.toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/flow/execute')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/flow/meta')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/register/passkey/start')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/register/passkey/finish')).toBeInTheDocument(); + expect(screen.queryByText('https://localhost:8090/oauth2/authorize')).not.toBeInTheDocument(); + expect(screen.queryByText('https://localhost:8090/oauth2/token')).not.toBeInTheDocument(); + }); - render(); + it('shows the standard OAuth2/OIDC endpoints (not App Native ones) for a pure browser SPA', () => { + renderWithProviders(); - expect(getIntegrationGuidesForTemplate).toHaveBeenCalledWith('react'); + expect(screen.getByText('Useful Endpoints')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/oauth2/authorize')).toBeInTheDocument(); + expect(screen.queryByText('https://localhost:8090/flow/execute')).not.toBeInTheDocument(); + expect(screen.queryByText('https://localhost:8090/flow/meta')).not.toBeInTheDocument(); }); - it('should call getIntegrationGuidesForTemplate with empty string when template is not defined', () => { - vi.mocked(getIntegrationGuidesForTemplate).mockReturnValue(null); - const appWithoutTemplate = {...mockApplication, template: undefined}; + it('shows both OAuth2/OIDC and App Native endpoints for the Custom template', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('Useful Endpoints')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/oauth2/authorize')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/flow/execute')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/flow/meta')).toBeInTheDocument(); + }); + + it('also shows App Native flow endpoints for a fullstack application (e.g. Next.js)', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('Useful Endpoints')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/flow/execute')).toBeInTheDocument(); + expect(screen.getByText('https://localhost:8090/flow/meta')).toBeInTheDocument(); + }); - render(); + it('renders the sign-in preview in a phone-style frame', () => { + renderWithProviders(); - expect(getIntegrationGuidesForTemplate).toHaveBeenCalledWith(''); + expect(screen.getByTestId('gate-preview')).toBeInTheDocument(); }); }); }); diff --git a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/__tests__/TechnologyGuide.test.tsx b/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/__tests__/TechnologyGuide.test.tsx index ea0f079c54..c3f9ade580 100644 --- a/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/__tests__/TechnologyGuide.test.tsx +++ b/frontend/apps/console/src/features/applications/components/edit-application/integration-guides/__tests__/TechnologyGuide.test.tsx @@ -18,65 +18,34 @@ vi.mock('@thunderid/contexts', async (importOriginal) => { favicon: {light: 'assets/images/favicon.ico', dark: 'assets/images/favicon-inverted.ico'}, }, }, + getDocumentationLink: () => undefined, }), }; }); +const REDIRECT_BASED_PROMPT_URL = 'https://thunderid.dev/prompts/react/redirect-based.txt'; +const EMBEDDED_PROMPT_URL = 'https://thunderid.dev/prompts/react/embedded.txt'; + const mockIntegrationGuides: IntegrationGuides = { - INBUILT: { + REDIRECT_BASED: { llm_prompt: { - id: 'llm-1', - title: 'Use AI Assistant', - description: 'Get AI-powered integration guidance', - type: 'llm' as const, - icon: 'sparkles', - content: 'Integrate with clientId: {{clientId}} and applicationId: {{applicationId}}', + docsUrl: REDIRECT_BASED_PROMPT_URL, }, - manual_steps: [ - { - step: 1, - title: 'Install dependencies', - description: 'Install required packages for your application', - subDescription: 'Run the following command in your terminal', - bullets: ['npm for Node Package Manager', 'yarn for Yarn Package Manager'], - code: { - language: 'bash', - filename: 'terminal', - content: 'npm install @awesome-product/sdk', - }, - }, - { - step: 2, - title: 'Configure client', - description: 'Set up your application with the client ID', - code: { - language: 'typescript', - filename: 'config.ts', - content: 'const clientId = "{{clientId}}";', - }, - }, - ], }, EMBEDDED: { llm_prompt: { - id: 'llm-2', - title: 'Embedded Integration', - description: 'Custom login UI integration', - type: 'llm' as const, - icon: 'sparkles', - content: 'Embedded integration prompt', + docsUrl: EMBEDDED_PROMPT_URL, }, - manual_steps: [ - { - step: 1, - title: 'Setup custom UI', - description: 'Create your custom login form', - }, - ], }, }; +const promptFixtures: Record = { + [REDIRECT_BASED_PROMPT_URL]: 'Integrate with clientId: {{clientId}} and applicationId: {{applicationId}}', + [EMBEDDED_PROMPT_URL]: 'Embedded integration prompt', +}; + const mockWriteText = vi.fn(); +const mockFetch = vi.fn(); const renderWithProviders = (component: React.ReactElement) => render({component}); @@ -88,6 +57,14 @@ describe('TechnologyGuide', () => { vi.useFakeTimers({shouldAdvanceTime: true}); vi.clearAllMocks(); mockWriteText.mockResolvedValue(undefined); + mockFetch.mockImplementation((url: string) => + Promise.resolve({ + ok: true, + status: 200, + text: () => Promise.resolve(promptFixtures[url] ?? ''), + }), + ); + vi.stubGlobal('fetch', mockFetch); Object.defineProperty(navigator, 'clipboard', { value: { writeText: mockWriteText, @@ -100,6 +77,7 @@ describe('TechnologyGuide', () => { afterEach(() => { vi.runOnlyPendingTimers(); vi.useRealTimers(); + vi.unstubAllGlobals(); Object.defineProperty(navigator, 'clipboard', { value: originalClipboard, writable: true, @@ -115,32 +93,45 @@ describe('TechnologyGuide', () => { }); it('should return null when selected guide is not found', () => { - const guidesWithoutInbuilt: IntegrationGuides = { - OTHER: mockIntegrationGuides.INBUILT, + const guidesWithoutRedirectBased: IntegrationGuides = { + OTHER: mockIntegrationGuides.REDIRECT_BASED, }; - const {container} = renderWithProviders(); + const {container} = renderWithProviders( + , + ); expect(container.firstChild?.firstChild).toBeFalsy(); }); - it('should render inbuilt guide for non-embedded template', () => { + it('should render redirect-based guide for non-embedded template', async () => { renderWithProviders(); - expect(screen.getByText('Use AI Assistant')).toBeInTheDocument(); - expect(screen.getByText('Get AI-powered integration guidance')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('copy-prompt-button')); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledWith(REDIRECT_BASED_PROMPT_URL); + expect(mockWriteText).toHaveBeenCalledWith( + 'Integrate with clientId: {{clientId}} and applicationId: {{applicationId}}', + ); + }); }); - it('should render embedded guide for embedded template', () => { + it('should render embedded guide for embedded template', async () => { renderWithProviders(); - expect(screen.getByText('Embedded Integration')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('copy-prompt-button')); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledWith(EMBEDDED_PROMPT_URL); + expect(mockWriteText).toHaveBeenCalledWith('Embedded integration prompt'); + }); }); - it('should default to inbuilt guide when templateId is null', () => { + it('should default to redirect-based guide when templateId is null', () => { renderWithProviders(); - expect(screen.getByText('Use AI Assistant')).toBeInTheDocument(); + expect(screen.getByText('Integrate with a coding agent')).toBeInTheDocument(); }); }); @@ -148,8 +139,8 @@ describe('TechnologyGuide', () => { it('should render LLM prompt card with title and description', () => { renderWithProviders(); - expect(screen.getByText('Use AI Assistant')).toBeInTheDocument(); - expect(screen.getByText('Get AI-powered integration guidance')).toBeInTheDocument(); + expect(screen.getByText('Integrate with a coding agent')).toBeInTheDocument(); + expect(screen.getByText('Copy a ready-made prompt for Claude, Cursor, or any agent.')).toBeInTheDocument(); }); it('should render copy prompt button', () => { @@ -160,140 +151,21 @@ describe('TechnologyGuide', () => { }); }); - describe('Manual Steps Section', () => { - it('should render divider with "or" text', () => { - renderWithProviders(); - - expect(screen.getByText('or')).toBeInTheDocument(); - }); - - it('should render all manual steps', () => { - renderWithProviders(); - - expect(screen.getByText('Install dependencies')).toBeInTheDocument(); - expect(screen.getByText('Configure client')).toBeInTheDocument(); - }); - - it('should render step numbers', () => { - renderWithProviders(); - - expect(screen.getByText('1')).toBeInTheDocument(); - expect(screen.getByText('2')).toBeInTheDocument(); - }); - - it('should render step descriptions', () => { - renderWithProviders(); - - expect(screen.getByText('Install required packages for your application')).toBeInTheDocument(); - expect(screen.getByText('Set up your application with the client ID')).toBeInTheDocument(); - }); - - it('should render sub-descriptions when provided', () => { - renderWithProviders(); - - expect(screen.getByText('Run the following command in your terminal')).toBeInTheDocument(); - }); - - it('should render bullet points when provided', () => { - renderWithProviders(); - - expect(screen.getByText('npm for Node Package Manager')).toBeInTheDocument(); - expect(screen.getByText('yarn for Yarn Package Manager')).toBeInTheDocument(); - }); - }); - - describe('Code Blocks', () => { - it('should render code blocks for steps with code', () => { - const {container} = renderWithProviders(); - - // Check that code blocks exist - const codeBlocks = container.querySelectorAll('pre'); - expect(codeBlocks).toHaveLength(2); - - // Check code content is present - expect(container.textContent).toContain('npm install @awesome-product/sdk'); - expect(container.textContent).toContain('const clientId = "{{clientId}}";'); - }); - - it('should render filenames when provided', () => { - renderWithProviders(); - - expect(screen.getByText('terminal')).toBeInTheDocument(); - expect(screen.getByText('config.ts')).toBeInTheDocument(); - }); - - it('should render copy buttons for each code block', () => { - renderWithProviders(); - - const copyButtons = screen.getAllByTestId(/copy-code-button-/); - expect(copyButtons).toHaveLength(2); - }); - }); - describe('Empty States', () => { - it('should not render code block when step has no code', () => { - const guidesWithoutCode: IntegrationGuides = { - INBUILT: { - llm_prompt: mockIntegrationGuides.INBUILT.llm_prompt, - manual_steps: [ - { - step: 1, - title: 'No code step', - description: 'This step has no code', - }, - ], - }, - }; - - const {container} = renderWithProviders(); - - const codeBlocks = container.querySelectorAll('pre'); - expect(codeBlocks).toHaveLength(0); - }); - - it('should not render manual steps section when manual_steps is empty', () => { - const guidesWithoutSteps: IntegrationGuides = { - INBUILT: { - llm_prompt: mockIntegrationGuides.INBUILT.llm_prompt, - manual_steps: [], - }, - }; - - renderWithProviders(); - - expect(screen.queryByText('or')).not.toBeInTheDocument(); - }); - - it('should not render copy prompt button when llm_prompt has no content', () => { - const guidesWithoutContent: IntegrationGuides = { - INBUILT: { - llm_prompt: { - id: 'llm-1', - title: 'Use AI Assistant', - description: 'Get AI-powered integration guidance', - type: 'llm' as const, - icon: 'sparkles', - }, - manual_steps: [], + it('should not render copy prompt button when llm_prompt has no docsUrl', () => { + const guidesWithoutDocsUrl: IntegrationGuides = { + REDIRECT_BASED: { + llm_prompt: {}, }, }; - renderWithProviders(); + renderWithProviders(); expect(screen.queryByTestId('copy-prompt-button')).not.toBeInTheDocument(); }); }); describe('Placeholder Replacement', () => { - it('should replace {{clientId}} placeholder in code blocks', () => { - const {container} = renderWithProviders( - , - ); - - expect(container.textContent).toContain('const clientId = "my-client-id";'); - expect(container.textContent).not.toContain('{{clientId}}'); - }); - it('should replace {{applicationId}} placeholder in LLM prompt when copied', async () => { renderWithProviders( { }); }); - it('should not replace placeholders when clientId is empty', () => { - const {container} = renderWithProviders( - , - ); - - expect(container.textContent).toContain('{{clientId}}'); - }); - it('should not replace applicationId placeholder when applicationId is empty', async () => { renderWithProviders( , @@ -364,63 +228,34 @@ describe('TechnologyGuide', () => { }); }); - it('should copy code to clipboard when copy code button is clicked', async () => { - renderWithProviders( - , - ); + it('should not call fetch when prompt has no docsUrl', () => { + const guidesWithoutDocsUrl: IntegrationGuides = { + REDIRECT_BASED: { + llm_prompt: { + docsUrl: '', + }, + }, + }; - const copyCodeButton = screen.getByTestId('copy-code-button-1'); - fireEvent.click(copyCodeButton); + renderWithProviders(); - await waitFor(() => { - expect(mockWriteText).toHaveBeenCalledWith('npm install @awesome-product/sdk'); - }); + // Button should not render when docsUrl is an empty string + expect(screen.queryByTestId('copy-prompt-button')).toBeNull(); + expect(mockFetch).not.toHaveBeenCalled(); + expect(mockWriteText).not.toHaveBeenCalled(); }); - it('should show copied feedback after copying code', async () => { - renderWithProviders(); - - const copyCodeButton = screen.getByTestId('copy-code-button-1'); - fireEvent.click(copyCodeButton); + it('should log an error and not copy when fetching the prompt fails', async () => { + mockFetch.mockResolvedValue({ok: false, status: 500, text: () => Promise.resolve('')}); - // The copied feedback is shown as translated text - await waitFor(() => { - expect(screen.getByText('Copied to clipboard')).toBeInTheDocument(); - }); - }); - - it('should replace placeholders in copied code', async () => { - renderWithProviders( - , - ); + renderWithProviders(); - const copyCodeButton = screen.getByTestId('copy-code-button-2'); - fireEvent.click(copyCodeButton); + const copyButton = screen.getByTestId('copy-prompt-button'); + fireEvent.click(copyButton); await waitFor(() => { - expect(mockWriteText).toHaveBeenCalledWith('const clientId = "replaced-client-id";'); + expect(mockFetch).toHaveBeenCalledWith(REDIRECT_BASED_PROMPT_URL); }); - }); - - it('should not call clipboard when prompt has no content', () => { - const guidesWithEmptyContent: IntegrationGuides = { - INBUILT: { - llm_prompt: { - id: 'llm-1', - title: 'Use AI Assistant', - description: 'Get AI-powered integration guidance', - type: 'llm' as const, - icon: 'sparkles', - content: '', - }, - manual_steps: [], - }, - }; - - renderWithProviders(); - - // Button should not render when content is empty string - expect(screen.queryByTestId('copy-prompt-button')).toBeNull(); expect(mockWriteText).not.toHaveBeenCalled(); }); @@ -441,22 +276,6 @@ describe('TechnologyGuide', () => { }); }); - it('should use fallback method when clipboard API fails for code', async () => { - mockWriteText.mockRejectedValue(new Error('Clipboard API failed')); - - const mockExecCommand = vi.fn().mockReturnValue(true); - document.execCommand = mockExecCommand; - - renderWithProviders(); - - const copyCodeButton = screen.getByTestId('copy-code-button-1'); - fireEvent.click(copyCodeButton); - - await waitFor(() => { - expect(mockExecCommand).toHaveBeenCalledWith('copy'); - }); - }); - it('should handle fallback failure gracefully for prompt', () => { mockWriteText.mockRejectedValue(new Error('Clipboard API failed')); @@ -472,169 +291,6 @@ describe('TechnologyGuide', () => { // Should not throw - component handles error gracefully expect(() => fireEvent.click(copyButton)).not.toThrow(); }); - - it('should handle fallback failure gracefully for code', () => { - mockWriteText.mockRejectedValue(new Error('Clipboard API failed')); - - const mockExecCommand = vi.fn().mockImplementation(() => { - throw new Error('execCommand failed'); - }); - document.execCommand = mockExecCommand; - - renderWithProviders(); - - const copyCodeButton = screen.getByTestId('copy-code-button-1'); - - // Should not throw - component handles error gracefully - expect(() => fireEvent.click(copyCodeButton)).not.toThrow(); - }); - }); - }); - - describe('Code Block Language Mapping', () => { - it('should map terminal language to bash', () => { - const guidesWithTerminal: IntegrationGuides = { - INBUILT: { - llm_prompt: mockIntegrationGuides.INBUILT.llm_prompt, - manual_steps: [ - { - step: 1, - title: 'Run command', - description: 'Execute this command', - code: { - language: 'terminal', - content: 'npm install', - }, - }, - ], - }, - }; - - const {container} = renderWithProviders(); - - const codeBlock = container.querySelector('pre'); - expect(codeBlock).toBeInTheDocument(); - }); - - it('should map .env language to properties', () => { - const guidesWithEnv: IntegrationGuides = { - INBUILT: { - llm_prompt: mockIntegrationGuides.INBUILT.llm_prompt, - manual_steps: [ - { - step: 1, - title: 'Configure env', - description: 'Set environment variables', - code: { - language: '.env', - filename: '.env', - content: 'API_KEY=your-key', - }, - }, - ], - }, - }; - - const {container} = renderWithProviders(); - - const codeBlock = container.querySelector('pre'); - expect(codeBlock).toBeInTheDocument(); - }); - - it('should map typescript language to tsx', () => { - const guidesWithTs: IntegrationGuides = { - INBUILT: { - llm_prompt: mockIntegrationGuides.INBUILT.llm_prompt, - manual_steps: [ - { - step: 1, - title: 'Add code', - description: 'Add TypeScript code', - code: { - language: 'typescript', - content: 'const x: string = "test";', - }, - }, - ], - }, - }; - - const {container} = renderWithProviders(); - - const codeBlock = container.querySelector('pre'); - expect(codeBlock).toBeInTheDocument(); - }); - - it('should pass through unknown languages unchanged', () => { - const guidesWithPython: IntegrationGuides = { - INBUILT: { - llm_prompt: mockIntegrationGuides.INBUILT.llm_prompt, - manual_steps: [ - { - step: 1, - title: 'Python code', - description: 'Add Python code', - code: { - language: 'python', - content: 'print("hello")', - }, - }, - ], - }, - }; - - const {container} = renderWithProviders(); - - const codeBlock = container.querySelector('pre'); - expect(codeBlock).toBeInTheDocument(); - }); - - it('should render code block without filename header when filename is not provided', () => { - const guidesWithoutFilename: IntegrationGuides = { - INBUILT: { - llm_prompt: mockIntegrationGuides.INBUILT.llm_prompt, - manual_steps: [ - { - step: 1, - title: 'Run command', - description: 'Execute this command', - code: { - language: 'bash', - content: 'npm install', - }, - }, - ], - }, - }; - - renderWithProviders(); - - // Should not have a filename displayed - expect(screen.queryByText('terminal')).not.toBeInTheDocument(); - expect(screen.queryByText('config.ts')).not.toBeInTheDocument(); - }); - }); - - describe('Bullets Rendering', () => { - it('should not render bullets section when bullets array is empty', () => { - const guidesWithEmptyBullets: IntegrationGuides = { - INBUILT: { - llm_prompt: mockIntegrationGuides.INBUILT.llm_prompt, - manual_steps: [ - { - step: 1, - title: 'Step without bullets', - description: 'This step has empty bullets array', - bullets: [], - }, - ], - }, - }; - - const {container} = renderWithProviders(); - - const bulletLists = container.querySelectorAll('ul'); - expect(bulletLists).toHaveLength(0); }); }); }); diff --git a/frontend/apps/console/src/features/applications/config/TechnologyBasedApplicationTemplateMetadata.tsx b/frontend/apps/console/src/features/applications/config/TechnologyBasedApplicationTemplateMetadata.tsx index 30b85a5e9c..097d4e3fd2 100644 --- a/frontend/apps/console/src/features/applications/config/TechnologyBasedApplicationTemplateMetadata.tsx +++ b/frontend/apps/console/src/features/applications/config/TechnologyBasedApplicationTemplateMetadata.tsx @@ -1,8 +1,12 @@ // Copyright 2025 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 +import {AndroidLogo, AppleIcon, FlutterLogo} from '@thunderid/components'; import {MCP} from '@wso2/oxygen-ui-icons-react'; +import AndroidTemplate from '../data/application-templates/technology-based/android.json'; import ExpressTemplate from '../data/application-templates/technology-based/express.json'; +import FlutterTemplate from '../data/application-templates/technology-based/flutter.json'; +import IOSTemplate from '../data/application-templates/technology-based/ios.json'; import MCPClientTemplate from '../data/application-templates/technology-based/mcp-client.json'; import NextJSTemplate from '../data/application-templates/technology-based/nextjs.json'; import NodeTemplate from '../data/application-templates/technology-based/node.json'; @@ -165,6 +169,30 @@ const TechnologyBasedApplicationTemplateMetadata: ApplicationTemplateMetadata, + titleKey: 'applications:onboarding.configure.stack.technology.ios.title', + descriptionKey: 'applications:onboarding.configure.stack.technology.ios.description', + template: IOSTemplate as ApplicationTemplate, + categories: ['mobile'], + }, + { + value: TechnologyApplicationTemplate.ANDROID, + icon: , + titleKey: 'applications:onboarding.configure.stack.technology.android.title', + descriptionKey: 'applications:onboarding.configure.stack.technology.android.description', + template: AndroidTemplate as ApplicationTemplate, + categories: ['mobile'], + }, + { + value: TechnologyApplicationTemplate.FLUTTER, + icon: , + titleKey: 'applications:onboarding.configure.stack.technology.flutter.title', + descriptionKey: 'applications:onboarding.configure.stack.technology.flutter.description', + template: FlutterTemplate as ApplicationTemplate, + categories: ['mobile'], + }, { value: TechnologyApplicationTemplate.MCP_CLIENT, icon: , diff --git a/frontend/apps/console/src/features/applications/constants/template-constants.ts b/frontend/apps/console/src/features/applications/constants/template-constants.ts index 8e9cdc64f4..dce68df24b 100644 --- a/frontend/apps/console/src/features/applications/constants/template-constants.ts +++ b/frontend/apps/console/src/features/applications/constants/template-constants.ts @@ -7,7 +7,7 @@ const TemplateConstants = { /** * Template modifier suffix for embedded (inbuilt) approach. - * Appended to technology template IDs when INBUILT approach is selected. + * Appended to technology template IDs when REDIRECT_BASED approach is selected. */ EMBEDDED_SUFFIX: '-embedded', diff --git a/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/ApplicationCreateContext.tsx b/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/ApplicationCreateContext.tsx index 84edd2734b..fdd77e343f 100644 --- a/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/ApplicationCreateContext.tsx +++ b/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/ApplicationCreateContext.tsx @@ -176,7 +176,7 @@ export interface ApplicationCreateContextType { setSmsOtpSenderId: (senderId: string) => void; /** - * The selected sign-in approach (INBUILT or CUSTOM). + * The selected sign-in approach (REDIRECT_BASED or CUSTOM). * @remark Needed for step 04: Configure Approach. */ signInApproach: ApplicationCreateFlowSignInApproach; diff --git a/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/ApplicationCreateProvider.tsx b/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/ApplicationCreateProvider.tsx index debaa388ee..d889c381c7 100644 --- a/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/ApplicationCreateProvider.tsx +++ b/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/ApplicationCreateProvider.tsx @@ -101,7 +101,7 @@ const INITIAL_STATE: { isSmsOtpMfaEnabled: false, smsOtpSenderId: '', selectedAuthFlow: null, - signInApproach: ApplicationCreateFlowSignInApproach.INBUILT as ApplicationCreateFlowSignInApproach, + signInApproach: ApplicationCreateFlowSignInApproach.REDIRECT_BASED as ApplicationCreateFlowSignInApproach, registrationFlowId: null, isRegistrationFlowEnabled: false, recoveryFlowId: null, diff --git a/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/__tests__/ApplicationCreateContext.test.tsx b/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/__tests__/ApplicationCreateContext.test.tsx index 89638656ef..1ed52e4dbc 100644 --- a/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/__tests__/ApplicationCreateContext.test.tsx +++ b/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/__tests__/ApplicationCreateContext.test.tsx @@ -58,7 +58,7 @@ function TestWithMockValue() { setSmsOtpSenderId: vi.fn(), selectedAuthFlow: null, setSelectedAuthFlow: vi.fn(), - signInApproach: 'INBUILT', + signInApproach: 'REDIRECT_BASED', setSignInApproach: vi.fn(), registrationFlowId: null, setRegistrationFlowId: vi.fn(), @@ -172,7 +172,7 @@ describe('ApplicationCreateContext', () => { setSmsOtpSenderId: () => null, selectedAuthFlow: null, setSelectedAuthFlow: () => null, - signInApproach: 'INBUILT', + signInApproach: 'REDIRECT_BASED', setSignInApproach: () => null, registrationFlowId: null, setRegistrationFlowId: () => null, @@ -262,7 +262,7 @@ describe('ApplicationCreateContext', () => { setSmsOtpSenderId: () => null, selectedAuthFlow: null, // Should allow null setSelectedAuthFlow: () => null, - signInApproach: 'INBUILT', + signInApproach: 'REDIRECT_BASED', setSignInApproach: () => null, registrationFlowId: null, setRegistrationFlowId: () => null, diff --git a/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/__tests__/ApplicationCreateProvider.test.tsx b/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/__tests__/ApplicationCreateProvider.test.tsx index 67f6bba07d..ec62179bf5 100644 --- a/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/__tests__/ApplicationCreateProvider.test.tsx +++ b/frontend/apps/console/src/features/applications/contexts/ApplicationCreate/__tests__/ApplicationCreateProvider.test.tsx @@ -155,7 +155,9 @@ describe('ApplicationCreateProvider', () => { expect(screen.getByTestId('email-otp-mfa')).toHaveTextContent('false'); expect(screen.getByTestId('sms-otp-mfa')).toHaveTextContent('false'); expect(screen.getByTestId('sms-otp-sender-id')).toHaveTextContent(''); - expect(screen.getByTestId('sign-in-approach')).toHaveTextContent(ApplicationCreateFlowSignInApproach.INBUILT); + expect(screen.getByTestId('sign-in-approach')).toHaveTextContent( + ApplicationCreateFlowSignInApproach.REDIRECT_BASED, + ); expect(screen.getByTestId('selected-technology')).toHaveTextContent('null'); expect(screen.getByTestId('selected-platform')).toHaveTextContent('null'); expect(screen.getByTestId('mcp-client-type')).toHaveTextContent('userDelegated'); diff --git a/frontend/apps/console/src/features/applications/data/application-templates/platform-based/backend.json b/frontend/apps/console/src/features/applications/data/application-templates/platform-based/backend.json index 85953b3647..e0ac62c421 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/platform-based/backend.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/platform-based/backend.json @@ -3,6 +3,12 @@ "type": "m2m", "displayName": "Backend", "description": "Machine-to-machine backend service", + "quickstarts": [ + { + "label": "Node.js", + "docsUrl": "{{applications.templates.node.docs}}" + } + ], "creationFlow": { "steps": ["ORGANIZATION_UNIT", "DETAILS", "COMPLETE"], "previewSteps": [] diff --git a/frontend/apps/console/src/features/applications/data/application-templates/platform-based/browser.json b/frontend/apps/console/src/features/applications/data/application-templates/platform-based/browser.json index 9f567d6b0a..70594cb8cc 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/platform-based/browser.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/platform-based/browser.json @@ -3,6 +3,19 @@ "type": "browser", "displayName": "Browser", "description": "Web application running in browser", + "quickstarts": [ + { + "label": "JavaScript", + "docsUrl": "{{applications.templates.browser.docs}}" + } + ], + "playgrounds": [ + { + "label": "JavaScript", + "environment": "stackblitz", + "url": "{{applications.templates.browser.playground}}" + } + ], "capabilities": { "cors": true }, @@ -29,9 +42,18 @@ }, "fieldConstraints": { "oauth2": { - "publicClient": {"readOnly": true, "value": true}, - "pkceRequired": {"readOnly": true, "value": true}, - "tokenEndpointAuthMethod": {"readOnly": true, "value": "none"} + "publicClient": { + "readOnly": true, + "value": true + }, + "pkceRequired": { + "readOnly": true, + "value": true + }, + "tokenEndpointAuthMethod": { + "readOnly": true, + "value": "none" + } } } } diff --git a/frontend/apps/console/src/features/applications/data/application-templates/platform-based/full-stack.json b/frontend/apps/console/src/features/applications/data/application-templates/platform-based/full-stack.json index e0e2b1734d..d94cfefa5c 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/platform-based/full-stack.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/platform-based/full-stack.json @@ -3,6 +3,46 @@ "type": "fullstack", "displayName": "Full-Stack", "description": "Apps with both server and client code", + "quickstarts": [ + { + "label": "Next.js", + "docsUrl": "{{applications.templates.nextjs.docs}}" + }, + { + "label": "Nuxt", + "docsUrl": "{{applications.templates.nuxt.docs}}" + }, + { + "label": "Express", + "docsUrl": "{{applications.templates.express.docs}}" + }, + { + "label": "Node.js", + "docsUrl": "{{applications.templates.node.docs}}" + } + ], + "playgrounds": [ + { + "label": "Next.js", + "environment": "stackblitz", + "url": "{{applications.templates.nextjs.playground}}" + }, + { + "label": "Nuxt", + "environment": "stackblitz", + "url": "{{applications.templates.nuxt.playground}}" + }, + { + "label": "Express", + "environment": "stackblitz", + "url": "{{applications.templates.express.playground}}" + }, + { + "label": "Node.js", + "environment": "stackblitz", + "url": "{{applications.templates.node.playground}}" + } + ], "creationFlow": { "steps": ["ORGANIZATION_UNIT", "DETAILS", "SECURITY", "DESIGN", "CONFIGURE", "COMPLETE"], "previewSteps": ["DETAILS", "SECURITY", "DESIGN"] diff --git a/frontend/apps/console/src/features/applications/data/application-templates/platform-based/mobile.json b/frontend/apps/console/src/features/applications/data/application-templates/platform-based/mobile.json index 04a7e4fa06..ac60a3a625 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/platform-based/mobile.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/platform-based/mobile.json @@ -3,12 +3,28 @@ "type": "mobile", "displayName": "Mobile", "description": "Native mobile application", + "previewDevice": "mobile", + "quickstarts": [ + { + "label": "iOS", + "docsUrl": "{{applications.templates.ios.docs}}" + }, + { + "label": "Android", + "docsUrl": "{{applications.templates.android.docs}}" + }, + { + "label": "Flutter", + "docsUrl": "{{applications.templates.flutter.docs}}" + } + ], "creationFlow": { "steps": ["ORGANIZATION_UNIT", "DETAILS", "SECURITY", "DESIGN", "CONFIGURE", "COMPLETE"], "previewSteps": ["DETAILS", "SECURITY", "DESIGN"] }, "defaults": { "name": "Mobile Application", + "signInApproach": "EMBEDDED", "inboundAuthConfig": [ { "type": "oauth2", diff --git a/frontend/apps/console/src/features/applications/data/application-templates/platform-based/wallet.json b/frontend/apps/console/src/features/applications/data/application-templates/platform-based/wallet.json index bf5c4bffd4..2add0168e6 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/platform-based/wallet.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/platform-based/wallet.json @@ -3,6 +3,20 @@ "type": "mobile", "displayName": "Digital Wallet", "description": "OpenID4VCI wallet that requests verifiable credentials", + "quickstarts": [ + { + "label": "iOS", + "docsUrl": "{{applications.templates.ios.docs}}" + }, + { + "label": "Android", + "docsUrl": "{{applications.templates.android.docs}}" + }, + { + "label": "Flutter", + "docsUrl": "{{applications.templates.flutter.docs}}" + } + ], "creationFlow": { "steps": ["ORGANIZATION_UNIT", "DETAILS", "SECURITY", "DESIGN", "CONFIGURE", "COMPLETE"], "previewSteps": ["DETAILS", "SECURITY", "DESIGN"] diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/android.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/android.json new file mode 100644 index 0000000000..328859e0f3 --- /dev/null +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/android.json @@ -0,0 +1,52 @@ +{ + "id": "android", + "type": "mobile", + "displayName": "Android", + "description": "Native Android application built with Jetpack Compose", + "previewDevice": "mobile", + "quickstarts": [ + { + "label": "Android", + "docsUrl": "{{applications.templates.android.docs}}" + } + ], + "creationFlow": { + "steps": ["ORGANIZATION_UNIT", "DETAILS", "SECURITY", "DESIGN", "CONFIGURE", "COMPLETE"], + "previewSteps": ["DETAILS", "SECURITY", "DESIGN"] + }, + "defaults": { + "name": "Android Application", + "signInApproach": "EMBEDDED", + "inboundAuthConfig": [ + { + "type": "oauth2", + "config": { + "grantTypes": ["authorization_code", "refresh_token"], + "responseTypes": ["code"], + "redirectUris": [], + "pkceRequired": true, + "tokenEndpointAuthMethod": "none", + "publicClient": true + } + } + ], + "allowedUserTypes": [] + }, + "fieldConstraints": { + "oauth2": { + "publicClient": {"readOnly": true, "value": true}, + "pkceRequired": {"readOnly": true, "value": true}, + "tokenEndpointAuthMethod": {"readOnly": true, "value": "none"} + } + }, + "capabilities": { + "attestation": true + }, + "integrationGuides": { + "REDIRECT_BASED": { + "llm_prompt": { + "docsUrl": "{{applications.templates.android.llmPrompt.redirectBased}}" + } + } + } +} diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/express.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/express.json index a2393115d9..ff2aedead2 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/express.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/express.json @@ -3,6 +3,19 @@ "type": "fullstack", "displayName": "Express", "description": "Server-side Node.js application built with Express", + "quickstarts": [ + { + "label": "Express", + "docsUrl": "{{applications.templates.express.docs}}" + } + ], + "playgrounds": [ + { + "label": "Express", + "environment": "stackblitz", + "url": "{{applications.templates.express.playground}}" + } + ], "creationFlow": { "steps": ["ORGANIZATION_UNIT", "DETAILS", "SECURITY", "DESIGN", "CONFIGURE", "COMPLETE"], "previewSteps": ["DETAILS", "SECURITY", "DESIGN"] @@ -37,76 +50,15 @@ } }, "integrationGuides": { - "INBUILT": { + "REDIRECT_BASED": { "llm_prompt": { - "id": "llm-prompt", - "title": "Integrate with a Coding Agent Prompt", - "description": "Use AI to generate integration code for your Express application", - "type": "llm", - "icon": "sparkles", - "content": "# Integrate {{productName}} Authentication in an Express Application (Inbuilt Mode)\n\n## Context\nI have an Express application and I want to integrate {{productName}} authentication using the ThunderID Express SDK with {{productName}}-hosted sign-in pages.\n\n## Requirements\n- Use `@thunderid/express` in a Node.js + Express application\n- Use `cookie-parser` and `express.json()` middleware\n- Configure ThunderID middleware with `baseUrl`, `clientId`, `clientSecret`, `afterSignInUrl`, and `afterSignOutUrl`\n- Implement `/login` with `handleSignIn()` and `/logout` with `handleSignOut()`\n- Protect a route (`/protected`) using `protect((res) => res.redirect('/login'))`\n- Add a `/me` route that returns the authenticated user profile as JSON\n- Keep the code minimal, production-lean, and fully runnable\n\n## Configuration\n- **Client ID**: {{clientId}}\n- **Client Secret**: ``\n- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL)\n- **App URL**: http://localhost:3000\n- **Login Callback Route**: `/login`\n- **Logout Callback Route**: `/logout`\n- **SDK**: @thunderid/express\n\n## Important Rules\n- Use CommonJS syntax (`require`) in `index.js`\n- Use these SDK imports exactly: `thunderID`, `handleSignIn`, `handleSignOut`, `protect`\n- Ensure redirect URL alignment: the app callback URL must be `http://localhost:3000/login`, and the post-logout redirect URL must be `http://localhost:3000/logout`\n- Do not invent unsupported SDK APIs or custom wrappers\n- Keep route names and behavior exactly as specified\n\n## Implementation Steps\n1. Create a new project and install `express` and `cookie-parser`\n2. Install `@thunderid/express`\n3. Add `index.js` with ThunderID middleware and auth routes\n4. Add `/protected` and `/me` routes with `protect()`\n5. Start the server with `node index.js`\n6. Validate the flow by opening `/protected`, then `/me`\n\nPlease provide:\n- The exact terminal commands\n- A complete `index.js` file\n- A short verification checklist for sign-in, sign-out, and protected route behavior" - }, - "manual_steps": [ - { - "step": 1, - "title": "Create an Express app", - "description": "Create your project and install the base dependencies:", - "code": { - "language": "terminal", - "content": "mkdir my-express-app\ncd my-express-app\nnpm init -y\nnpm install express cookie-parser" - } - }, - { - "step": 2, - "title": "Install @thunderid/express", - "description": "Install the ThunderID Express SDK package:", - "code": { - "language": "terminal", - "content": "npm install @thunderid/express" - } - }, - { - "step": 3, - "title": "Add ThunderID middleware and authentication routes", - "description": "Create an index.js file with middleware and auth route handlers:", - "code": { - "language": "javascript", - "filename": "index.js", - "content": "const express = require('express');\nconst cookieParser = require('cookie-parser');\nconst {thunderID, handleSignIn, handleSignOut, protect} = require('@thunderid/express');\n\nconst app = express();\nconst port = 3000;\n\napp.use(cookieParser());\napp.use(express.json());\n\napp.use(\n thunderID({\n baseUrl: 'https://localhost:8090',\n clientId: '{{clientId}}',\n clientSecret: '',\n afterSignInUrl: 'http://localhost:3000/login',\n afterSignOutUrl: 'http://localhost:3000/logout',\n }),\n);\n\napp.get('/', (_req, res) => {\n res.send('Go to protected page');\n});\n\napp.get('/login', handleSignIn());\napp.get('/logout', handleSignOut());\n\napp.get(\n '/protected',\n protect((res) => res.redirect('/login')),\n (_req, res) => {\n res.send('You are signed in and can access this protected route.');\n },\n);\n\napp.get('/me', protect(), async (req, res) => {\n const user = await req.thunderIDAuth.getUserFromRequest(req);\n res.json(user);\n});\n\napp.listen(port, () => {\n console.log(`Server running on http://localhost:${port}`);\n});" - } - }, - { - "step": 4, - "title": "Update credentials", - "description": "Replace the placeholders with your actual application credentials from {{productName}}:", - "bullets": [ - "Replace `{{clientId}}` with your Client ID", - "Replace `` with your Client Secret", - "Ensure your authorized redirect URL is `http://localhost:3000/login`", - "Ensure your allowed post-logout redirect URL is `http://localhost:3000/logout`" - ] - }, - { - "step": 5, - "title": "Run the app", - "description": "Start your Express server:", - "code": { - "language": "terminal", - "content": "node index.js" - } - }, - { - "step": 6, - "title": "Verify authentication flow", - "description": "Test the integration end-to-end:", - "bullets": [ - "Open `http://localhost:3000/protected` and verify redirect to {{productName}} sign-in", - "Sign in and confirm access to the protected route", - "Open `http://localhost:3000/me` to inspect the signed-in user profile JSON", - "Open `http://localhost:3000/logout` while signed in and verify sign-out" - ] - } - ] + "docsUrl": "{{applications.templates.express.llmPrompt.redirectBased}}" + } + }, + "EMBEDDED": { + "llm_prompt": { + "docsUrl": "{{applications.templates.express.llmPrompt.embedded}}" + } } } } diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/flutter.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/flutter.json new file mode 100644 index 0000000000..720025b9cf --- /dev/null +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/flutter.json @@ -0,0 +1,52 @@ +{ + "id": "flutter", + "type": "mobile", + "displayName": "Flutter", + "description": "Cross-platform mobile application built with Flutter", + "previewDevice": "mobile", + "quickstarts": [ + { + "label": "Flutter", + "docsUrl": "{{applications.templates.flutter.docs}}" + } + ], + "creationFlow": { + "steps": ["ORGANIZATION_UNIT", "DETAILS", "SECURITY", "DESIGN", "CONFIGURE", "COMPLETE"], + "previewSteps": ["DETAILS", "SECURITY", "DESIGN"] + }, + "defaults": { + "name": "Flutter Application", + "signInApproach": "EMBEDDED", + "inboundAuthConfig": [ + { + "type": "oauth2", + "config": { + "grantTypes": ["authorization_code", "refresh_token"], + "responseTypes": ["code"], + "redirectUris": [], + "pkceRequired": true, + "tokenEndpointAuthMethod": "none", + "publicClient": true + } + } + ], + "allowedUserTypes": [] + }, + "fieldConstraints": { + "oauth2": { + "publicClient": {"readOnly": true, "value": true}, + "pkceRequired": {"readOnly": true, "value": true}, + "tokenEndpointAuthMethod": {"readOnly": true, "value": "none"} + } + }, + "capabilities": { + "attestation": true + }, + "integrationGuides": { + "REDIRECT_BASED": { + "llm_prompt": { + "docsUrl": "{{applications.templates.flutter.llmPrompt.redirectBased}}" + } + } + } +} diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/ios.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/ios.json new file mode 100644 index 0000000000..e9b31618f5 --- /dev/null +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/ios.json @@ -0,0 +1,52 @@ +{ + "id": "ios", + "type": "mobile", + "displayName": "iOS", + "description": "Native iOS application built with SwiftUI", + "previewDevice": "mobile", + "quickstarts": [ + { + "label": "iOS", + "docsUrl": "{{applications.templates.ios.docs}}" + } + ], + "creationFlow": { + "steps": ["ORGANIZATION_UNIT", "DETAILS", "SECURITY", "DESIGN", "CONFIGURE", "COMPLETE"], + "previewSteps": ["DETAILS", "SECURITY", "DESIGN"] + }, + "defaults": { + "name": "iOS Application", + "signInApproach": "EMBEDDED", + "inboundAuthConfig": [ + { + "type": "oauth2", + "config": { + "grantTypes": ["authorization_code", "refresh_token"], + "responseTypes": ["code"], + "redirectUris": [], + "pkceRequired": true, + "tokenEndpointAuthMethod": "none", + "publicClient": true + } + } + ], + "allowedUserTypes": [] + }, + "fieldConstraints": { + "oauth2": { + "publicClient": {"readOnly": true, "value": true}, + "pkceRequired": {"readOnly": true, "value": true}, + "tokenEndpointAuthMethod": {"readOnly": true, "value": "none"} + } + }, + "capabilities": { + "attestation": true + }, + "integrationGuides": { + "REDIRECT_BASED": { + "llm_prompt": { + "docsUrl": "{{applications.templates.ios.llmPrompt.redirectBased}}" + } + } + } +} diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/mcp-client.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/mcp-client.json index e03762fa4e..d532801796 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/mcp-client.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/mcp-client.json @@ -3,6 +3,12 @@ "type": "mcp", "displayName": "MCP Client", "description": "AI application that connects to MCP servers using the Model Context Protocol", + "quickstarts": [ + { + "label": "Python", + "docsUrl": "{{applications.templates.mcpClient.docs}}" + } + ], "creationFlow": { "steps": ["ORGANIZATION_UNIT", "DETAILS", "CLIENT_TYPE", "COMPLETE"], "previewSteps": [] diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/nextjs.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/nextjs.json index 9593788691..f2c077c1d9 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/nextjs.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/nextjs.json @@ -3,6 +3,19 @@ "type": "fullstack", "displayName": "Next.js", "description": "Server-side rendered application built with Next.js", + "quickstarts": [ + { + "label": "Next.js", + "docsUrl": "{{applications.templates.nextjs.docs}}" + } + ], + "playgrounds": [ + { + "label": "Next.js", + "environment": "stackblitz", + "url": "{{applications.templates.nextjs.playground}}" + } + ], "devServer": { "id": "nextjs", "label": "Create Next App", @@ -46,85 +59,15 @@ } }, "integrationGuides": { - "INBUILT": { + "REDIRECT_BASED": { "llm_prompt": { - "id": "llm-prompt", - "title": "Integrate with a Coding Agent Prompt", - "description": "Use AI to generate integration code for your Next.js application", - "type": "llm", - "icon": "sparkles", - "content": "# Integrate {{productName}} Authentication in Next.js Application\n\n## Context\nI have a Next.js application (App Router) and I want to integrate {{productName}}'s authentication system using the {{productName}} Next.js SDK with {{productName}}-hosted login pages.\n\n## Requirements\n- Use @thunderid/nextjs SDK for authentication\n- Configure {{productName}}-hosted login pages (not custom/embedded)\n- Use the App Router (not Pages Router)\n- Implement sign-in and sign-out with prebuilt components\n- Add middleware for route protection and automatic token refresh\n- Display signed-in user's profile information\n\n## Configuration\n- **Client ID**: {{clientId}}\n- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL)\n- **SDK**: @thunderid/nextjs\n\n## IMPORTANT Configuration Rules\n- Use environment variables for configuration (NOT props on the provider)\n- Required env vars: NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SECRET, NODE_TLS_REJECT_UNAUTHORIZED=0\n- Import ThunderIDProvider from '@thunderid/nextjs/server' (NOT from '@thunderid/nextjs')\n- Import middleware utilities from '@thunderid/nextjs/server'\n- Import UI components from '@thunderid/nextjs'\n- The ThunderIDProvider handles the OAuth callback automatically — no manual callback route is needed\n\n## Implementation Steps\n1. Create a Next.js app: npx create-next-app@latest nextjs-demo\n2. Navigate into the project: cd nextjs-demo\n3. Install @thunderid/nextjs: npm install @thunderid/nextjs\n4. Create .env.local with NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SECRET, NODE_TLS_REJECT_UNAUTHORIZED=0\n5. Wrap root layout with from '@thunderid/nextjs/server'\n6. Create proxy.ts with thunderIDProxy and createRouteMatcher from '@thunderid/nextjs/server' for route protection\n7. Add SignedIn, UserDropdown, SignedOut, SignInButton components to pages\n8. Run: npm run dev\n\nPlease provide complete, working code with proper configuration for {{productName}} authentication using the {{productName}} Next.js SDK." - }, - "manual_steps": [ - { - "step": 1, - "title": "Create a Next.js app", - "description": "Run the following command to create a new Next.js app:", - "code": { - "language": "terminal", - "content": "npx create-next-app@latest my-nextjs-app\ncd my-nextjs-app" - } - }, - { - "step": 2, - "title": "Install @thunderid/nextjs", - "description": "The {{productName}} Next.js SDK provides server-side authentication, middleware, and prebuilt UI components.", - "subDescription": "Run the following command to install the SDK:", - "code": { - "language": "terminal", - "content": "npm install @thunderid/nextjs" - } - }, - { - "step": 3, - "title": "Set environment variables", - "description": "Create a .env.local file in your project root with the following configuration:", - "code": { - "language": "bash", - "filename": ".env.local", - "content": "NEXT_PUBLIC_THUNDERID_BASE_URL=https://localhost:8090\nNEXT_PUBLIC_THUNDERID_CLIENT_ID={{clientId}}\nTHUNDERID_CLIENT_SECRET=\nTHUNDERID_SECRET=\n# DANGER: Disables ALL TLS verification. Only for local development with self-signed certs. NEVER use in production.\nNODE_TLS_REJECT_UNAUTHORIZED=0" - } - }, - { - "step": 4, - "title": "Add to your layout", - "description": "Wrap your root layout with the ThunderIDProvider from the server export:", - "code": { - "language": "typescript", - "filename": "app/layout.tsx", - "content": "import type { Metadata } from \"next\";\nimport { Geist, Geist_Mono } from \"next/font/google\";\nimport { ThunderIDProvider } from '@thunderid/nextjs/server'\nimport \"./globals.css\";\n\nconst geistSans = Geist({\n variable: \"--font-geist-sans\",\n subsets: [\"latin\"],\n});\n\nconst geistMono = Geist_Mono({\n variable: \"--font-geist-mono\",\n subsets: [\"latin\"],\n});\n\nexport const metadata: Metadata = {\n title: \"Create Next App\",\n description: \"Generated by create next app\",\n};\n\nexport default function RootLayout({\n children,\n}: Readonly<{\n children: React.ReactNode;\n}>) {\n return (\n \n \n {children}\n \n \n );\n}" - } - }, - { - "step": 5, - "title": "Add the ThunderID proxy", - "description": "Create a proxy.ts file at your project root to proxy requests through ThunderID and protect routes:", - "code": { - "language": "typescript", - "filename": "proxy.ts", - "content": "import {\n thunderIDProxy,\n createRouteMatcher,\n} from '@thunderid/nextjs/server'\n\nconst isProtectedRoute = createRouteMatcher([\n // Add the paths you want to protect, e.g. '/dashboard(.*)'\n])\n\nexport default thunderIDProxy(async (thunderid, request) => {\n if (isProtectedRoute(request)) {\n await thunderid.protectRoute()\n }\n})\n\nexport const config = {\n matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],\n}" - } - }, - { - "step": 6, - "title": "Build with ThunderID components", - "description": "Update your home page with ThunderID authentication components:", - "code": { - "language": "typescript", - "filename": "app/page.tsx", - "content": "import { SignedIn, UserDropdown, SignedOut, SignInButton } from \"@thunderid/nextjs\";\n\nexport default function Home() {\n return (\n
\n \n \n \n \n Sign In\n \n
\n );\n}" - } - }, - { - "step": 7, - "title": "Run the app", - "description": "Start your development server and test the authentication flow:", - "code": { - "language": "terminal", - "content": "npm run dev" - } - } - ] + "docsUrl": "{{applications.templates.nextjs.llmPrompt.redirectBased}}" + } + }, + "EMBEDDED": { + "llm_prompt": { + "docsUrl": "{{applications.templates.nextjs.llmPrompt.embedded}}" + } } } } diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/node.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/node.json index 32934e8de8..7c0e13ae79 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/node.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/node.json @@ -3,6 +3,19 @@ "type": "fullstack", "displayName": "Node.js", "description": "Backend service built with Node.js", + "quickstarts": [ + { + "label": "Node.js", + "docsUrl": "{{applications.templates.node.docs}}" + } + ], + "playgrounds": [ + { + "label": "Node.js", + "environment": "stackblitz", + "url": "{{applications.templates.node.playground}}" + } + ], "creationFlow": { "steps": ["ORGANIZATION_UNIT", "DETAILS", "COMPLETE"], "previewSteps": [] @@ -37,96 +50,15 @@ } }, "integrationGuides": { - "INBUILT": { + "REDIRECT_BASED": { "llm_prompt": { - "id": "llm-prompt", - "title": "Integrate with a Coding Agent Prompt", - "description": "Use AI to generate integration code for your Node.js application", - "type": "llm", - "icon": "sparkles", - "content": "# Integrate {{productName}} Authentication in a Node.js Application\n\n## Context\nI have a Node.js application and I want to integrate {{productName}} authentication using the @thunderid/node SDK and the built-in http module — no framework required.\n\n## Requirements\n- Use @thunderid/node SDK with the built-in Node.js http module\n- Initialize ThunderIDNodeClient with clientId, clientSecret, baseUrl, afterSignInUrl, afterSignOutUrl\n- Implement /login route to start the sign-in flow (redirects to {{productName}})\n- Implement /callback route to handle the OAuth authorization code exchange\n- Implement /logout route to sign out and clear the session cookie\n- Protect the /profile route using isSignedIn() and display user info with getUser()\n- Manage sessions using a session ID stored in an HttpOnly cookie\n- Keep code minimal and fully runnable with CommonJS require()\n\n## Configuration\n- **Client ID**: {{clientId}}\n- **Client Secret**: ``\n- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL)\n- **App URL**: http://localhost:3000\n- **Callback Route**: /callback\n- **SDK**: @thunderid/node\n\n## Important Rules\n- Use CommonJS syntax (require) in index.js\n- Initialize ThunderIDNodeClient with auth.initialize({...}) before starting the server\n- signIn() works in two phases: first call redirects the user (authUrlCallback), second call (with code+state) exchanges the token\n- Store the session ID in a cookie named 'tid_session' with HttpOnly and SameSite=Lax flags\n- Use randomUUID() from the built-in 'crypto' module to generate session IDs\n- Use isSignedIn(sessionId) to guard protected routes\n- Use getUser(sessionId) to retrieve the authenticated user profile\n- Use signOut(sessionId) to get the OIDC end-session URL, then clear the local cookie and redirect\n\n## Implementation Steps\n1. Create a new project: mkdir my-node-app && cd my-node-app && npm init -y\n2. Install @thunderid/node: npm install @thunderid/node\n3. Create index.js with ThunderIDNodeClient initialization\n4. Add /login route: generate session ID cookie and redirect to {{productName}} auth URL\n5. Add /callback route: exchange authorization code for tokens using signIn()\n6. Add /logout route: call signOut() to get end-session URL, clear cookie, redirect\n7. Add / route: show sign-in or profile link based on isSignedIn()\n8. Add /profile route: guard with isSignedIn(), display user info from getUser()\n9. Start the server: node index.js\n\nPlease provide a complete, working index.js file with all routes and the ThunderIDNodeClient wired up correctly." - }, - "manual_steps": [ - { - "step": 1, - "title": "Create a Node.js project", - "description": "Initialize a new Node.js project:", - "code": { - "language": "terminal", - "content": "mkdir my-node-app\ncd my-node-app\nnpm init -y" - } - }, - { - "step": 2, - "title": "Install @thunderid/node", - "description": "The {{productName}} Node.js SDK provides a framework-agnostic authentication client for server-side Node.js applications.", - "subDescription": "Run the following command to install the SDK:", - "code": { - "language": "terminal", - "content": "npm install @thunderid/node" - } - }, - { - "step": 3, - "title": "Initialize the client", - "description": "Create an index.js file and initialize the ThunderIDNodeClient with your application credentials:", - "code": { - "language": "javascript", - "filename": "index.js", - "content": "const http = require('http');\nconst { URL } = require('url');\nconst { randomUUID } = require('crypto');\nconst { ThunderIDNodeClient } = require('@thunderid/node');\n\nconst PORT = 3000;\nconst SESSION_COOKIE = 'tid_session';\n\nconst auth = new ThunderIDNodeClient();\n\nfunction getSessionId(req) {\n const cookieHeader = req.headers.cookie ?? '';\n for (const part of cookieHeader.split(';')) {\n const [name, value] = part.trim().split('=');\n if (name === SESSION_COOKIE) return decodeURIComponent(value);\n }\n return null;\n}\n\nasync function main() {\n await auth.initialize({\n clientId: '{{clientId}}',\n clientSecret: '',\n baseUrl: 'https://localhost:8090',\n afterSignInUrl: 'http://localhost:3000/callback',\n afterSignOutUrl: 'http://localhost:3000',\n });\n\n const server = http.createServer(async (req, res) => {\n // routes added in the next step\n });\n\n server.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n });\n}\n\nmain();" - } - }, - { - "step": 4, - "title": "Add sign-in, callback, and sign-out routes", - "description": "Replace the routes comment in index.js with sign-in, callback, and sign-out handling:", - "code": { - "language": "javascript", - "filename": "index.js", - "content": " const url = new URL(req.url, `http://localhost:${PORT}`);\n\n try {\n if (url.pathname === '/login') {\n let sessionId = getSessionId(req);\n const extraHeaders = {};\n if (!sessionId) {\n sessionId = randomUUID();\n extraHeaders['Set-Cookie'] =\n `${SESSION_COOKIE}=${sessionId}; HttpOnly; SameSite=Lax; Path=/`;\n }\n await auth.signIn((authUrl) => {\n res.writeHead(302, { ...extraHeaders, Location: authUrl });\n res.end();\n }, sessionId);\n\n } else if (url.pathname === '/callback') {\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n const sessionState = url.searchParams.get('session_state');\n const sessionId = getSessionId(req);\n\n if (!sessionId || !code || !state) {\n res.writeHead(400);\n return res.end('Bad request');\n }\n\n await auth.signIn(() => {}, sessionId, code, sessionState, state);\n res.writeHead(302, { Location: '/profile' });\n res.end();\n\n } else if (url.pathname === '/logout') {\n const sessionId = getSessionId(req);\n if (!sessionId) {\n res.writeHead(302, { Location: '/' });\n return res.end();\n }\n const signOutUrl = await auth.signOut(sessionId);\n res.writeHead(302, {\n Location: signOutUrl,\n 'Set-Cookie': `${SESSION_COOKIE}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`,\n });\n res.end();\n }\n } catch {\n res.writeHead(500);\n res.end('Internal server error');\n }" - } - }, - { - "step": 5, - "title": "Add protected profile route", - "description": "Add the home and profile routes inside the same try block, before the closing }:", - "code": { - "language": "javascript", - "filename": "index.js", - "content": " if (url.pathname === '/') {\n const sessionId = getSessionId(req);\n const signedIn = sessionId && (await auth.isSignedIn(sessionId));\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end(signedIn\n ? 'View profile | Sign out'\n : 'Sign in'\n );\n\n } else if (url.pathname === '/profile') {\n const sessionId = getSessionId(req);\n if (!sessionId || !(await auth.isSignedIn(sessionId))) {\n res.writeHead(302, { Location: '/login' });\n return res.end();\n }\n const user = await auth.getUser(sessionId);\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end(`\n

Welcome, ${user.name || user.username}!

\n

Email: ${user.email}

\n

First name: ${user.given_name}

\n

Last name: ${user.family_name}

\n Sign out\n `);\n }" - } - }, - { - "step": 6, - "title": "Update credentials", - "description": "Replace the placeholders with your actual application credentials from {{productName}}:", - "bullets": [ - "Replace `{{clientId}}` with your Client ID", - "Replace `` with your Client Secret", - "Ensure your authorized redirect URL is `http://localhost:3000/callback`" - ] - }, - { - "step": 7, - "title": "Run the app", - "description": "Start your Node.js server:", - "code": { - "language": "terminal", - "content": "node index.js" - } - }, - { - "step": 8, - "title": "Verify authentication flow", - "description": "Test the integration end-to-end:", - "bullets": [ - "Open `http://localhost:3000` and click Sign in to be redirected to {{productName}}", - "Authenticate with your test user and confirm redirect back to `/profile`", - "Verify user profile details are displayed on the profile page", - "Click Sign out and verify the session is cleared" - ] - } - ] + "docsUrl": "{{applications.templates.node.llmPrompt.redirectBased}}" + } + }, + "EMBEDDED": { + "llm_prompt": { + "docsUrl": "{{applications.templates.node.llmPrompt.embedded}}" + } } } } diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/nuxt.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/nuxt.json index 3d27c28bfc..ef0751c12e 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/nuxt.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/nuxt.json @@ -3,6 +3,19 @@ "type": "fullstack", "displayName": "Nuxt", "description": "Full-stack Vue framework with server-side rendering", + "quickstarts": [ + { + "label": "Nuxt", + "docsUrl": "{{applications.templates.nuxt.docs}}" + } + ], + "playgrounds": [ + { + "label": "Nuxt", + "environment": "stackblitz", + "url": "{{applications.templates.nuxt.playground}}" + } + ], "devServer": { "id": "nuxt", "label": "Nuxt", @@ -45,85 +58,15 @@ } }, "integrationGuides": { - "INBUILT": { + "REDIRECT_BASED": { "llm_prompt": { - "id": "llm-prompt", - "title": "Integrate with a Coding Agent Prompt", - "description": "Use AI to generate integration code for your Nuxt 3 application", - "type": "llm", - "icon": "sparkles", - "content": "# Integrate {{productName}} Authentication in Nuxt 3 Application\n\n## Context\nI have a Nuxt 3 application and I want to integrate {{productName}}'s authentication system using the @thunderid/nuxt module with {{productName}}-hosted login pages.\n\n## Requirements\n- Use @thunderid/nuxt module for authentication\n- Register the module in nuxt.config.ts\n- Configure via environment variables (no inline config)\n- Wrap app.vue content with \n- Implement sign-in and sign-out with auto-imported components\n- Display signed-in user's profile information\n- Optionally protect pages with the built-in thunderIDMiddleware\n\n## Configuration\n- **Client ID**: {{clientId}}\n- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL)\n- **Callback URL**: http://localhost:3000/api/auth/callback (auto-registered by the module)\n- **SDK**: @thunderid/nuxt\n\n## IMPORTANT Configuration Rules\n- Add '@thunderid/nuxt' to the modules array in nuxt.config.ts — no other config needed there\n- All configuration is read from environment variables with NUXT_PUBLIC_ prefix for public values\n- Required env vars: NUXT_PUBLIC_THUNDERID_BASE_URL, NUXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SESSION_SECRET\n- THUNDERID_CLIENT_SECRET and THUNDERID_SESSION_SECRET must NOT have the NUXT_PUBLIC_ prefix\n- The /api/auth/callback route is auto-registered by the module — do NOT create it manually\n- Wrap with in app.vue\n- All components (SignedIn, SignedOut, SignInButton, SignOutButton, User) and composables are auto-imported\n- Protect pages by adding definePageMeta({ middleware: ['thunderIDMiddleware'] })\n\n## Implementation Steps\n1. Create a Nuxt 3 app: npx nuxi@latest init my-nuxt-app\n2. Navigate into the project: cd my-nuxt-app && npm install\n3. Install @thunderid/nuxt: npm install @thunderid/nuxt\n4. Add '@thunderid/nuxt' to modules in nuxt.config.ts\n5. Create .env with NUXT_PUBLIC_THUNDERID_BASE_URL, NUXT_PUBLIC_THUNDERID_CLIENT_ID, THUNDERID_CLIENT_SECRET, THUNDERID_SESSION_SECRET\n6. Wrap with in app.vue\n7. Create pages/index.vue with SignInButton, SignOutButton, SignedIn, SignedOut, and User components\n8. Optionally add definePageMeta({ middleware: ['thunderIDMiddleware'] }) to protected pages\n9. Run: npm run dev\n\nPlease provide complete, working code with proper configuration for {{productName}} authentication using the @thunderid/nuxt module." - }, - "manual_steps": [ - { - "step": 1, - "title": "Create a Nuxt 3 app", - "description": "Run the following command to create a new Nuxt 3 app:", - "code": { - "language": "terminal", - "content": "npm create nuxt@latest my-nuxt-app\ncd my-nuxt-app" - } - }, - { - "step": 2, - "title": "Install @thunderid/nuxt", - "description": "The {{productName}} Nuxt module provides server-side auth, auto-imported components, and route middleware.", - "subDescription": "Run the following command to install the module:", - "code": { - "language": "terminal", - "content": "npm install @thunderid/nuxt" - } - }, - { - "step": 3, - "title": "Register the module", - "description": "Add @thunderid/nuxt to the modules array in nuxt.config.ts:", - "code": { - "language": "typescript", - "filename": "nuxt.config.ts", - "content": "export default defineNuxtConfig({\n compatibilityDate: '2025-07-15',\n devtools: { enabled: true },\n modules: ['@thunderid/nuxt'],\n})" - } - }, - { - "step": 4, - "title": "Set environment variables", - "description": "Create a .env file in your project root with the following configuration:", - "code": { - "language": "bash", - "filename": ".env", - "content": "NUXT_PUBLIC_THUNDERID_BASE_URL=https://localhost:8090\nNUXT_PUBLIC_THUNDERID_CLIENT_ID={{clientId}}\nTHUNDERID_CLIENT_SECRET=\nTHUNDERID_SESSION_SECRET=\n# DANGER: Disables ALL TLS verification. Only for local development with self-signed certs. NEVER use in production.\nNODE_TLS_REJECT_UNAUTHORIZED=0" - } - }, - { - "step": 5, - "title": "Wrap your app with ", - "description": "Update app.vue to wrap your application content with the ThunderIDRoot component:", - "code": { - "language": "vue", - "filename": "app.vue", - "content": "" - } - }, - { - "step": 6, - "title": "Build with ThunderID components", - "description": "Update your home page with ThunderID authentication components (all auto-imported by the module):", - "code": { - "language": "vue", - "filename": "app/pages/index.vue", - "content": "" - } - }, - { - "step": 7, - "title": "Run the app", - "description": "Start your development server and test the authentication flow:", - "code": { - "language": "terminal", - "content": "npm run dev" - } - } - ] + "docsUrl": "{{applications.templates.nuxt.llmPrompt.redirectBased}}" + } + }, + "EMBEDDED": { + "llm_prompt": { + "docsUrl": "{{applications.templates.nuxt.llmPrompt.embedded}}" + } } } } diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/other.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/other.json index 473b4b34db..43f1b613c7 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/other.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/other.json @@ -3,6 +3,19 @@ "type": "browser", "displayName": "Other", "description": "Custom application with standard OAuth2 configuration", + "quickstarts": [ + { + "label": "JavaScript", + "docsUrl": "{{applications.templates.browser.docs}}" + } + ], + "playgrounds": [ + { + "label": "JavaScript", + "environment": "stackblitz", + "url": "{{applications.templates.browser.playground}}" + } + ], "creationFlow": { "steps": ["ORGANIZATION_UNIT", "DETAILS", "SECURITY", "DESIGN", "CONFIGURE", "COMPLETE"], "previewSteps": ["DETAILS", "SECURITY", "DESIGN"] diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/react.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/react.json index 19ccf37685..eb8321a552 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/react.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/react.json @@ -3,6 +3,19 @@ "type": "browser", "displayName": "React", "description": "Single Page Application built with React", + "quickstarts": [ + { + "label": "React", + "docsUrl": "{{applications.templates.react.docs}}" + } + ], + "playgrounds": [ + { + "label": "React", + "environment": "stackblitz", + "url": "{{applications.templates.react.playground}}" + } + ], "capabilities": { "cors": true }, @@ -49,133 +62,15 @@ } }, "integrationGuides": { - "INBUILT": { + "REDIRECT_BASED": { "llm_prompt": { - "id": "llm-prompt", - "title": "Integrate with a Coding Agent Prompt", - "description": "Use AI to generate integration code for your React application", - "type": "llm", - "icon": "sparkles", - "content": "# Integrate {{productName}} Authentication in React Application (Inbuilt Mode)\n\n## Context\nI have a React application and I want to integrate {{productName}}'s authentication system using the ThunderID React SDK with {{productName}}-hosted login pages.\n\n## Requirements\n- Use @thunderid/react SDK for authentication\n- Configure {{productName}}-hosted login, registration, and account management UIs\n- Implement sign-in and sign-out functionality using prebuilt components\n- Display signed-in user's profile information\n- Handle authentication state automatically\n\n## Configuration\n- **Client ID**: {{clientId}}\n- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL)\n- **SDK**: @thunderid/react\n\n## IMPORTANT Configuration Rules\n- DO NOT create a separate config object - pass all configuration as individual props directly to \n- Required props: `clientId` and `baseUrl`\n- Optional props: `afterSignInUrl`, `afterSignOutUrl`, `scopes` (string array)\n- NEVER use property names like `signInRedirectURL` or `signOutRedirectURL`\n- Example: \n\n## Implementation Steps\n1. Create a React app using Vite by running: `npm create vite@latest my-react-app -- --template react`\n2. Navigate into the project directory: `cd my-react-app`\n3. Install dependencies: `npm install`\n4. Install @thunderid/react package: `npm install @thunderid/react`\n5. Wrap your app with and configure clientId and baseUrl\n6. Build with ThunderID components: use , , , and to control what signed-in and signed-out users see\n7. Run the development server: `npm run dev`\n\nPlease provide complete, working code with proper configuration for {{productName}} authentication using the ThunderID React SDK." - }, - "manual_steps": [ - { - "step": 1, - "title": "Create a React app using Vite", - "description": "Run the following command to create a new React app with Vite:", - "code": { - "language": "terminal", - "content": "npm create vite@latest my-react-app -- --template react\ncd my-react-app\nnpm install" - } - }, - { - "step": 2, - "title": "Install @thunderid/react", - "description": "The ThunderID React SDK provides prebuilt components, hooks, and helpers for {{productName}} authentication.", - "subDescription": "Run the following command to install the SDK:", - "code": { - "language": "terminal", - "content": "npm install @thunderid/react" - } - }, - { - "step": 3, - "title": "Add to your app", - "description": "In your main.jsx or index.jsx, wrap your application with the ThunderIDProvider component:", - "code": { - "language": "javascript", - "filename": "src/main.jsx", - "content": "import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { ThunderIDProvider } from '@thunderid/react'\nimport App from './App.jsx'\nimport './index.css'\n\ncreateRoot(document.getElementById('root')).render(\n \n \n \n \n \n)" - } - }, - { - "step": 4, - "title": "Build with ThunderID components", - "description": "You can control which content signed-in and signed-out users can see with the prebuilt control components. Replace the existing content of App.jsx with the following:", - "code": { - "language": "javascript", - "filename": "src/App.jsx", - "content": "import {\n SignedIn,\n SignedOut,\n SignInButton,\n UserDropdown\n} from '@thunderid/react'\nimport './App.css'\n\nfunction App() {\n return (\n <>\n
\n \n \n \n \n Sign In\n \n
\n \n )\n}\n\nexport default App" - } - }, - { - "step": 5, - "title": "Run the app", - "description": "Start your development server and test the authentication flow:", - "code": { - "language": "terminal", - "content": "npm run dev" - } - } - ] + "docsUrl": "{{applications.templates.react.llmPrompt.redirectBased}}" + } }, "EMBEDDED": { "llm_prompt": { - "id": "llm-prompt", - "title": "Integrate with a Coding Agent Prompt", - "description": "Use AI to generate integration code for your React application", - "type": "llm", - "icon": "sparkles", - "content": "# Integrate {{productName}} Authentication in React Application (Custom Mode)\n\n## Context\nI have a React application and I want to integrate {{productName}}'s authentication system using the ThunderID React SDK with a custom login UI instead of {{productName}}-hosted pages.\n\n## Requirements\n- Use @thunderid/react SDK for authentication\n- Build custom login UI with the component\n- Use react-router for routing to the custom sign-in page\n- Configure custom signInUrl in ThunderIDProvider\n- Implement sign-in and sign-out functionality\n- Display signed-in user's profile information\n- Handle authentication state automatically\n\n## Configuration\n- **Application ID**: {{applicationId}}\n- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL)\n- **Sign In URL**: http://localhost:5173/signin (custom route)\n- **SDK**: @thunderid/react\n- **Router**: react-router\n\n## IMPORTANT Configuration Rules\n- DO NOT create a separate config object - pass all configuration as individual props directly to \n- Required props: `baseUrl`, `signInUrl`, and `applicationId`\n- Optional props: `afterSignInUrl`, `afterSignOutUrl`, `scopes`\n- NEVER use property names like `signInRedirectURL` or `signOutRedirectURL`\n- Example: \n\n## Available SDK APIs\n\n### Hook: useThunderID (ONLY hook available)\n- The SDK provides ONLY ONE hook: `useThunderID`\n- No other hooks like useAuth, useSession, useUser, etc. exist\n- Use this hook to access authentication state and methods\n\nExample:\n```javascript\nimport { useThunderID } from '@thunderid/react';\n\nconst MyComponent = () => {\n const { isSignedIn, user, signIn, signOut } = useThunderID();\n\n return (\n
\n {isSignedIn ? (\n <>\n

Welcome, {user.displayName}!

\n \n \n ) : (\n \n )}\n
\n );\n};\n```\n\n### User Data Components (use if needed)\n\n1. **User Component** - Render props for user data:\n```javascript\nimport { User } from '@thunderid/react'\n\nPlease sign in

}>\n {(user) => (\n
\n

Welcome, {user?.displayName}!

\n

Email: {user?.email}

\n
\n )}\n
\n```\n\n2. **UserProfile Component** - Pre-built user profile UI:\n```javascript\nimport { UserProfile } from '@thunderid/react'\n\n\n```\n\n## Implementation Steps\n1. Create a React app using Vite by running: `npm create vite@latest my-react-app -- --template react`\n2. Navigate into the project directory: `cd my-react-app`\n3. Install dependencies: `npm install`\n4. Install react-router package: `npm install react-router`\n5. Install @thunderid/react package: `npm install @thunderid/react`\n6. Wrap your app with and configure baseUrl, signInUrl, and applicationId\n7. Build with ThunderID components: set up React Router with BrowserRouter, create a /signin route with the component, and use , , , , and for authentication UI\n8. Run the development server: `npm run dev`\n\nPlease provide complete, working code with:\n- Proper routing configuration\n- Custom sign-in page integration\n- Proper integration with the ThunderID React SDK\n- Use of useThunderID hook if programmatic access to auth state is needed" - }, - "manual_steps": [ - { - "step": 1, - "title": "Create a React app using Vite", - "description": "Run the following command to create a new React app with Vite:", - "code": { - "language": "terminal", - "content": "npm create vite@latest my-react-app -- --template react\ncd my-react-app\nnpm install" - } - }, - { - "step": 2, - "title": "Install react-router", - "description": "Install react-router for routing to the custom sign-in page:", - "code": { - "language": "terminal", - "content": "npm install react-router" - } - }, - { - "step": 3, - "title": "Install @thunderid/react", - "description": "The ThunderID React SDK provides prebuilt components, hooks, and helpers for {{productName}} authentication:", - "code": { - "language": "terminal", - "content": "npm install @thunderid/react" - } - }, - { - "step": 4, - "title": "Add to your app", - "description": "In your main.jsx or index.jsx, wrap your application with the ThunderIDProvider component and configure the signInUrl:", - "code": { - "language": "javascript", - "filename": "src/main.jsx", - "content": "import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport './index.css'\nimport App from './App.jsx'\nimport { ThunderIDProvider } from '@thunderid/react'\n\ncreateRoot(document.getElementById('root')).render(\n \n \n \n \n \n)" - } - }, - { - "step": 5, - "title": "Build with ThunderID components", - "description": "You can control which content signed-in and signed-out users can see with the prebuilt control components. Replace the existing content of App.jsx with the following. This sets up routing, a dedicated sign-in page using the component, and uses for the signed-in user:", - "code": { - "language": "javascript", - "filename": "src/App.jsx", - "content": "import { BrowserRouter as Router, Routes, Route } from 'react-router'\nimport { SignIn, SignedIn, SignedOut, SignInButton, SignOutButton, UserDropdown } from '@thunderid/react'\nimport './App.css'\n\nfunction App() {\n return (\n \n
\n \n \n \n \n \n \n \n
\n \n } />\n \n
\n )\n}\n\nexport default App" - } - }, - { - "step": 6, - "title": "Run the app", - "description": "Start your development server and test the authentication flow with custom sign-in page:", - "code": { - "language": "terminal", - "content": "npm run dev" - } - } - ] + "docsUrl": "{{applications.templates.react.llmPrompt.embedded}}" + } } } } diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/vanilla-js.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/vanilla-js.json index d7a66ad507..e49a31bcad 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/vanilla-js.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/vanilla-js.json @@ -3,6 +3,19 @@ "type": "browser", "displayName": "JavaScript", "description": "Browser application built with vanilla JavaScript", + "quickstarts": [ + { + "label": "JavaScript", + "docsUrl": "{{applications.templates.browser.docs}}" + } + ], + "playgrounds": [ + { + "label": "JavaScript", + "environment": "stackblitz", + "url": "{{applications.templates.browser.playground}}" + } + ], "capabilities": { "cors": true }, @@ -49,75 +62,15 @@ } }, "integrationGuides": { - "INBUILT": { + "REDIRECT_BASED": { "llm_prompt": { - "id": "llm-prompt", - "title": "Integrate with a Coding Agent Prompt", - "description": "Use AI to generate integration code for your JavaScript application", - "type": "llm", - "icon": "sparkles", - "content": "# Integrate {{productName}} Authentication in Vanilla JavaScript Application\n\n## Context\nI have a vanilla JavaScript application and I want to integrate {{productName}}'s authentication system using the ThunderID Browser SDK with {{productName}}-hosted login pages.\n\n## Requirements\n- Use @thunderid/browser SDK for authentication\n- Configure {{productName}}-hosted login pages\n- Implement sign-in and sign-out functionality\n- Display signed-in user's profile information\n- Handle authentication state\n\n## Configuration\n- **Client ID**: {{clientId}}\n- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL)\n- **SDK**: @thunderid/browser\n\n## IMPORTANT Configuration Rules\n- Create a ThunderIDBrowserClient instance and call initialize() with a config object\n- Required config properties: `clientId` and `baseUrl`\n- Optional config properties: `afterSignInUrl`, `afterSignOutUrl`, `scopes` (string array), `storage`\n- Storage options: 'sessionStorage' (default), 'localStorage', 'browserMemory'\n- NEVER use property names like `signInRedirectURL` or `signOutRedirectURL`\n\n## Available SDK APIs\n\n### ThunderIDBrowserClient\n- `initialize(config)` - Initialize the client with configuration\n- `signIn()` - Redirect to {{productName}} sign-in page\n- `signOut()` - Sign out and clear session\n- `isSignedIn()` - Check if user is signed in (returns Promise)\n- `getUser()` - Get authenticated user profile (returns Promise)\n- `getAccessToken()` - Get current access token\n- `getIdToken()` - Get ID token\n- `getDecodedIdToken()` - Get decoded ID token claims\n- `httpRequest(config)` - Make authenticated HTTP request\n- `on(hook, callback)` - Register event callbacks (Hooks.SignIn, Hooks.SignOut, etc.)\n\n### User Object Properties\n- `displayName` - User's display name\n- `username` - Username\n- `email` - Email address\n- `given_name` - First name\n- `family_name` - Last name\n- `picture` - Profile picture URL\n\n## Implementation Steps\n1. Create a vanilla JS app using Vite by running: `npm create vite@latest js-demo -- --template vanilla`\n2. Navigate into the project directory: `cd js-demo`\n3. Install dependencies: `npm install`\n4. Install @thunderid/browser package: `npm install @thunderid/browser`\n5. Create src/auth.js to initialize ThunderIDBrowserClient with clientId and baseUrl\n6. Update src/main.js to check isSignedIn(), show sign-in button or user profile\n7. Add event listeners for sign-in and sign-out buttons\n8. Run the development server: `npm run dev`\n\nPlease provide complete, working code with proper configuration for {{productName}} authentication using the ThunderID Browser SDK." - }, - "manual_steps": [ - { - "step": 1, - "title": "Create a JavaScript app using Vite", - "description": "Run the following command to create a new vanilla JavaScript app with Vite:", - "code": { - "language": "terminal", - "content": "npm create vite@latest js-demo -- --template vanilla\ncd js-demo\nnpm install" - } - }, - { - "step": 2, - "title": "Install @thunderid/browser", - "description": "The ThunderID Browser SDK provides a framework-agnostic authentication client for browser applications.", - "subDescription": "Run the following command to install the SDK:", - "code": { - "language": "terminal", - "content": "npm install @thunderid/browser" - } - }, - { - "step": 3, - "title": "Initialize the SDK", - "description": "Create a new file src/auth.js to initialize and export the ThunderIDBrowserClient:", - "code": { - "language": "javascript", - "filename": "src/auth.js", - "content": "import { ThunderIDBrowserClient } from '@thunderid/browser'\n\nconst auth = new ThunderIDBrowserClient()\n\nawait auth.initialize({\n clientId: '{{clientId}}',\n baseUrl: 'https://localhost:8090',\n afterSignInUrl: window.location.origin,\n afterSignOutUrl: window.location.origin,\n})\n\nexport default auth" - } - }, - { - "step": 4, - "title": "Add sign-in and sign-out", - "description": "Replace the content of src/main.js with the following to add authentication:", - "code": { - "language": "javascript", - "filename": "src/main.js", - "content": "import './style.css'\nimport auth from './auth.js'\n\nasync function renderApp() {\n const isSignedIn = await auth.isSignedIn()\n\n if (isSignedIn) {\n const user = await auth.getUser()\n\n document.querySelector('#app').innerHTML = `\n
\n

ThunderID Auth Demo

\n
\n

Welcome, ${user.displayName || user.username}!

\n

Email: ${user.email || 'N/A'}

\n \n
\n
\n `\n\n document.querySelector('#sign-out-btn')\n .addEventListener('click', () => auth.signOut())\n } else {\n document.querySelector('#app').innerHTML = `\n
\n

ThunderID Auth Demo

\n
\n

You are not signed in.

\n \n
\n
\n `\n\n document.querySelector('#sign-in-btn')\n .addEventListener('click', () => auth.signIn())\n }\n}\n\nrenderApp()" - } - }, - { - "step": 5, - "title": "Display signed-in user's profile information", - "description": "Update src/main.js to display additional user profile details:", - "code": { - "language": "javascript", - "filename": "src/main.js", - "content": "import './style.css'\nimport auth from './auth.js'\n\nasync function renderApp() {\n const isSignedIn = await auth.isSignedIn()\n\n if (isSignedIn) {\n const user = await auth.getUser()\n\n document.querySelector('#app').innerHTML = `\n
\n

ThunderID Auth Demo

\n
\n ${user.picture ? `\"Avatar\"` : ''}\n

Welcome, ${user.displayName || user.username}!

\n
\n

Email: ${user.email || 'N/A'}

\n

First Name: ${user.given_name || 'N/A'}

\n

Last Name: ${user.family_name || 'N/A'}

\n
\n \n
\n
\n `\n\n document.querySelector('#sign-out-btn')\n .addEventListener('click', () => auth.signOut())\n } else {\n document.querySelector('#app').innerHTML = `\n
\n

ThunderID Auth Demo

\n
\n

You are not signed in.

\n \n
\n
\n `\n\n document.querySelector('#sign-in-btn')\n .addEventListener('click', () => auth.signIn())\n }\n}\n\nrenderApp()" - } - }, - { - "step": 6, - "title": "Run the app", - "description": "Start your development server and test the authentication flow:", - "code": { - "language": "terminal", - "content": "npm run dev" - } - } - ] + "docsUrl": "{{applications.templates.browser.llmPrompt.redirectBased}}" + } + }, + "EMBEDDED": { + "llm_prompt": { + "docsUrl": "{{applications.templates.browser.llmPrompt.embedded}}" + } } } } diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/vue.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/vue.json index 64fb0b51fa..688514e2fb 100644 --- a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/vue.json +++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/vue.json @@ -3,6 +3,19 @@ "type": "browser", "displayName": "Vue", "description": "Single Page Application built with Vue 3", + "quickstarts": [ + { + "label": "Vue", + "docsUrl": "{{applications.templates.vue.docs}}" + } + ], + "playgrounds": [ + { + "label": "Vue", + "environment": "stackblitz", + "url": "{{applications.templates.vue.playground}}" + } + ], "capabilities": { "cors": true }, @@ -49,65 +62,15 @@ } }, "integrationGuides": { - "INBUILT": { + "REDIRECT_BASED": { "llm_prompt": { - "id": "llm-prompt", - "title": "Integrate with a Coding Agent Prompt", - "description": "Use AI to generate integration code for your Vue 3 application", - "type": "llm", - "icon": "sparkles", - "content": "# Integrate {{productName}} Authentication in Vue 3 Application\n\n## Context\nI have a Vue 3 application and I want to integrate {{productName}}'s authentication system using the @thunderid/vue SDK with {{productName}}-hosted login pages.\n\n## Requirements\n- Use @thunderid/vue SDK for authentication\n- Register the ThunderIDPlugin in main.js\n- Wrap the app with ThunderIDProvider in App.vue\n- Implement sign-in and sign-out with prebuilt components\n- Display signed-in user's profile using the UserDropdown component\n\n## Configuration\n- **Client ID**: {{clientId}}\n- **Base URL**: https://localhost:8090 (or your {{productName}} instance URL)\n- **SDK**: @thunderid/vue\n\n## IMPORTANT Configuration Rules\n- Register ThunderIDPlugin via app.use(ThunderIDPlugin) in src/main.js\n- Import ThunderIDPlugin from '@thunderid/vue'\n- Wrap the app root in App.vue with \n- Pass configuration as kebab-case attributes: `client-id` and `base-url`\n- Import UI components (SignInButton, SignOutButton, SignedIn, SignedOut, UserDropdown) from '@thunderid/vue'\n- Use inside to display the signed-in user's profile and sign-out option\n\n## Implementation Steps\n1. Create a Vue 3 app: npm create vite@latest my-vue-app -- --template vue\n2. Navigate into the project: cd my-vue-app && npm install\n3. Install @thunderid/vue: npm install @thunderid/vue\n4. Register ThunderIDPlugin in src/main.js with app.use(ThunderIDPlugin)\n5. Wrap app content with in src/App.vue\n6. Add SignInButton inside SignedOut and UserDropdown inside SignedIn conditional wrappers\n7. Run: npm run dev\n\nPlease provide complete, working code with proper configuration for {{productName}} authentication using the @thunderid/vue SDK." - }, - "manual_steps": [ - { - "step": 1, - "title": "Create a Vue 3 app", - "description": "Run the following command to create a new Vue 3 app with Vite:", - "code": { - "language": "terminal", - "content": "npm create vite@latest my-vue-app -- --template vue\ncd my-vue-app\nnpm install" - } - }, - { - "step": 2, - "title": "Install @thunderid/vue", - "description": "The {{productName}} Vue SDK provides a plugin, prebuilt components, and helpers for authentication.", - "subDescription": "Run the following command to install the SDK:", - "code": { - "language": "terminal", - "content": "npm install @thunderid/vue" - } - }, - { - "step": 3, - "title": "Register the ThunderIDPlugin", - "description": "Update src/main.js to register the ThunderID Vue plugin:", - "code": { - "language": "javascript", - "filename": "src/main.js", - "content": "import { createApp } from 'vue'\nimport { ThunderIDPlugin } from '@thunderid/vue'\nimport './style.css'\nimport App from './App.vue'\n\nconst app = createApp(App)\napp.use(ThunderIDPlugin)\napp.mount('#app')" - } - }, - { - "step": 4, - "title": "Build with ThunderID components", - "description": "Replace the content of src/App.vue with ThunderID authentication components:", - "code": { - "language": "vue", - "filename": "src/App.vue", - "content": "\n\n" - } - }, - { - "step": 5, - "title": "Run the app", - "description": "Start your development server and test the authentication flow:", - "code": { - "language": "terminal", - "content": "npm run dev" - } - } - ] + "docsUrl": "{{applications.templates.vue.llmPrompt.redirectBased}}" + } + }, + "EMBEDDED": { + "llm_prompt": { + "docsUrl": "{{applications.templates.vue.llmPrompt.embedded}}" + } } } } diff --git a/frontend/apps/console/src/features/applications/hooks/__tests__/useApplicationCreateContext.test.tsx b/frontend/apps/console/src/features/applications/hooks/__tests__/useApplicationCreateContext.test.tsx index c913453510..79af702ef3 100644 --- a/frontend/apps/console/src/features/applications/hooks/__tests__/useApplicationCreateContext.test.tsx +++ b/frontend/apps/console/src/features/applications/hooks/__tests__/useApplicationCreateContext.test.tsx @@ -45,7 +45,7 @@ describe('useApplicationCreateContext', () => { setSmsOtpSenderId: vi.fn(), selectedAuthFlow: null, setSelectedAuthFlow: vi.fn(), - signInApproach: ApplicationCreateFlowSignInApproach.INBUILT, + signInApproach: ApplicationCreateFlowSignInApproach.REDIRECT_BASED, setSignInApproach: vi.fn(), registrationFlowId: null, setRegistrationFlowId: vi.fn(), @@ -103,7 +103,7 @@ describe('useApplicationCreateContext', () => { expect(result.current.currentStep).toBe(ApplicationCreateFlowStep.DETAILS); expect(result.current.appName).toBe('Test App'); expect(result.current.selectedTheme).toBeNull(); - expect(result.current.signInApproach).toBe(ApplicationCreateFlowSignInApproach.INBUILT); + expect(result.current.signInApproach).toBe(ApplicationCreateFlowSignInApproach.REDIRECT_BASED); }); it('should throw an error when used outside of ApplicationCreateProvider', () => { @@ -221,7 +221,10 @@ describe('useApplicationCreateContext', () => { }); it('should return sign-in approach values', () => { - const approaches = [ApplicationCreateFlowSignInApproach.INBUILT, ApplicationCreateFlowSignInApproach.EMBEDDED]; + const approaches = [ + ApplicationCreateFlowSignInApproach.REDIRECT_BASED, + ApplicationCreateFlowSignInApproach.EMBEDDED, + ]; approaches.forEach((approach) => { const contextWithApproach: ApplicationCreateContextType = { diff --git a/frontend/apps/console/src/features/applications/models/__tests__/application-create-flow.test.ts b/frontend/apps/console/src/features/applications/models/__tests__/application-create-flow.test.ts index 3979dff2fb..33714b94b6 100644 --- a/frontend/apps/console/src/features/applications/models/__tests__/application-create-flow.test.ts +++ b/frontend/apps/console/src/features/applications/models/__tests__/application-create-flow.test.ts @@ -44,8 +44,8 @@ describe('application-create-flow models', () => { }); describe('ApplicationCreateFlowSignInApproach', () => { - it('should have INBUILT approach', () => { - expect(ApplicationCreateFlowSignInApproach.INBUILT).toBe('INBUILT'); + it('should have REDIRECT_BASED approach', () => { + expect(ApplicationCreateFlowSignInApproach.REDIRECT_BASED).toBe('REDIRECT_BASED'); }); it('should have EMBEDDED approach', () => { diff --git a/frontend/apps/console/src/features/applications/models/__tests__/application-templates.test.ts b/frontend/apps/console/src/features/applications/models/__tests__/application-templates.test.ts index 69c61f225f..b247e6dbec 100644 --- a/frontend/apps/console/src/features/applications/models/__tests__/application-templates.test.ts +++ b/frontend/apps/console/src/features/applications/models/__tests__/application-templates.test.ts @@ -3,7 +3,7 @@ import {describe, expect, it} from 'vitest'; import {TechnologyApplicationTemplate, PlatformApplicationTemplate} from '../application-templates'; -import type {IntegrationGuide, IntegrationStepCode, TemplateCategory} from '../application-templates'; +import type {IntegrationGuide, TemplateCategory} from '../application-templates'; describe('Application Templates Models', () => { describe('TechnologyApplicationTemplate', () => { @@ -32,7 +32,20 @@ describe('Application Templates Models', () => { }); it('should have all expected properties', () => { - const expectedKeys = ['REACT', 'EXPRESS', 'NEXTJS', 'VANILLA_JS', 'VUE', 'NUXT', 'NODEJS', 'OTHER', 'MCP_CLIENT']; + const expectedKeys = [ + 'REACT', + 'EXPRESS', + 'NEXTJS', + 'VANILLA_JS', + 'VUE', + 'NUXT', + 'NODEJS', + 'IOS', + 'ANDROID', + 'FLUTTER', + 'OTHER', + 'MCP_CLIENT', + ]; expect(Object.keys(TechnologyApplicationTemplate)).toEqual(expectedKeys); }); @@ -79,72 +92,26 @@ describe('Application Templates Models', () => { }); describe('IntegrationGuide Interface', () => { - it('should accept valid integration guide with llm type', () => { + it('should accept a docsUrl referencing a documentation.links key', () => { const guide: IntegrationGuide = { - id: 'guide-1', - title: 'AI-Assisted Integration', - description: 'Use AI to integrate your app', - type: 'llm', - icon: 'ai-icon', - content: 'LLM prompt content', + docsUrl: '{{applications.templates.react.llmPrompt.redirectBased}}', }; - expect(guide.type).toBe('llm'); - expect(guide.content).toBe('LLM prompt content'); + expect(guide.docsUrl).toBe('{{applications.templates.react.llmPrompt.redirectBased}}'); }); - it('should accept valid integration guide with manual type', () => { + it('should accept a literal docsUrl', () => { const guide: IntegrationGuide = { - id: 'guide-2', - title: 'Manual Integration', - description: 'Step-by-step integration', - type: 'manual', - icon: 'manual-icon', + docsUrl: 'https://thunderid.dev/prompts/react/redirect-based.txt', }; - expect(guide.type).toBe('manual'); - expect(guide.content).toBeUndefined(); + expect(guide.docsUrl).toBe('https://thunderid.dev/prompts/react/redirect-based.txt'); }); - it('should have required properties', () => { - const guide: IntegrationGuide = { - id: 'test', - title: 'Test Guide', - description: 'Test description', - type: 'manual', - icon: 'test-icon', - }; - - expect(guide).toHaveProperty('id'); - expect(guide).toHaveProperty('title'); - expect(guide).toHaveProperty('description'); - expect(guide).toHaveProperty('type'); - expect(guide).toHaveProperty('icon'); - }); - }); - - describe('IntegrationStepCode Interface', () => { - it('should accept code block with all properties', () => { - const codeBlock: IntegrationStepCode = { - language: 'typescript', - filename: 'app.ts', - content: 'const app = "test";', - }; - - expect(codeBlock.language).toBe('typescript'); - expect(codeBlock.filename).toBe('app.ts'); - expect(codeBlock.content).toBe('const app = "test";'); - }); - - it('should accept code block without optional filename', () => { - const codeBlock: IntegrationStepCode = { - language: 'javascript', - content: 'console.log("hello");', - }; + it('should allow an integration guide with no docsUrl', () => { + const guide: IntegrationGuide = {}; - expect(codeBlock.language).toBe('javascript'); - expect(codeBlock.filename).toBeUndefined(); - expect(codeBlock.content).toBe('console.log("hello");'); + expect(guide.docsUrl).toBeUndefined(); }); }); }); diff --git a/frontend/apps/console/src/features/applications/models/application-create-flow.ts b/frontend/apps/console/src/features/applications/models/application-create-flow.ts index 6b7ca34617..2618e9ae6d 100644 --- a/frontend/apps/console/src/features/applications/models/application-create-flow.ts +++ b/frontend/apps/console/src/features/applications/models/application-create-flow.ts @@ -23,7 +23,7 @@ export const ApplicationCreateFlowStep = { * @public */ export const ApplicationCreateFlowSignInApproach = { - INBUILT: 'INBUILT', + REDIRECT_BASED: 'REDIRECT_BASED', EMBEDDED: 'EMBEDDED', } as const; diff --git a/frontend/apps/console/src/features/applications/models/application-templates.ts b/frontend/apps/console/src/features/applications/models/application-templates.ts index 0d02cd5563..728bdefe5f 100644 --- a/frontend/apps/console/src/features/applications/models/application-templates.ts +++ b/frontend/apps/console/src/features/applications/models/application-templates.ts @@ -20,6 +20,9 @@ export const TechnologyApplicationTemplate = { VUE: 'VUE', NUXT: 'NUXT', NODEJS: 'NODEJS', + IOS: 'IOS', + ANDROID: 'ANDROID', + FLUTTER: 'FLUTTER', OTHER: 'OTHER', MCP_CLIENT: 'MCP_CLIENT', } as const; @@ -47,90 +50,19 @@ export const PlatformApplicationTemplate = { */ export interface IntegrationGuide { /** - * Unique identifier for the guide + * Hosted prompt document for this guide. Either a literal URL, or a + * `{{documentation.links key}}` reference (e.g. + * '{{applications.templates.react.llmPrompt.redirectBased}}') resolved against the configured + * docs site, same convention as {@link QuickstartLink.docsUrl}. The referenced document is + * fetched at render time and may contain `{{productName}}`, `{{clientId}}`, and + * `{{applicationId}}` placeholders. */ - id: string; - /** - * Display title of the guide - */ - title: string; - /** - * Brief description of what the guide offers - */ - description: string; - /** - * Type of guide (llm for AI-assisted, manual for step-by-step) - */ - type: 'llm' | 'manual'; - /** - * Icon identifier for the guide - */ - icon: string; - /** - * Markdown content for LLM prompts - */ - content?: string; -} - -/** - * Integration step code block. - * - * @public - */ -export interface IntegrationStepCode { - /** - * Programming language for syntax highlighting - */ - language: string; - /** - * Optional filename to display - */ - filename?: string; - /** - * Code content - */ - content: string; - /** - * Optional tabs for different package managers - */ - tabs?: string[]; -} - -/** - * Integration step for manual integration guide. - * - * @public - */ -export interface IntegrationStep { - /** - * Step number - */ - step: number; - /** - * Step title - */ - title: string; - /** - * Main description - */ - description: string; - /** - * Optional sub-description - */ - subDescription?: string; - /** - * Optional bullet points - */ - bullets?: string[]; - /** - * Optional code block - */ - code?: IntegrationStepCode; + docsUrl?: string; } /** * Integration guides structure containing LLM prompt and manual steps. - * Keys represent different integration approaches (e.g., 'inbuilt', 'embedded'). + * Keys represent different integration approaches (e.g., 'redirect_based', 'embedded'). * * @public */ @@ -141,10 +73,6 @@ export type IntegrationGuides = Record< * LLM prompt guide option */ llm_prompt: IntegrationGuide; - /** - * Manual step-by-step integration guide - */ - manual_steps: IntegrationStep[]; } >; @@ -214,6 +142,14 @@ export interface ApplicationTemplate { */ cors?: boolean; }; + /** + * Which device frame the application-creation wizard's live sign-in preview should render at. + * Defaults to `'desktop'` when omitted. Set to `'mobile'` for templates whose sign-in UI is + * actually rendered on a phone screen (e.g. the native Android/iOS/Flutter templates and the + * generic Mobile platform template) — everything else, including other public-client templates + * like digital wallets, previews at desktop size. + */ + previewDevice?: 'desktop' | 'mobile'; /** * Optional metadata describing the template's local development server, used to offer a * "quick add" shortcut on the Configuration step for its conventional default URL. @@ -230,6 +166,59 @@ export interface ApplicationTemplate { * Optional integration guides for this template */ integrationGuides?: IntegrationGuides; + /** + * Optional quickstart guides for this template, shown on the application's Overview tab as + * "read the docs" cards. Most templates have exactly one; templates that cover more than one + * platform SDK (e.g. the generic Mobile template covering iOS, Android, and Flutter) list one + * entry per platform. + */ + quickstarts?: QuickstartLink[]; + /** + * Optional runnable playgrounds for this template, shown as a banner on the application's + * Overview tab when there's exactly one. Distinct from {@link quickstarts}: a quickstart is a + * hosted docs link, a playground is a live, runnable environment (e.g. StackBlitz). Not every + * template has a runnable playground (e.g. native mobile platforms don't). + */ + playgrounds?: PlaygroundLink[]; +} + +/** + * A single quickstart guide entry for a template. + * + * @public + */ +export interface QuickstartLink { + /** Short label identifying the guide, e.g. 'React', 'iOS', 'Android', 'Flutter'. */ + label: string; + /** + * Hosted documentation guide for connecting an application built with this template/platform. + * Either a literal URL, or a `{{documentation.links key}}` reference (e.g. + * '{{applications.templates.react.docs}}') resolved against the configured docs site, letting + * the docs site stay the source of truth. + */ + docsUrl: string; +} + +/** + * A single runnable playground entry for a template. + * + * @public + */ +export interface PlaygroundLink { + /** Short label identifying the guide, e.g. 'React', 'Next.js', 'Nuxt'. */ + label: string; + /** + * The runnable environment this playground is hosted on, e.g. `'stackblitz'`. Only + * `'stackblitz'` is currently rendered; other values are reserved for future environments the + * console doesn't support yet, and are silently not rendered. + */ + environment: string; + /** + * URL of the runnable sample. Either a literal URL, or a `{{documentation.links key}}` + * reference (e.g. '{{applications.templates.react.playground}}'), same resolution rules as + * {@link QuickstartLink.docsUrl}. + */ + url: string; } /** diff --git a/frontend/apps/console/src/features/applications/pages/ApplicationCreatePage.tsx b/frontend/apps/console/src/features/applications/pages/ApplicationCreatePage.tsx index c1718a72db..0d4e49940c 100644 --- a/frontend/apps/console/src/features/applications/pages/ApplicationCreatePage.tsx +++ b/frontend/apps/console/src/features/applications/pages/ApplicationCreatePage.tsx @@ -44,7 +44,7 @@ import { ApplicationCreateFlowStep, OrganizationUnitDefaultItem, } from '../models/application-create-flow'; -import {PlatformApplicationTemplate} from '../models/application-templates'; +import {PlatformApplicationTemplate, TechnologyApplicationTemplate} from '../models/application-templates'; import {McpClientTypes} from '../models/mcp-client'; import type {CreateApplicationRequest} from '../models/requests'; import getApplicationErrorMessage from '../utils/getApplicationErrorMessage'; @@ -54,6 +54,7 @@ import mergeCorsOrigins from '../utils/mergeCorsOrigins'; import resolveApplicationType from '../utils/resolveApplicationType'; import resolveCreationFlow from '../utils/resolveCreationFlow'; import GatePreview from '@/components/GatePreview/GatePreview'; +import {VIEWPORT_WIDTHS, VIEWPORT_HEIGHTS} from '@/features/design/components/viewportConstants'; export default function ApplicationCreatePage(): JSX.Element { const {t} = useTranslation(); @@ -372,13 +373,17 @@ export default function ApplicationCreatePage(): JSX.Element { ); // Browser-based SPAs are public clients that must use the redirect-based flow, so the - // embedded (native) sign-in approach is not offered for them. Native mobile apps and digital - // wallets are also public clients but legitimately use app-native flows, so they are excluded - // from this rule. + // embedded (native) sign-in approach is not offered for them. Native mobile apps (both the + // generic Mobile platform template and the individual Android/iOS/Flutter technology + // templates) and digital wallets are also public clients but legitimately use app-native + // flows, so they are excluded from this rule. const isBrowserSpaTemplate = useMemo((): boolean => { if ( selectedPlatform === PlatformApplicationTemplate.MOBILE || - selectedPlatform === PlatformApplicationTemplate.WALLET + selectedPlatform === PlatformApplicationTemplate.WALLET || + selectedTechnology === TechnologyApplicationTemplate.ANDROID || + selectedTechnology === TechnologyApplicationTemplate.IOS || + selectedTechnology === TechnologyApplicationTemplate.FLUTTER ) { return false; } @@ -386,7 +391,7 @@ export default function ApplicationCreatePage(): JSX.Element { (config) => config.type === 'oauth2', )?.config; return oauthConfig?.publicClient === true; - }, [selectedTemplateConfig, selectedPlatform]); + }, [selectedTemplateConfig, selectedPlatform, selectedTechnology]); // Wallets are OID4VCI issuance flows that redirect to a hosted page; there's no native UI to // embed, so unlike mobile apps they get no sign-in approach choice at all. @@ -396,7 +401,7 @@ export default function ApplicationCreatePage(): JSX.Element { // selected (e.g. after switching away from a BYOUI-capable template). useEffect((): void => { if (isBrowserSpaTemplate && signInApproach === ApplicationCreateFlowSignInApproach.EMBEDDED) { - setSignInApproach(ApplicationCreateFlowSignInApproach.INBUILT); + setSignInApproach(ApplicationCreateFlowSignInApproach.REDIRECT_BASED); } }, [isBrowserSpaTemplate, signInApproach, setSignInApproach]); @@ -907,6 +912,14 @@ export default function ApplicationCreatePage(): JSX.Element { // `previewSteps` list. const hasNoPreview = !creationFlow.previewSteps.includes(currentStep); + // The wizard preview has no viewport switcher of its own (see the `showToolbar={false}` below) + // — the device frame it renders at is fixed per template via `previewDevice`, defaulting to + // desktop when a template doesn't set it. + const isMobilePreview = selectedTemplateConfig?.previewDevice === 'mobile'; + const previewViewport = isMobilePreview + ? {width: VIEWPORT_WIDTHS.mobile, height: VIEWPORT_HEIGHTS.mobile} + : undefined; + const isFirstStep = visibleSteps.indexOf(currentStep) === 0; const isLastStep = visibleSteps.indexOf(currentStep) === visibleSteps.length - 1; @@ -973,6 +986,9 @@ export default function ApplicationCreatePage(): JSX.Element { baseTheme={DefaultTheme as Theme} mock={activePreviewMock} displayName={appName ?? undefined} + showToolbar={false} + viewport={previewViewport} + frameStyle={isMobilePreview ? 'phone' : 'browser'} footer={ currentStep === ApplicationCreateFlowStep.SECURITY && totalPreviewSteps >= 2 ? ( diff --git a/frontend/apps/console/src/features/applications/pages/ApplicationEditPage.tsx b/frontend/apps/console/src/features/applications/pages/ApplicationEditPage.tsx index 2705db59c6..92e588aa3c 100644 --- a/frontend/apps/console/src/features/applications/pages/ApplicationEditPage.tsx +++ b/frontend/apps/console/src/features/applications/pages/ApplicationEditPage.tsx @@ -43,7 +43,6 @@ import TemplateConstants from '../constants/template-constants'; import {McpClientTypes} from '../models/mcp-client'; import deriveMcpClientType from '../utils/deriveMcpClientType'; import getApplicationErrorMessage from '../utils/getApplicationErrorMessage'; -import {getIntegrationGuideForTemplate} from '../utils/getIntegrationGuidesForTemplate'; import getTemplateCapabilities from '../utils/getTemplateCapabilities'; import getTemplateFieldConstraints from '../utils/getTemplateFieldConstraints'; import getTemplateMetadata from '../utils/getTemplateMetadata'; @@ -103,7 +102,6 @@ export default function ApplicationEditPage() { // clean form — they keep local state (redirect URI list, react-hook-form defaults) that a // `setEditedApp({})` alone wouldn't reset. const [sectionResetKey, setSectionResetKey] = useState(0); - const [copiedField, setCopiedField] = useState(null); const [isEditingName, setIsEditingName] = useState(false); const [isEditingDescription, setIsEditingDescription] = useState(false); const [tempName, setTempName] = useState(''); @@ -122,24 +120,6 @@ export default function ApplicationEditPage() { setActiveTab(newValue); }; - const handleCopyToClipboard = useCallback( - async (text: string, fieldName: string) => { - try { - await navigator.clipboard.writeText(text); - setCopiedField(fieldName); - setTimeout(() => setCopiedField(null), 2000); - } catch { - logger.error('Failed to copy to clipboard'); - } - }, - [logger], - ); - - const hasIntegrationGuides = useMemo( - () => Boolean(application && getIntegrationGuideForTemplate(application.template)), - [application], - ); - const oauth2Constraints = useMemo( () => getTemplateFieldConstraints(application?.template)?.oauth2, [application?.template], @@ -272,7 +252,7 @@ export default function ApplicationEditPage() { const unsavedChangesMessage = validationIssues.length > 0 ? validationIssues.join(' ') : t('applications:edit.page.unsavedChanges'); - const mcpTabs: McpTabConfig[] = isMcpClient + const baseMcpTabs: McpTabConfig[] = isMcpClient ? ( [ { @@ -350,6 +330,29 @@ export default function ApplicationEditPage() { ).filter((tab) => !tab.hidden) : []; + const mcpFlowsTabIndex = baseMcpTabs.findIndex((tab) => tab.key === 'flows'); + const mcpCustomizationTabIndex = baseMcpTabs.findIndex((tab) => tab.key === 'customization'); + + const mcpTabs: McpTabConfig[] = isMcpClient + ? [ + { + key: 'overview', + label: t('applications:edit.page.tabs.overview'), + panel: ( + = 0 ? () => setActiveTab(mcpFlowsTabIndex + 1) : undefined} + onGoToCustomization={ + mcpCustomizationTabIndex >= 0 ? () => setActiveTab(mcpCustomizationTabIndex + 1) : undefined + } + /> + ), + }, + ...baseMcpTabs, + ] + : []; + const safeActiveTab = mcpTabs.length > 0 ? Math.min(activeTab, mcpTabs.length - 1) : 0; return ( @@ -543,42 +546,40 @@ export default function ApplicationEditPage() { <> {/* Tabs */} - {hasIntegrationGuides && ( - - )} + @@ -586,21 +587,22 @@ export default function ApplicationEditPage() { {/* Tab Panels */} <> {/* Overview Tab */} - {hasIntegrationGuides && ( - - - - )} + + setActiveTab(2)} + onGoToCustomization={() => setActiveTab(3)} + /> + {/* General Tab */} - + { handleBack().catch(() => null); }} @@ -611,7 +613,7 @@ export default function ApplicationEditPage() { {/* Flows Tab */} - + {/* Customization Tab */} - + {/* Token Tab */} - + {/* Advanced Settings Tab */} - + onApproachChange('EMBEDDED')}> Select Embedded - + -
- ), - ), + default: vi.fn(() =>
General Settings
), })); vi.mock('../../components/edit-application/flows-settings/EditFlowsSettings', () => ({ @@ -151,6 +135,9 @@ vi.mock('../../components/edit-application/mcp/McpConnectTab', () => ({ vi.mock('@thunderid/components', async () => { const React = await import('react'); return { + AppleIcon: vi.fn(() => null), + AndroidLogo: vi.fn(() => null), + FlutterLogo: vi.fn(() => null), CopyableId: vi.fn(() => null), EmojiPicker: vi.fn(() => null), PageLoadingAnimation: vi.fn(() =>
), @@ -465,13 +452,13 @@ describe('ApplicationEditPage', () => { expect(screen.getByRole('tab', {name: /overview/i})).toBeInTheDocument(); }); - it('should display general settings tab by default when no integration guides', () => { - // Mock returns null by default (no integration guides) + it('should display the overview tab by default even when there are no integration guides', () => { + // Mock returns null by default (no integration guides) — the Overview tab still shows, + // since it also surfaces application identifiers and endpoints regardless of guides. renderComponent(); - // When there are no integration guides, general tab should be first and selected - const generalTab = screen.getByRole('tab', {name: /general/i}); - expect(generalTab).toHaveAttribute('aria-selected', 'true'); + const overviewTab = screen.getByRole('tab', {name: /overview/i}); + expect(overviewTab).toHaveAttribute('aria-selected', 'true'); }); it('should display overview tab by default when integration guides are available', async () => { @@ -972,6 +959,9 @@ describe('ApplicationEditPage', () => { const user = userEvent.setup(); renderComponent(); + // EditGeneralSettings only mounts once its TabPanel is active + await mountSectionTab(user, 'General', 'edit-general-settings'); + const {initialKey, keyAfterReset} = await editFieldAndResetSection( user, EditGeneralSettings, @@ -1076,6 +1066,9 @@ describe('ApplicationEditPage', () => { renderComponent(); + // EditGeneralSettings only mounts once its TabPanel is active + await mountSectionTab(user, 'General', 'edit-general-settings'); + const initialKey = vi.mocked(EditGeneralSettings).mock.calls.at(-1)?.[0].sectionResetKey; await editInlineField(user, 'Test Application', 'Updated Application'); @@ -1607,59 +1600,6 @@ describe('ApplicationEditPage', () => { }); }); - describe('Copy to Clipboard', () => { - const originalClipboard = navigator.clipboard; - - afterEach(() => { - Object.defineProperty(navigator, 'clipboard', { - value: originalClipboard, - writable: true, - configurable: true, - }); - }); - - it('should copy text to clipboard when copy button is clicked', async () => { - const writeTextMock = vi.fn().mockResolvedValue(undefined); - Object.defineProperty(navigator, 'clipboard', { - value: {writeText: writeTextMock}, - writable: true, - configurable: true, - }); - - renderComponent(); - - fireEvent.click(screen.getByTestId('copy-button')); - - await waitFor(() => { - expect(writeTextMock).toHaveBeenCalledWith('test-text'); - }); - - await waitFor(() => { - expect(screen.getByTestId('copied-field')).toHaveTextContent('clientId'); - }); - }); - - it('should handle clipboard write failure gracefully', async () => { - const writeTextMock = vi.fn().mockRejectedValue(new Error('Clipboard error')); - Object.defineProperty(navigator, 'clipboard', { - value: {writeText: writeTextMock}, - writable: true, - configurable: true, - }); - - renderComponent(); - - fireEvent.click(screen.getByTestId('copy-button')); - - await waitFor(() => { - expect(writeTextMock).toHaveBeenCalledWith('test-text'); - }); - - // Component should still be functional after error - expect(screen.getByText('Test Application')).toBeInTheDocument(); - }); - }); - describe('Avatar Image Error', () => { it('should hide avatar image when image fails to load', () => { renderComponent(); @@ -1916,7 +1856,7 @@ describe('ApplicationEditPage', () => { expect(screen.getByRole('tab', {name: 'Advanced Settings'})).toBeInTheDocument(); }); - it('renders the General tab panel by default', () => { + it('renders the Overview tab panel by default, and the General tab panel once selected', async () => { mockUseGetApplication.mockReturnValue({ data: mockMcpApplication, isLoading: false, @@ -1924,8 +1864,14 @@ describe('ApplicationEditPage', () => { error: null, } as UseQueryResult); + const user = userEvent.setup(); renderComponent(); + expect(screen.getByTestId('integration-guides')).toBeInTheDocument(); + expect(screen.queryByTestId('mcp-connect-tab')).not.toBeInTheDocument(); + + await user.click(screen.getByRole('tab', {name: 'General'})); + expect(screen.getByTestId('mcp-connect-tab')).toBeInTheDocument(); }); @@ -1978,6 +1924,9 @@ describe('ApplicationEditPage', () => { const user = userEvent.setup(); renderComponent(); + // McpConnectTab only mounts once its General TabPanel is active + await mountSectionTab(user, 'General', 'mcp-connect-tab'); + // Make a change via the page header, which is shared across MCP and non-MCP layouts const {initialKey, keyAfterReset} = await editFieldAndResetSection( user, @@ -2102,6 +2051,9 @@ describe('ApplicationEditPage', () => { const user = userEvent.setup(); renderComponent(); + // McpConnectTab only mounts once its General TabPanel is active + await mountSectionTab(user, 'General', 'mcp-connect-tab'); + await user.click(screen.getByTestId('mcp-connect-tab-report-invalid')); // Trigger a change so the Save bar is rendered. diff --git a/frontend/apps/console/src/features/applications/utils/__tests__/getIntegrationGuidesForTemplate.test.ts b/frontend/apps/console/src/features/applications/utils/__tests__/getIntegrationGuidesForTemplate.test.ts index 36bcd29a21..1aa2cfc361 100644 --- a/frontend/apps/console/src/features/applications/utils/__tests__/getIntegrationGuidesForTemplate.test.ts +++ b/frontend/apps/console/src/features/applications/utils/__tests__/getIntegrationGuidesForTemplate.test.ts @@ -16,27 +16,10 @@ vi.mock('../../config/TechnologyBasedApplicationTemplateMetadata', () => ({ template: { id: 'express', integrationGuides: { - INBUILT: { + REDIRECT_BASED: { llm_prompt: { - id: 'express-llm', - title: 'AI-Assisted Integration', - description: 'Express integration guide', - type: 'llm' as const, - icon: 'express', - content: 'LLM prompt content', + docsUrl: 'https://thunderid.dev/prompts/redirect-based.txt', }, - manual_steps: [ - { - step: 1, - title: 'Step E1', - description: 'First step description', - }, - { - step: 2, - title: 'Step E2', - description: 'Second step description', - }, - ], }, }, }, @@ -45,44 +28,15 @@ vi.mock('../../config/TechnologyBasedApplicationTemplateMetadata', () => ({ template: { id: 'react', integrationGuides: { - INBUILT: { + REDIRECT_BASED: { llm_prompt: { - id: 'react-llm', - title: 'AI-Assisted Integration', - description: 'React integration guide', - type: 'llm' as const, - icon: 'react', - content: 'LLM prompt content', + docsUrl: 'https://thunderid.dev/prompts/redirect-based.txt', }, - manual_steps: [ - { - step: 1, - title: 'Step 1', - description: 'First step description', - }, - { - step: 2, - title: 'Step 2', - description: 'Second step description', - }, - ], }, EMBEDDED: { llm_prompt: { - id: 'react-embedded-llm', - title: 'AI-Assisted Integration', - description: 'React embedded integration guide', - type: 'llm' as const, - icon: 'react', - content: 'Embedded LLM prompt content', + docsUrl: 'https://thunderid.dev/prompts/embedded.txt', }, - manual_steps: [ - { - step: 1, - title: 'Embedded Step 1', - description: 'First embedded step description', - }, - ], }, }, }, @@ -91,27 +45,10 @@ vi.mock('../../config/TechnologyBasedApplicationTemplateMetadata', () => ({ template: { id: 'nextjs', integrationGuides: { - INBUILT: { + REDIRECT_BASED: { llm_prompt: { - id: 'nextjs-llm', - title: 'AI-Assisted Integration', - description: 'Next.js integration guide', - type: 'llm' as const, - icon: 'nextjs', - content: 'LLM prompt content', + docsUrl: 'https://thunderid.dev/prompts/redirect-based.txt', }, - manual_steps: [ - { - step: 1, - title: 'Step A', - description: 'First step description', - }, - { - step: 2, - title: 'Step B', - description: 'Second step description', - }, - ], }, }, }, @@ -130,27 +67,10 @@ vi.mock('../../config/PlatformBasedApplicationTemplateMetadata', () => ({ template: { id: 'browser', integrationGuides: { - INBUILT: { + REDIRECT_BASED: { llm_prompt: { - id: 'browser-llm', - title: 'AI-Assisted Integration', - description: 'Browser integration guide', - type: 'llm' as const, - icon: 'browser', - content: 'LLM prompt content', + docsUrl: 'https://thunderid.dev/prompts/redirect-based.txt', }, - manual_steps: [ - { - step: 1, - title: 'Step X', - description: 'First step description', - }, - { - step: 2, - title: 'Step Y', - description: 'Second step description', - }, - ], }, }, }, @@ -172,119 +92,39 @@ vi.mock('../normalizeTemplateId', () => ({ // Test data - define after mocks const mockReactGuides: IntegrationGuides = { - INBUILT: { + REDIRECT_BASED: { llm_prompt: { - id: 'react-llm', - title: 'AI-Assisted Integration', - description: 'React integration guide', - type: 'llm' as const, - icon: 'react', - content: 'LLM prompt content', + docsUrl: 'https://thunderid.dev/prompts/redirect-based.txt', }, - manual_steps: [ - { - step: 1, - title: 'Step 1', - description: 'First step description', - }, - { - step: 2, - title: 'Step 2', - description: 'Second step description', - }, - ], }, EMBEDDED: { llm_prompt: { - id: 'react-embedded-llm', - title: 'AI-Assisted Integration', - description: 'React embedded integration guide', - type: 'llm' as const, - icon: 'react', - content: 'Embedded LLM prompt content', + docsUrl: 'https://thunderid.dev/prompts/embedded.txt', }, - manual_steps: [ - { - step: 1, - title: 'Embedded Step 1', - description: 'First embedded step description', - }, - ], }, }; const mockExpressGuides: IntegrationGuides = { - INBUILT: { + REDIRECT_BASED: { llm_prompt: { - id: 'express-llm', - title: 'AI-Assisted Integration', - description: 'Express integration guide', - type: 'llm' as const, - icon: 'express', - content: 'LLM prompt content', + docsUrl: 'https://thunderid.dev/prompts/redirect-based.txt', }, - manual_steps: [ - { - step: 1, - title: 'Step E1', - description: 'First step description', - }, - { - step: 2, - title: 'Step E2', - description: 'Second step description', - }, - ], }, }; const mockNextjsGuides: IntegrationGuides = { - INBUILT: { + REDIRECT_BASED: { llm_prompt: { - id: 'nextjs-llm', - title: 'AI-Assisted Integration', - description: 'Next.js integration guide', - type: 'llm' as const, - icon: 'nextjs', - content: 'LLM prompt content', + docsUrl: 'https://thunderid.dev/prompts/redirect-based.txt', }, - manual_steps: [ - { - step: 1, - title: 'Step A', - description: 'First step description', - }, - { - step: 2, - title: 'Step B', - description: 'Second step description', - }, - ], }, }; const mockBrowserGuides: IntegrationGuides = { - INBUILT: { + REDIRECT_BASED: { llm_prompt: { - id: 'browser-llm', - title: 'AI-Assisted Integration', - description: 'Browser integration guide', - type: 'llm' as const, - icon: 'browser', - content: 'LLM prompt content', + docsUrl: 'https://thunderid.dev/prompts/redirect-based.txt', }, - manual_steps: [ - { - step: 1, - title: 'Step X', - description: 'First step description', - }, - { - step: 2, - title: 'Step Y', - description: 'Second step description', - }, - ], }, }; @@ -365,35 +205,37 @@ describe('getIntegrationGuidesForTemplate', () => { }); describe('getIntegrationGuideVariantKey', () => { - it('should resolve INBUILT for a template without the embedded suffix', () => { - expect(getIntegrationGuideVariantKey('react')).toBe('INBUILT'); + it('should resolve REDIRECT_BASED for a template without the embedded suffix', () => { + expect(getIntegrationGuideVariantKey('react')).toBe('REDIRECT_BASED'); }); it('should resolve EMBEDDED for a template with the embedded suffix', () => { expect(getIntegrationGuideVariantKey('react-embedded')).toBe('EMBEDDED'); }); - it('should resolve INBUILT for undefined and null template IDs', () => { - expect(getIntegrationGuideVariantKey(undefined)).toBe('INBUILT'); - expect(getIntegrationGuideVariantKey(null)).toBe('INBUILT'); + it('should resolve REDIRECT_BASED for undefined and null template IDs', () => { + expect(getIntegrationGuideVariantKey(undefined)).toBe('REDIRECT_BASED'); + expect(getIntegrationGuideVariantKey(null)).toBe('REDIRECT_BASED'); }); }); describe('getIntegrationGuideForTemplate', () => { - it('should return the INBUILT guide for the react template', () => { - expect(getIntegrationGuideForTemplate('react')).toEqual(mockReactGuides.INBUILT); + it('should return the REDIRECT_BASED guide for the react template', () => { + expect(getIntegrationGuideForTemplate('react')).toEqual(mockReactGuides.REDIRECT_BASED); }); it('should return the EMBEDDED guide for the react-embedded template', () => { expect(getIntegrationGuideForTemplate('react-embedded')).toEqual(mockReactGuides.EMBEDDED); }); - it('should return the INBUILT guide for the express template', () => { - expect(getIntegrationGuideForTemplate('express')).toEqual(mockExpressGuides.INBUILT); + it('should return the REDIRECT_BASED guide for the express template', () => { + expect(getIntegrationGuideForTemplate('express')).toEqual(mockExpressGuides.REDIRECT_BASED); }); - it('should return null for express-embedded since no EMBEDDED variant exists', () => { - expect(getIntegrationGuideForTemplate('express-embedded')).toBeNull(); + it('should fall back to the REDIRECT_BASED guide for express-embedded since no EMBEDDED variant exists', () => { + // Every technology template should offer a coding-agent prompt regardless of sign-in + // approach, so a missing variant falls back to whichever one is authored. + expect(getIntegrationGuideForTemplate('express-embedded')).toEqual(mockExpressGuides.REDIRECT_BASED); }); it('should return null for a template without any integration guides', () => { diff --git a/frontend/apps/console/src/features/applications/utils/__tests__/getPlaygroundsForTemplate.test.ts b/frontend/apps/console/src/features/applications/utils/__tests__/getPlaygroundsForTemplate.test.ts new file mode 100644 index 0000000000..07c45262d1 --- /dev/null +++ b/frontend/apps/console/src/features/applications/utils/__tests__/getPlaygroundsForTemplate.test.ts @@ -0,0 +1,82 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import {describe, expect, it, vi} from 'vitest'; +import getPlaygroundsForTemplate from '../getPlaygroundsForTemplate'; + +vi.mock('../../config/TechnologyBasedApplicationTemplateMetadata', () => ({ + default: [ + { + template: { + id: 'react', + playgrounds: [ + { + label: 'React', + environment: 'stackblitz', + url: 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/react/quickstart?file=README.md', + }, + ], + }, + }, + { + template: { + id: 'mcp-client', + }, + }, + ], +})); + +vi.mock('../../config/PlatformBasedApplicationTemplateMetadata', () => ({ + default: [ + { + template: { + id: 'browser', + }, + }, + { + template: { + // Native mobile platforms have no runnable environment, so no playgrounds. + id: 'mobile', + }, + }, + ], +})); + +describe('getPlaygroundsForTemplate', () => { + it('returns null when no template ID is provided', () => { + expect(getPlaygroundsForTemplate(undefined)).toBeNull(); + }); + + it('returns the playgrounds for a technology-based template', () => { + expect(getPlaygroundsForTemplate('react')).toEqual([ + { + label: 'React', + environment: 'stackblitz', + url: 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/react/quickstart?file=README.md', + }, + ]); + }); + + it('resolves embedded template variants to their base template', () => { + expect(getPlaygroundsForTemplate('react-embedded')).toEqual([ + { + label: 'React', + environment: 'stackblitz', + url: 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/react/quickstart?file=README.md', + }, + ]); + }); + + it('returns null when the template has no playground configured', () => { + expect(getPlaygroundsForTemplate('mcp-client')).toBeNull(); + expect(getPlaygroundsForTemplate('browser')).toBeNull(); + }); + + it('returns null for a template with no runnable environment (e.g. native mobile)', () => { + expect(getPlaygroundsForTemplate('mobile')).toBeNull(); + }); + + it('returns null for an unknown template ID', () => { + expect(getPlaygroundsForTemplate('unknown-template')).toBeNull(); + }); +}); diff --git a/frontend/apps/console/src/features/applications/utils/__tests__/getQuickstartsForTemplate.test.ts b/frontend/apps/console/src/features/applications/utils/__tests__/getQuickstartsForTemplate.test.ts new file mode 100644 index 0000000000..b64bcf64c8 --- /dev/null +++ b/frontend/apps/console/src/features/applications/utils/__tests__/getQuickstartsForTemplate.test.ts @@ -0,0 +1,95 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import {describe, expect, it, vi} from 'vitest'; +import getQuickstartsForTemplate from '../getQuickstartsForTemplate'; + +vi.mock('../../config/TechnologyBasedApplicationTemplateMetadata', () => ({ + default: [ + { + template: { + id: 'react', + quickstarts: [ + { + label: 'React', + docsUrl: 'https://thunderid.dev/docs/next/guides/getting-started/connect-your-application/react/', + }, + ], + }, + }, + { + template: { + id: 'mcp-client', + }, + }, + ], +})); + +vi.mock('../../config/PlatformBasedApplicationTemplateMetadata', () => ({ + default: [ + { + template: { + id: 'browser', + }, + }, + { + template: { + id: 'mobile', + quickstarts: [ + { + label: 'iOS', + docsUrl: 'https://thunderid.dev/docs/next/guides/getting-started/connect-your-application/ios/', + }, + { + label: 'Android', + docsUrl: 'https://thunderid.dev/docs/next/guides/getting-started/connect-your-application/android/', + }, + { + label: 'Flutter', + docsUrl: 'https://thunderid.dev/docs/next/guides/getting-started/connect-your-application/flutter/', + }, + ], + }, + }, + ], +})); + +describe('getQuickstartsForTemplate', () => { + it('returns null when no template ID is provided', () => { + expect(getQuickstartsForTemplate(undefined)).toBeNull(); + }); + + it('returns the quickstart guides for a technology-based template', () => { + expect(getQuickstartsForTemplate('react')).toEqual([ + { + label: 'React', + docsUrl: 'https://thunderid.dev/docs/next/guides/getting-started/connect-your-application/react/', + }, + ]); + }); + + it('resolves embedded template variants to their base template', () => { + expect(getQuickstartsForTemplate('react-embedded')).toEqual([ + { + label: 'React', + docsUrl: 'https://thunderid.dev/docs/next/guides/getting-started/connect-your-application/react/', + }, + ]); + }); + + it('returns one quickstart guide per platform for a multi-platform template', () => { + const quickstarts = getQuickstartsForTemplate('mobile'); + + expect(quickstarts).toHaveLength(3); + expect(quickstarts?.map((q) => q.label)).toEqual(['iOS', 'Android', 'Flutter']); + }); + + it('returns null when the template has no quickstart configured', () => { + expect(getQuickstartsForTemplate('mcp-client')).toBeNull(); + expect(getQuickstartsForTemplate('browser')).toBeNull(); + }); + + it('returns null for an unknown template ID', () => { + expect(getQuickstartsForTemplate('unknown-template')).toBeNull(); + }); +}); diff --git a/frontend/apps/console/src/features/applications/utils/getIntegrationGuidesForTemplate.ts b/frontend/apps/console/src/features/applications/utils/getIntegrationGuidesForTemplate.ts index 7b8782c9bf..71badeedb1 100644 --- a/frontend/apps/console/src/features/applications/utils/getIntegrationGuidesForTemplate.ts +++ b/frontend/apps/console/src/features/applications/utils/getIntegrationGuidesForTemplate.ts @@ -46,26 +46,30 @@ export default function getIntegrationGuidesForTemplate(templateId: string | und * Resolves the integration guide variant key for a template ID. * * Templates with the '-embedded' suffix (e.g., 'react-embedded') use the EMBEDDED - * variant; all others use the INBUILT variant. + * variant; all others use the REDIRECT_BASED variant. * * @param templateId - The template ID (e.g., 'react', 'react-embedded') - * @returns The guide variant key (EMBEDDED or INBUILT) + * @returns The guide variant key (EMBEDDED or REDIRECT_BASED) */ export function getIntegrationGuideVariantKey(templateId: string | undefined | null): string { const isEmbedded = templateId?.includes(TemplateConstants.EMBEDDED_SUFFIX) ?? false; - return isEmbedded ? ApplicationCreateFlowSignInApproach.EMBEDDED : ApplicationCreateFlowSignInApproach.INBUILT; + return isEmbedded ? ApplicationCreateFlowSignInApproach.EMBEDDED : ApplicationCreateFlowSignInApproach.REDIRECT_BASED; } /** * Gets the integration guide for the variant selected by a template ID. * * Unlike {@link getIntegrationGuidesForTemplate}, which returns the full guides object, - * this returns only the guide for the selected variant (EMBEDDED or INBUILT), or null - * when that variant has no content. Use this to decide whether a guide can be rendered. + * this returns only the guide for the selected variant (EMBEDDED or REDIRECT_BASED), or null + * when neither variant has content. Use this to decide whether a guide can be rendered. + * + * Falls back to the other variant when the selected one isn't authored for this template (e.g. a + * template whose default sign-in approach is EMBEDDED but which only has REDIRECT_BASED guide content), + * every technology template should offer a coding-agent prompt regardless of sign-in approach. * * @param templateId - The template ID (e.g., 'react', 'react-embedded', 'express-embedded') - * @returns The guide for the selected variant, or null if not found + * @returns The guide for the selected variant (or the other variant as a fallback), or null if neither exists */ export function getIntegrationGuideForTemplate(templateId: string | undefined): IntegrationGuides[string] | null { const guides = getIntegrationGuidesForTemplate(templateId); @@ -74,5 +78,11 @@ export function getIntegrationGuideForTemplate(templateId: string | undefined): return null; } - return guides[getIntegrationGuideVariantKey(templateId)] ?? null; + const selectedVariant = getIntegrationGuideVariantKey(templateId); + const otherVariant = + selectedVariant === ApplicationCreateFlowSignInApproach.EMBEDDED + ? ApplicationCreateFlowSignInApproach.REDIRECT_BASED + : ApplicationCreateFlowSignInApproach.EMBEDDED; + + return guides[selectedVariant] ?? guides[otherVariant] ?? null; } diff --git a/frontend/apps/console/src/features/applications/utils/getPlaygroundsForTemplate.ts b/frontend/apps/console/src/features/applications/utils/getPlaygroundsForTemplate.ts new file mode 100644 index 0000000000..d5ed4df8d4 --- /dev/null +++ b/frontend/apps/console/src/features/applications/utils/getPlaygroundsForTemplate.ts @@ -0,0 +1,40 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import normalizeTemplateId from './normalizeTemplateId'; +import PlatformBasedApplicationTemplateMetadata from '../config/PlatformBasedApplicationTemplateMetadata'; +import TechnologyBasedApplicationTemplateMetadata from '../config/TechnologyBasedApplicationTemplateMetadata'; +import type {PlaygroundLink} from '../models/application-templates'; + +/** + * Gets the runnable playgrounds (e.g. StackBlitz samples) for a given template ID. Most templates + * have exactly one, or none for platforms with no runnable environment (e.g. native mobile). + * @param templateId - The template ID (e.g., 'react', 'react-embedded', 'nextjs', 'mobile') + * @returns The playgrounds, or null when this template has none + */ +export default function getPlaygroundsForTemplate(templateId: string | undefined): PlaygroundLink[] | null { + if (!templateId) { + return null; + } + + // Normalize the template ID to handle embedded variants (e.g., 'react-embedded' -> 'react') + const normalizedTemplateId = normalizeTemplateId(templateId) ?? templateId; + + const techTemplate = TechnologyBasedApplicationTemplateMetadata.find( + (metadata) => metadata.template.id === normalizedTemplateId, + ); + + if (techTemplate?.template.playgrounds) { + return techTemplate.template.playgrounds; + } + + const platformTemplate = PlatformBasedApplicationTemplateMetadata.find( + (metadata) => metadata.template.id === normalizedTemplateId, + ); + + if (platformTemplate?.template.playgrounds) { + return platformTemplate.template.playgrounds; + } + + return null; +} diff --git a/frontend/apps/console/src/features/applications/utils/getQuickstartsForTemplate.ts b/frontend/apps/console/src/features/applications/utils/getQuickstartsForTemplate.ts new file mode 100644 index 0000000000..9f2e7904ed --- /dev/null +++ b/frontend/apps/console/src/features/applications/utils/getQuickstartsForTemplate.ts @@ -0,0 +1,42 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import normalizeTemplateId from './normalizeTemplateId'; +import PlatformBasedApplicationTemplateMetadata from '../config/PlatformBasedApplicationTemplateMetadata'; +import TechnologyBasedApplicationTemplateMetadata from '../config/TechnologyBasedApplicationTemplateMetadata'; +import type {QuickstartLink} from '../models/application-templates'; + +/** + * Gets the quickstart guides (hosted docs, and StackBlitz sample where runnable) for a given + * template ID. Most templates have exactly one; templates that cover more than one platform SDK + * (e.g. the generic Mobile template covering iOS, Android, and Flutter) return one entry per + * platform. + * @param templateId - The template ID (e.g., 'react', 'react-embedded', 'nextjs', 'mobile') + * @returns The quickstart guides, or null when this template has none + */ +export default function getQuickstartsForTemplate(templateId: string | undefined): QuickstartLink[] | null { + if (!templateId) { + return null; + } + + // Normalize the template ID to handle embedded variants (e.g., 'react-embedded' -> 'react') + const normalizedTemplateId = normalizeTemplateId(templateId) ?? templateId; + + const techTemplate = TechnologyBasedApplicationTemplateMetadata.find( + (metadata) => metadata.template.id === normalizedTemplateId, + ); + + if (techTemplate?.template.quickstarts) { + return techTemplate.template.quickstarts; + } + + const platformTemplate = PlatformBasedApplicationTemplateMetadata.find( + (metadata) => metadata.template.id === normalizedTemplateId, + ); + + if (platformTemplate?.template.quickstarts) { + return platformTemplate.template.quickstarts; + } + + return null; +} diff --git a/frontend/apps/console/src/features/applications/utils/resolveTemplateLink.ts b/frontend/apps/console/src/features/applications/utils/resolveTemplateLink.ts new file mode 100644 index 0000000000..ed2c17813a --- /dev/null +++ b/frontend/apps/console/src/features/applications/utils/resolveTemplateLink.ts @@ -0,0 +1,31 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +const DOCUMENTATION_LINK_KEY_PATTERN = /^\{\{(.+)\}\}$/; + +/** + * Resolves a template field that may hold either a literal URL or a `{{documentation.links key}}` + * reference (e.g. '{{applications.templates.react.docs}}'), matching the `{{productName}}`/ + * `{{clientId}}` placeholder convention already used elsewhere in these templates, so the docs + * site stays the source of truth. A value that isn't wrapped in `{{ }}` is used as-is, which keeps + * not-yet-migrated templates working unchanged. A `{{ }}`-wrapped reference that isn't configured + * resolves to undefined rather than the raw placeholder, so callers can hide the dependent UI. + * + * Used for {@link QuickstartLink.docsUrl}, {@link PlaygroundLink.url}, and + * {@link IntegrationGuide.docsUrl}. + * + * @public + */ +export default function resolveTemplateLink( + value: string | undefined, + getDocumentationLink: (key: string) => string | undefined, +): string | undefined { + if (!value) { + return undefined; + } + const keyMatch = DOCUMENTATION_LINK_KEY_PATTERN.exec(value); + if (!keyMatch) { + return value; + } + return getDocumentationLink(keyMatch[1]); +} diff --git a/frontend/packages/components/src/CopyableField/CopyableField.tsx b/frontend/packages/components/src/CopyableField/CopyableField.tsx new file mode 100644 index 0000000000..2b54d18502 --- /dev/null +++ b/frontend/packages/components/src/CopyableField/CopyableField.tsx @@ -0,0 +1,101 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import {useLogger} from '@thunderid/logger/react'; +import {Box, Stack, Typography, Tooltip, IconButton} from '@wso2/oxygen-ui'; +import {Copy, Check} from '@wso2/oxygen-ui-icons-react'; +import {useState, useCallback, useRef, useEffect, type JSX} from 'react'; +import {useTranslation} from 'react-i18next'; + +export interface CopyableFieldProps { + /** + * Small label rendered above the value (e.g. "Application ID", "Token endpoint"). + */ + label: string; + /** + * The value displayed in monospace and copied to the clipboard. + */ + value: string; + /** + * Tooltip text shown before copying. Falls back to a generic "Copy" label. + */ + copyLabel?: string; +} + +/** + * A labeled, bordered row displaying a monospace identifier or URL with a click-to-copy button. + * Used for read-only identifiers and endpoints in resource overview panels (e.g. application IDs, + * OIDC endpoints). + */ +export default function CopyableField({label, value, copyLabel = undefined}: CopyableFieldProps): JSX.Element { + const {t} = useTranslation(); + const logger = useLogger('CopyableField'); + const [copied, setCopied] = useState(false); + const timeoutRef = useRef | null>(null); + + useEffect( + () => () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }, + [], + ); + + const handleCopy = useCallback(async () => { + await navigator.clipboard.writeText(value); + setCopied(true); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => setCopied(false), 2000); + }, [value]); + + const handleClick = () => { + handleCopy().catch((error: unknown) => { + logger.error('Failed to copy to clipboard', error instanceof Error ? error : {error}); + }); + }; + + return ( + + + {label} + + + + {value} + + + + {copied ? : } + + + + + ); +} diff --git a/frontend/packages/components/src/CopyableField/__tests__/CopyableField.test.tsx b/frontend/packages/components/src/CopyableField/__tests__/CopyableField.test.tsx new file mode 100644 index 0000000000..a3a87938ed --- /dev/null +++ b/frontend/packages/components/src/CopyableField/__tests__/CopyableField.test.tsx @@ -0,0 +1,64 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import {render, screen, waitFor, fireEvent} from '@testing-library/react'; +import {describe, it, expect, vi, afterEach} from 'vitest'; +import CopyableField from '../CopyableField'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, fallback?: string) => fallback ?? key, + }), +})); + +vi.mock('@thunderid/logger/react', () => ({ + useLogger: () => ({ + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + }), +})); + +describe('CopyableField', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('renders the label and value', () => { + render(); + + expect(screen.getByText('Client ID')).toBeInTheDocument(); + expect(screen.getByText('abc123')).toBeInTheDocument(); + }); + + it('copies the value to the clipboard on click', async () => { + const mockClipboard = {writeText: vi.fn().mockResolvedValue(undefined)}; + Object.defineProperty(navigator, 'clipboard', {value: mockClipboard, writable: true, configurable: true}); + + render(); + fireEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(mockClipboard.writeText).toHaveBeenCalledWith('abc123'); + }); + }); + + it('logs an error when the clipboard write fails', async () => { + const mockClipboard = {writeText: vi.fn().mockRejectedValue(new Error('denied'))}; + Object.defineProperty(navigator, 'clipboard', {value: mockClipboard, writable: true, configurable: true}); + + render(); + fireEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(mockClipboard.writeText).toHaveBeenCalled(); + }); + }); + + it('uses a custom copy label for the button aria-label', () => { + render(); + + expect(screen.getByRole('button', {name: 'Copy client ID'})).toBeInTheDocument(); + }); +}); diff --git a/frontend/packages/components/src/ExternalLinkConfirm/ExternalLinkConfirmDialog.tsx b/frontend/packages/components/src/ExternalLinkConfirm/ExternalLinkConfirmDialog.tsx index 7e656bf38c..9b16ce3e7b 100644 --- a/frontend/packages/components/src/ExternalLinkConfirm/ExternalLinkConfirmDialog.tsx +++ b/frontend/packages/components/src/ExternalLinkConfirm/ExternalLinkConfirmDialog.tsx @@ -141,13 +141,16 @@ export default function ExternalLinkConfirmDialog({ flexShrink: 0, }} > - + {t('common:externalLink.title', {productName})} - {t('common:externalLink.message', {productName})}{' '} + {t( + 'common:externalLink.message', + 'This link leads to an external site. Please verify the destination before proceeding:', + )}{' '} {hostname} diff --git a/frontend/packages/components/src/ExternalLinkConfirm/__tests__/ExternalLinkConfirmDialog.test.tsx b/frontend/packages/components/src/ExternalLinkConfirm/__tests__/ExternalLinkConfirmDialog.test.tsx new file mode 100644 index 0000000000..e3abb51491 --- /dev/null +++ b/frontend/packages/components/src/ExternalLinkConfirm/__tests__/ExternalLinkConfirmDialog.test.tsx @@ -0,0 +1,87 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import {render, screen, fireEvent} from '@testing-library/react'; +import {describe, it, expect, vi} from 'vitest'; +import ExternalLinkConfirmDialog from '../ExternalLinkConfirmDialog'; + +const KNOWN_TRANSLATIONS: Record = { + 'common:actions.stay': 'Stay', + 'common:actions.continue': 'Continue', + 'common:externalLink.title': 'You are leaving ThunderID', +}; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, fallback?: string) => KNOWN_TRANSLATIONS[key] ?? (typeof fallback === 'string' ? fallback : key), + }), +})); + +vi.mock('@thunderid/contexts', () => ({ + useConfig: () => ({ + config: {brand: {product_name: 'ThunderID'}}, + }), +})); + +vi.mock('@thunderid/logger/react', () => ({ + useLogger: () => ({error: vi.fn(), info: vi.fn(), debug: vi.fn(), warn: vi.fn()}), +})); + +describe('ExternalLinkConfirmDialog', () => { + it('does not render dialog content when closed', () => { + render( + , + ); + + expect(screen.queryByText("You're leaving the console")).not.toBeInTheDocument(); + }); + + it('shows the destination URL when open', () => { + render( + , + ); + + expect(screen.getByText('You are leaving ThunderID')).toBeInTheDocument(); + expect(screen.getByDisplayValue('https://thunderid.dev/docs')).toBeInTheDocument(); + }); + + it('invokes onCancel when Cancel is clicked', () => { + const onCancel = vi.fn(); + render( + , + ); + + fireEvent.click(screen.getByRole('button', {name: 'Stay'})); + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + it('invokes onConfirm when Continue is clicked', () => { + const onConfirm = vi.fn(); + render( + , + ); + + fireEvent.click(screen.getByRole('button', {name: 'Continue'})); + expect(onConfirm).toHaveBeenCalledTimes(1); + }); +}); diff --git a/frontend/packages/components/src/StackblitzQuickstartCard/StackblitzQuickstartCard.tsx b/frontend/packages/components/src/StackblitzQuickstartCard/StackblitzQuickstartCard.tsx new file mode 100644 index 0000000000..b94229455c --- /dev/null +++ b/frontend/packages/components/src/StackblitzQuickstartCard/StackblitzQuickstartCard.tsx @@ -0,0 +1,108 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import {Box, Stack, Typography} from '@wso2/oxygen-ui'; +import {ArrowRight} from '@wso2/oxygen-ui-icons-react'; +import type {JSX, ReactNode} from 'react'; +import WEB_CONTAINER_SPRITE_DATA_URI from './webContainerSprite'; +import StackblitzIcon from '../icons/logos/vendor/StackblitzIcon'; + +export interface StackblitzQuickstartCardProps { + /** + * The StackBlitz fork/edit URL to open in a new tab. + */ + url: string; + /** + * Primary heading line, e.g. "Try the live quickstart". + */ + heading: string; + /** + * Secondary heading line rendered below the heading with less emphasis, e.g. "Run Bifrost in StackBlitz". + * Accepts a node so callers can emphasize part of it, e.g. the app name in a monospace tag. + */ + subheading?: ReactNode; + /** + * Label for the call-to-action, e.g. "Open on StackBlitz". + */ + ctaLabel: string; +} + +/** + * A dark banner card linking out to a StackBlitz-hosted quickstart sample for the resource's + * template. Shared across resource overview tabs (applications, agents, etc.) that offer a + * live, runnable quickstart. + */ +export default function StackblitzQuickstartCard({ + url, + heading, + subheading = undefined, + ctaLabel, +}: StackblitzQuickstartCardProps): JSX.Element { + return ( + + + + + + + + {heading} + + {subheading && ( + + {subheading} + + )} + + + {ctaLabel} + + + + + + ); +} diff --git a/frontend/packages/components/src/StackblitzQuickstartCard/__tests__/StackblitzQuickstartCard.test.tsx b/frontend/packages/components/src/StackblitzQuickstartCard/__tests__/StackblitzQuickstartCard.test.tsx new file mode 100644 index 0000000000..3e549ba859 --- /dev/null +++ b/frontend/packages/components/src/StackblitzQuickstartCard/__tests__/StackblitzQuickstartCard.test.tsx @@ -0,0 +1,30 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import {render, screen} from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import StackblitzQuickstartCard from '../StackblitzQuickstartCard'; + +describe('StackblitzQuickstartCard', () => { + it('renders the title and links to the given StackBlitz URL in a new tab', () => { + render( + , + ); + + const link = screen.getByRole('link'); + expect(link).toHaveAttribute( + 'href', + 'https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/react/quickstart', + ); + expect(link).toHaveAttribute('target', '_blank'); + expect(link).toHaveAttribute('rel', 'noopener noreferrer'); + expect(screen.getByText('Try the live quickstart')).toBeInTheDocument(); + expect(screen.getByText('Run react-quickstart in StackBlitz')).toBeInTheDocument(); + expect(screen.getByText('Open on StackBlitz')).toBeInTheDocument(); + }); +}); diff --git a/frontend/packages/components/src/StackblitzQuickstartCard/webContainerSprite.ts b/frontend/packages/components/src/StackblitzQuickstartCard/webContainerSprite.ts new file mode 100644 index 0000000000..d0ae075ba1 --- /dev/null +++ b/frontend/packages/components/src/StackblitzQuickstartCard/webContainerSprite.ts @@ -0,0 +1,8 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +/** WebContainer isometric sprite rendered behind the StackBlitz quickstart card CTA. */ +const WEB_CONTAINER_SPRITE_DATA_URI = + 'data:image/webp;base64,UklGRr42AABXRUJQVlA4WAoAAAAQAAAAGgMAaAEAQUxQSHAGAAABoOPattXYohenF4RkpLSjyF8XSN2J+jm3ZJEROXXoLtwPzbA5W0KH9y2ZvfZ8JUUEHTaSJEmSJVJwGNZX+u4io2dHrbQ7abYdPm0VI6fJtsdMMcKwz9csRHg1o4J0sQIiRH2zAiJEU/yUTMTe9v2ESD8SYtNPhKiLDiZinw0LBisOESKdzQdMgTF590CMiX4s6LL5gTYo+pEQZT8SMc2GBd1i3qANhcm7OxiD4YEvEjGdDQvSzXxCGwQbtk5hCoN+n8QQ6MeCTQcRot50ECH6xZxzxP+MaSpIm37fCPU3bJ0jtO9HInbZfITyG7ZIiLO5iUn1DVskYueqa5pP3v0k1O73FaPeG7ZIxHQzLGg2Hc4Sim/YwjBReTWnHZE7vIbQH+RDhOg3lwgxZPPc0bcfCy7mPFqBn74TIbps/jvK9mPB5oEvEaK+mQ+ZhN2wRSL2frwma78Thaj9RIhU9iMR68WwoN90ECFey+tLIeaXskEi1rNhQefUa62I/U5llHHDlol4MSyo71ZAhOgXcy2TgA98mYhnc+/Rb8OWCNHN5mBG6fr9G2Xr9zHK9RMh0mxuZlLt2xZAIg6uvqbZhi0RIl3N2WfSa/KOROxmw4I+m8NpdevH4PlW2YFErO/mdYR64EuEGLL5nUmjb1sAiVhfDQvq2ZxPq02/9xnV6Wcink2BI86GLRGiW0wDJl02bJmIr9moIG36iRD1zZRgFKP/blocNTZsseBVjmuNDlxNjzOKsWGLROw1uSbI5J2FiUpa+0VhkuEzppGI9WxY0G86VBFCbNjCMFE5i3PNnL9hqw7+f+BLhOgVuja5/oGvPtHtG7ZUkG6mEq3L+2VidPrkXaXo8H6l8PkDX52Exx/4CiX8PV9EIg6LUUHa9KslfN0vl/B0PxKxW0w1tOgP94/cvWbTjcnHHUjETrxr3p28i0fr5Y+4gmGicjYBj2M/4goLuk0HEWJYTENOft2wlTD69IGvjLh18k6E6KS8Nnq0X8jozX4s2PRLKdz5j7kgQvSKXjM/9lNBumbTlMaR37YAEaKbTdfjwX5hcWU/DBOVc9lPhEhlBxKxuxsW9IvpS+O4B74Cc3Ld5B2JWK/9VNDJfK31Xj8LE5XX4jIR136l8dmGLRGi31wixFB2EBHTaza98dSGreA0vpovyi081S94dFI/FlyzyU7rpQ1bIkRXdiARB/GveeeBr/g0vtmwVZ+Tdybv4kfH9GNBv1gI0PqnH4aJStmPRKxnMyro7xYINK554IvCRCWV/UjEdLVoOA7pDwXhif54YPRFPxPxmo0K6vnvXAqeb3UPKw4R4lBeImI63I0K0rhYZNDs+/1R9hMhukdxkYgvaz8VjP9ufw71iUr68e/230Nd1GV/fEQfdfxQHR9N3u9RPlFJa3+YnNMe9/8XPFSPTupXHSfNFwNcHMobKHHc38k7EbFe+6ngZdMfLWKP+1mYqBzKDiJiWvupIG37Q0bs5YYtEeJQ9iMR147YYRc7Akfs4ANfIMRL2YFEPITQtZ3rJ0Js+4EQ9eOp3iWvJ87iy/OdvD8ey6IKu9UPhEjb/igSImzYxlfsfmQLJhw/eX88BBKO37DNOS9Zmx96dPvkPVsujzTR/0/fpcH1G7YCid2ZL7IwUdnMF4mIKbSu7cWGLRDiZXPjSuxAPxJx7cCCHwF2rXn2G7bBJZ76hm18xafdDwVp2x9h4nlv2IaYeNKTdyTiy9pBBYdNR5wJ/98gWkzvbD1UxDSDQXU240L0G5CYqNQzGeJChhjIEHW4ccS4lAw6MkRFxkSlQkMMZJC+a4gLGbH/riFuYDzfql6/2JcOj+Y3KpkMMZMhXsNtorLHpTmkYz2DIapzuAky4vG5RiMjoiFeyRDt1xI5mr+Y4HcNMZAhEhoio/FGhQw6NMSXiTEtLl/HkVGaQ1rMX+XmkBY3MsTr/4hLKzI+BqshYxB/fK70aEQyqG5kiCHYOD1XajQiGlyiTZBReuS4dCaDVzJER8ZEJaERz2TQkyHqYItHNAbxT5aBDDoyRLqRIV7RiGSQyBBVsIkTx6WJDCo0xDnYJiocl1ZocCNDjBmPj9zNYRaf96dRZxpI+S9/JWYSxPj3fhFmCCYqf2z+rQpUiLweJkg//u7f2zIDE5UjGc+3GjTeqPz4i7/AwcDJjAsxoCF+kCGmYGMfShkQLRki/UAjksEQeqICVlA4ICgwAABQBwGdASobA2kBPm00l0kkIqekIdG5gPANiWdu+DYIAuo3nBOG7PqDttxUfS89a5v6r+1+tHhN9d5o/uPqAfMr/jeqj9U+i357fsB823nden//h+ov+0vr9+rl6IfnP+s//cfNG1Qr1T/o/w+8zP8b/pP7l3lfsf9h+aucz/Red/+673fzT+l9AX8Q/lf+v/tv4q8iyAH9K/u/o6feehP936gH+R9LO9v9R9gP9O+kzn/et/YP8un2Nfu7///enFPkKLKmaSVPQO78aUzYJnmJxJ3jh5IrT9SPaCsJPZKAQ6T7/5ZuQmugk5wCl6g/qfxTnSpmklT0Du/GlT0Du/GjnJqNMrBRLOvRvuv9K7eXmzLuChuCE8fY37muQIIh2B1chRZUzSSp6B3fjSp43thc9euKWdTg++SzrxEu/pYNEbHO7fW1hrK5Hnk4J6DqB3fjSp6B3fjSp6B3XqeJVZk6OgBoRx593W9KknAJjrDiLdTjcdaVGudcmXRVv/73SyS+/GlT0Du/GlT0Du96iVFGGjhdTbvd9zqWMQjYBF2LdojtYwoU/dPCudSNKhXUYvqkdXIUWVM0kqegd34xrCrF/Bvy1XCkziehaxSUvMnHynwipjh4P6BnNjMEQ46e3QIQmpcfgtbAysyBIYpKnoHd+NKnoHWnyj2UTdbIQsWJ+E+tUbZqS1pMBvP3C/QZL/VJZbohVyaINFK7rZPOLUwOxt4M6hUzSSp6B3fjSp43thZ8wZSx/9yebUCHsjYNemsFrdMSw78BOg6+taJ1vFFfLbOjglQW4UQhRE0VDoXPdyqOoPoaJXdIpySp6B3fjSp6B1wNAYhk6ohGUTXEALNkf7C2G5ooSCRDSQpDwhVa8G6fBf2oT20nYHDNkEfzB4DRWrv87t7Aho2V7lqIrBuYKs8SL0GmtK3LKmaSVPQO78E+7lTyDPXWYnYHMh3Jz2jXaWB1AGSy1B08hPFqF/+w5MjXBgIS4p6rmNXoPUIvk8ZjnW8qPSZJ8tsebtyCH78DRTcwfW0kFK2X5B0P3tTMZr2s66NV8wtUuQosqZpJU8ZXcV934y/TEsrWBTiijwLMSNgR7e3kHLf1LO733gOUhvvP/gl+G3v/9mYlk++m8P3859vXxNnuZFbkGthmdkgBEE2Rm9S92NtHtPvffQ4GqwWFgY+tctVeAO7Rq39mIObhXRpGIbSp6B3fjR0KH6UXeNN7XdbUhaw65B51tVBID3wSaH8f/OuqHFPsjM4p/+ueEf/9n+jrq0iGROitxfF4crtvxpSnIl8M/+RlD//Y+dw9xClEAIWRQ0ACx0BKTDPZuOPCTUkgPnfrGkmJJJU9A7vxo7fD8bYkGAe7uI6ES9KiJ4nGYs6VgOfv5R/0A+fUPAoPyd/uXjMfmL3vmr4FL0txyteQYclRZ30BUVJIocbD2VPpGdYV48DncLePcva5udv7QiNNZryGrcjcvD99ESYvsgeDi7IWoK+16CTpl4URZUzSSmBbsyhc/fr/Ilu66fMouo5aRjB5irzk8K+LEobPSbMXO84+njbg/GmkmQiy4bgz4MvNV6sa5jZCBPCSnrn6vSi4f/9eMWRwtdmJWDqnm+ZBBN7+r2L9izQUb9N5s1vaBZu8R2S19m9ZR8BCiypmkNYIuD8a99skscAS5kmlcX0pU5ZACQNtQGnzLaOFvk9iXMy71cGIAUnSZgH+nEL89u0DhlNrPJohqAmriufoYiH2Oh5ZhtzTqlOFerBpT0z/g5oHGdS2fmCgPc253BLo/3CymsbrTGdIBn4wZz/M8tESkYVVH5yc3ufFw/eEycvhosT340qeMr7OqtlaQBAvNf0U/bNuVuomL1r57fLbhjXFlxl14zmpCQNQmcXVD/U4+TMmZ8b3UZBHCrNzQAWgzmLc3IZFssAVEJ4k2KKy/ucoX4q64CdMwaKsOGzbl5hgz/LiwEO2/lWAu8j0/9lcZM//nSzRUGU0uuL+04HWaYpVXTFraNRFlTLBilyRaI5OYuOHXpTR7e513JEhz22iQ6MX9rEY/EfeLxJaPmqITuL9aOTqyOzdawyiNoV4MsvS58y2J0KihiW8XPMGR5iSlX81KLIafkcZ2Uf36jLX3UYjxuaX36YCWCO7UQD//Lb5vgJ1y0c6kyHwosqZXh3ZMlXcqXrJJK+KKKqiLgUeGner8JwwGzIHY9RIKYMRLo7eEG/S5Ga0272BO3ZwhxVwabtAILGKMn1N+2hdRgyh9vSghFseAar7VWH2PSPLtk+r1c8L+++DP5p5BX4gmZvcOFz/H8dfHHprehP8akDKKfkIKOflPxsos0kqajokDjyxmMGE5rLgo1qMz31C6wQADvCsHbRlU4znq3YH9VFFqhWiBMyN1Vx0dAp7jHezOUixzcqBGc5aAVQmmTCeWfqk1AiJOgcF+lHz1bqKr1B09PhPzUW1UvZXILGpU9A6yjDxtdua9O/0nqL2amC4nC0rNA8SCyh6DU+BfJ6wqsmhJcQAIs0PGaUg/USvegb2/KLiCCYsULV1wey0cgOStsmH6XkzQyzSSp47DdxEzVos68zcBUqOxbBzhR8wL4us7n+qJY08fD4JcRc1nNSlgioTRiOxAcTdkmBNQ7T0BGd3uF9w3iRE9Z475kFXFQ/40eR8rf32WZQFMCvV9PWGrY9pjFkiOtM6fFZV9n8DeOB1acMhJk18MlgvMQVGxel3lmgMVHAw1Dvg8XihswNBJ8AsMFKnoHW8no+9ogUWm2nKvLrY481cXMs7lFO0aG0p0tfi1yTPyN0wSQSzhQRjQ1c2jvy+mKUH+57p4cepdIAA/vmJwAAAM7bkpGIByH1BK+ERV2RUllsLo6x5TqpFpP8GIh2n4uyPRrpoF5EcPfkWwkHmbHdU1PfIj+LVoNslsOIfn1g5VZDh7NugaVb5+awLukfPqQfchUSr6UpJ3jelw8eL+auRciZcx9AsMRnstVc45HvHtnIeaef1N8UDJz7IAwwee1yIKsEOlOiKyOQhA0R0IzUeEX4vEzqatEOX17A+yielOzxTrehzFwKIkVBEnMRqsnBLzzcVyGlE1fV1hoFc50KG8FHlOr3//D8kR7RflqAAAABjlYQaw/+wrseI5DKA1NZCPWvbFxklJNKada6+hIHRnsHax5DhU3xxSoXGMGVF0AmSKEcQoYDkRLrCeSB0Z4OzIHdG7bh8ldb4+oqDdkoQH3mfI77iPvB4fHxxZQOPS2aMxfBpwzFf0z0umF0GDHzeCBamLmcyq2qQZtwekXNua7wJ/Bd+Fy9bkJYJymsxVNx1h7hvPxwZvcG7AXRYQYYA2TcH6qmC3PJ6KkoSPeQ2QR3wAK9x1P/FXuJsfVSWipz01SwAAAH2smIH0pDXp8errzIern6I1UNDaLoin0G1nCTeU9xKNJO00rJ0b5KfOTEBmYKubOF2frgR2Up4oczsZ/yczjluVgpSFPpI/YSuoOLJyME8QnfI8yT54OsFgUGw9/PWd6Ny0QfJJf7cYslhwCBjVwuMmgkqFNxk4sDNPeAEOD+hjN+CgmM6SFl2qbW5a/OmHjnRHgXey4nJ+Jav1bAfn9HM13KJF2r1z2Kz7CY9Q5/Rd4jnhbLWcPrIqjuUI5h7X/CCy8rCdZzWAyYRg5P2QAAACCMVB4Pr03MA9E1eEi2DEEsWG2uzVqCnZnmjGk7mCnyAVe7vGO/ROXO9HlXAV2w3SgRKCt8mrs2bI5OOCQCMOPxbQemPqB2d2AxsZAzr280WK3weT53ctOWUf3xsAnlAu7KYM64gXEsyNOfx87mMYcp9RMqth/ODGkeO5oywtHMOIk9Qb0zkiXa8s+XZxHPfkGL75tuNn6kWlT8UUJE3RzSw2U8k4B+JFCf4TIKGyDCZ7pY6F2VGY3u9NlCGkFENB16QskIvSAAACeezI62i2OmPnoinSZTW9JfDPqUq0bhVliY2IBqtXO5k4uh1o8PekqPTKLMlQA7NdRqBblSM7N/Q0fyKoLayjSkdBWvTTWMnObNzCxlTxjMcwPwMdcWtzEw64aq5tgeM72b9j1vubPajWtEbTur272ulRPfeP4VPqdWPaMvUH3rSugxuoMMlJidIZtSYWZ8ABzgFiHeimLuLl3X5WWFm7FDpwey3bAt5M3jat6tceC+OOWkpkzMs4w4BdAYplxiNkZCkfMFykEMjSAAACxJZ1vkWDURHVAz2Rm2V1EbzK/q47G8PiF40XlQpxBQsNgx3jpZXa46gmQmwpRKEdBKNmu3L+yX+OtWwfHL1IEha8OYS7/WEdXJtXU6gkaCAPscVw0cEDsGOX2MG/yKppdUJI+xMx8BhgHek5ctIqwT3yNdb5gZcoxkWJOeLo/9hfE0WzODtzIyP5wOrR2SlVuicO6dPVoBD5E2wCz4DaSMfFtCHlzb4+IitZe6BZZdUBTG2b3lIHss/Blrb8AOzuq4onJNZSVGJEWVrcPhGg+afxXASZ9nhIqz3ICG32UAlGdgXnN7TAmkYL51BgR97fP15S0R6ic2zYIAAAM+A9MlDCtKyI0YeTe+NXDTHvWYe18xjpj3mf5lwBKCBIfv5yERvtoJwkfyomF1gsJmsa4BfioKTqLzEd3u/yO9zRo60KBDEfuUdJOYT6MhjQ8pFkh8ug1DeCDGl8Nb31BOCQ26Ssf91VDuvjHSXwc8w/ZH+keM9W1BvZyALYynBCFXxeNymf+zRR1jpMgfAgZjQU0+AuYlaXSU8icRR9CVUJCdrFnbYW3eiuoQwotOhu1Mwh1aPfT7TYci/Fsuj7cXTMOHbsFEQchrOiAQ8YAOMOEfdEx+7PcPGHQbAJEAoML8ifE2cE+cjgM87HAAAyFlz5JYurpvFNphoEoLnntMjCYrVvigw7DQuubjCfVgGQ8gvbZfMFYBMpJoU6qpKsuR2g5QljwfUebw3CCVcDyKO/o+V2QmdL2S600UMr3tLDMuFtCdUJBSXrDS7/DM1MdXVxu6JK2FSdHfE6ki+xbL1My5G7Cqtte/x4loSZKRosY3JZ0zn3LRQXcQ6XvRTPOp1GDG5V+YdE6jkYJukB3tTt0AWgOmB/RaeVZoargoanQ78PdcRpFaX6G6dtdrxbT7kzE+6qtXM+3G3Sbb4C4O8cUnXFJsUL0lC4gJFvZRaEx/BOYlBceZr1oc/KShT5ji0At4GXi8xNQj7l+8XLuI86A7CX9ASXEqCZ2CEjCpA3JwtwAAACOVYQ77rvzwYmFl63/UMTWSpSPm/h+kjDCHEzR+3r6SWCgXqrqqdBNesKQo4a/f5+IfzVMR6nTiTv3racTQov/5h7+8jm9TwWGCm+6CtceMjdaN0CQ9E1prFuwh8d1OLS+50wodSK0qcXWVpx4q1oIeimRduWJAMIo1/JbJnftEy5eLHyznOIulbf+xHXKVyIygZKYmmWr78utX9tslRS2lPQX8nB2sbIrEgsMzlXXzWqSH7TgE7lGyqFFcg5ttMKIlQg9kCLPgCaupN3qyQ305WpZhMgtVmICvbMgev0bdl4hjDkEvxSXP+R1SxUP4ZF5QKXmMgnQmiatAUzAM/7rmhWjI3Od/QgL6Rra+elmGn0ukOOPbDB8DiBfYAARdItjmLWfNa4ozy5v9hyGEoMm8hsTkAlu/BU+ZwBtJxuNKOmTA/BdZLIrJws9G3O2C6VXZ2K1P85wIC2cqI2jz+qrrIjdnfeiTDk5O4SXsp5Z7vTe9RuPsMFv+dheT8wIgnJxf1aLuhtZCNBpUxboSpgFWG8Wx/O+ajYegUw+QdFnYhPytHXiL5/fBDano4nNYwRzOpZ6c4F/KPRpUXhzKatAOD+gsf0ekRb/EBdnarRC7anmQlt4RlsOXQ2OYYPIQr1NZslg+7EAr5JpXwzEJe38c90UVnVruche0tGOzkzRwXIOZxTEFENO32JXPF8/bMZNA/KAuzNBQiZDTF3QRbjc0c4EOZ4G7X7LbeZTGseH9ZapcfGF6U+0vw8izJs69um8Dwn/RJdD96nAdFDfSw9dt3pKALeM+Uoqc4FZD9emBBs9uAtB4P7z2LZlGYIGxRZBJtqodA5VeKxKEcK1XBVzwhKu/1hsD66OGaaqngl1suqxgjrLE35ETEBBkSLJ+nRFlTtRFmqMMhjwx/GifBMiK47yah3B3/xE0DHVtcK4ZNxGErLTOqVL5KRnKh69R4OPw7wfy7s3BMN6U0zyw+M+USJPi4IU9/96OSyAJSYCUIEIanYiztJaJKWF0nK9rsOmS3nPpOaBHVtPg80IMGB75CbAlc6AWQmnVl+J0W3OlraXCIgRBmm2JTt8J1rLpIfGpEu+JUVyppEN4Pyl+Y6xITwh7tA+D5Z0uApgwwyo7WFy9RWuRoYY2DLB09aDzI1CEmJMQsFjvabXDTMofpz2rb8xBB+aOw9RMwsa0EYYljdqJyE8UZAUDshoS9M93WyEr//f3jOC9w8fUhONQAAAWJKqo8zUqMPVh+K+92Qx2bCGT97XxCyDWmAtpWwMSy8R/qEXgAI+NDxcaekBhMIqgIJ8U14P0NH8ioHYYtvzyUAyrcELnz4WARPV/uFzA+61q4sU7J23UK10j/4uYtjbV80zKCw3qL5CDf8tbEYg2KmFEwkGnWjFdDhQhYMRI26AZHeLl0Z3ibs4ycDp3W9fYRN5+1p0WA6xWFnO/lUL8/PEIjNXHrSpwfgdXzXAWzA9YZAgjR9brngZdNjOOIfhY0No3f2jSy03mft9UzQ6FM1Vurs30X0jafS7fvO7vwSGvJb9niSyDe48raw6z1WxgoAxaUuDaZ6X2qFhjNCOgEFVoa3gXK2BWr6ur7pgNFggabwsWTiEiPpnM9naZ0QpeoZpW3VaH59Nj28dKL3YHhZ/zgyAmkTFzDXdzbIV78mRj7ljNGffR04StCVXhqzViwyxC4j/71nRerf63lSW28VX62eieeP0WqLkl+nz9wBZx+2knlmkNP3TWDPm6em7dhxdlDdG6kaWr/WFEwEfNbUZx6FFnGQ76Oe/16N2OgNDCkzuNpJi7/earz25fi99XvIF5e+IAy8xXTp9KqianeI3atwbBlXZimns7e0PnaezMMMZoOLL+TGdYFFctQufGZXVUzZMoMKSU9zxP2Y8L8vjOs7iamGT9yoDVA2ThATVy3GXaZePkPKfwO/3qMqSsmXXrrltL0eudiWQh0OFB4JDMe+OhGPgNdksTGj0q79BCWlhRvjCbfCCTrExYzd2jNf+iQJnfqDHCc2aC3fXkS8nbZFLXl5E2OODTxIAab2SbAsk6geSZ/qQk0gDZYbPPC6As01H3d8RqfY6P6srLi/kc5wtvQmHJQly514syf/A77Rzn61iUbUa05xpZA0STRgIDntEZyvfCUzOqZZzkNa73KZfQrbkI758RxWIy90rdRtsSG9TA/B6CyQhEQVg9gABfzj6vnpgncqgx3VjPTiMSPwyO6uZlu7RLuPDwd4szFHcjWvkfKDnWTTbg7MR4nMdoF0cwir9eaCRx6hk6vspODj5VI2gbCUVr0zbXdz5IWMmMTt+qKNa7rhlL6VgU/Llp4Bpb4kfMIqLEde4ywngqcFYLEp8o0sRxh0D0wshquAS8EbTXskf2yNanagRGz2UNTvmDhE6cuKgCPLS9j1YZGA0xrMrUIOr65TzSTY28779hkHZRWdn/RF1hBMl5cU9OA5jlj1Je8PY4o35du04SwzpRdwli14RD2IodB9cVJzuW7S5a2BDPF2Qmw3mBjz5ma8CXLbIQk/79WmX0O7lcFQNKbvDPyqibaD3Q4o/GX71CDo1u1erDJmypUPkweifyNuVm5DqwmeZyBPU84cByb+pZU6EIXZ0kZJfbcj3VgG01B9fbZbESVQNSH7Ue6edJXPpdNfl+cy4HnnYHLQkaXCJgzJPuLWMWtwpHjttVFKRM1LSZ2trV5ioRo0CQOylFMqeH6th+3UKExoxzf9bz9xHkLk0GnVOeQolxJAIT+XWandMbD7kbTqw2aX8a7A8XLeJXjPcrGz/BE7d3Uf6DQaPizU+mjvdhkei1+eVZM4E40JT9qbDFPiyGStW8GeDWg3VND484r12lqke6zfwwYzGohfU5WUJGGMLrRrEKrW0G+k63GfG0id+91HsOgFBeEbrH08rrRghM/nCuoN4E3beJUGwP+E/qkWnEI1o9CYjjfYUF6ELs4aGNCi3Bk7Mmbasfc+sFIhipFKH5tt8zqdb9UvEz/6Bj0JuvIliDih4uhJgODf8Lh1S4kbA/0D0Zns4RIeivBSSs+ebD0NrSgz2StNUV/2ANAJmZzsW0jJI6ONuJmepm0mBlC2Z3OkeTnHJ7F11BEF0QCP6ABDozXlUz12BguX1B9hOP0kqdPXtDw52HkwFjwuB8Nc838clR0ZtMqxOwfW2k8n/OSf5CRy8vsx7QAIH1A/MSr1up0Mkm/AmCh7EOb5W3G96bZHAMv1saA+OjEmZh/7dnInyMPicoyf2T4F1jAV7hAAAB/jiJV8u4+tSqfuETC0VpXkWHClpX3F6E48tzIBag1zxI31pv7sWuh5vp9zHMNffbxlxGHQbGGo+AoPzMx9BGFlsyZJVCIW4ortwFImXwfujBpdDZfCWC9XRrt67QXnHn3/xnM7Oz/Wu0zBaB6tNNLZR9FFxyUPCMzNhOwqZMOuDBFJ/kPUGCsh5WgsioG7wWZMzX34fAog25pJLkqQ4A5IZvNRaGdYLZcL6+uXyqO1xf6HG4PGPnVTX6OLLfMommmlJhGqFhIEk8s9l5Zg34nsD9frWZJ5FIMhaBHiabwBswOtrS2+L2x1rXrMH4r0S/DVchgyqEy3+o3nOnoVdc9R1aO8YceVTGGeWs7wE5OCQ1vk3tmGXjab8rrtNd2z6bU1MVWW4m72g8YpZPsfMvBVqtUyIyE3zmDgeS/5f92/rrcBYqL3qi/W6jvjQztasUdH6kA5zM/zSf5MfsN+CkvFwvOzHho0bNAu1Tkgsa1ocZQsmVWoXhMv2WxpoylsVG3d3kPd2W5nagHZD8h5m7vMY0xr+QJS3Im+gFytKrCMtmGB3pUS9HdqymKFSmJEpfkryFsOnU2573x7i4ZKVqvk2zixpO1wACNJ1wVKaksg6b8LjBPSQ3RzFV5pPQecPuyQV39/hfwvGivPAD9KrQQqY3b00qyPYp9+7capkT/Gbl5m/yEpOnTpl7lVdL8xOhtQ0VRhdLcNAYxvQgKJNhoxrqBmgdNoD/HUry3SulnSml9L8izped33NrnhNoI50fGewHH9+y3IQBRKO05zktwvLQx4J43UtJnfSK4Pu0gQUNGZ1r4L1srwQZy1aizUzjNVxJ6hJY5koYu8votrBU9kN8EjSir1icfoQaSjRI0a/DyoCkbchRFWytidzlABiqhAJYpoQlKVYEq+Nf4AzlZs5gOzF4YjLXxI2WlBtBm18UsXVEYo0wVuwaDUS8Owo82ejcAo0wVxlhVqjYOP+1AAU9o1LBYYYEQl8xUcMsl30f3AeGSy7ezLvkKr0CJC6uqQQ9ZjaUddSC5Mo8RYCncLZDGO5Rhvhx6gZd1f+Mne8i7jEvF8Zj9J8GTO0A+WWmWCHHUVYTxumRUN8wl/toQ5xMIeAnbQSZXE7kfGJWrBwmcbFMKDwm7oZRw/q10B6ZtwzloztSuQQdzLW9/8Ws+GqxHwstyV8fTfCQ9N6Md2fYsEafF4TKy1BO7ArISrtvF6VqwUKKnOMW2TpZAkGHhXI6bB7SjPjfKTgSDWtaKXaZzzFDHu3j/yVr9xwfd/CltptUxWHe+e1PogOk+EunvEhkQJqgZdoMYvZ7bdrOEueeVFW4upeqlOZwS152/aIXO/MyIRHR/phPOnrkCZvFBcXCuRSw41QbUrAC2wf0RgzKAUhfsbztANm9UqOYmP3hwKZsURUXN2xYf0KxmlJVb/pKrWptTiT+DHQuXOanPcCm9zi43xWm7wLiu2N92vAAAgxK3zeCbBhHZ8NlLaNWzS1L00tIWTAudQK5aVvBV9wsJqFwj+0ky4nRh3FfsF/uhIjeVpPtOdEa1Dr9TpHYdz6OHdEyITs9uel8RaVHX79jl5QR/lr+v1Dqj7/bwPRjYgUtMBFL3Coqd8w6wWXMKCz7FKuV2locy8txalfB5plW7x16ntCXfhFGHgAqDSlnQFI90p0y0S0CkGtUaqJCTZy//vZWXokMZTgK/g1keltf9dA2vMvUvl3qwDFox8Y/g4TQLE/XIaRtz7SvWhWtG5gLC8ncVRFuOYoxN1CtxJsdEHyHHSrU/v1zPtyELSk5jwJOMbYC9zNE3/x0cQuveVkF0E6uGNjFE4VgN7Kmx00f8xEL3f1iz7RZg2dFwVvLM4sgzzBi8m49lier+QT2k8oq1+1hkLwcZIMvPzAazja/l5/gGKjFkWGBSKIRpzbFnqr6bcD7zz/PsrMNESmEQxR45rz6LBHaScve2eRPFxu8daeR+//3Ay3BrH1888c1kCh+F2EodJhgm33zIapm5AEDe88GgOGRFyQ8PRwCStxaztO0xckS1CQA3nsxV/a/f9c3LmMAwdADqySeXuDPzuz7GntmjnPx/DnpFCIxScno6Qn7teCTbZso0cNAIuKQNYXZyTtBtmtJNVSGGT7VWLUORFgvAYwWju6eAhjm/SoGRsPp6vLXe7J317x10D9OfGXm1OIocOCciZziA+d7i1ayLkBEa4R1UEB0KwyhWNnqFVLMWucsytjrgk/smbg9w/Alyj/zRghMh4pe6iUimTiiRAypGIVvwQ+rqLe09X0FIrxuetWmzi15e2CcyaEisMO+dNRxUI1vYUllIpvUXku3XH+DhZJZ4OIFUQGHPv6ustz1b2fv2PJsXfZpN6X6bMbPzrKJDbutWWULDlWndQ09JUvK/UA/q1ZfTvVitjlHc4Zds5vmLbZtJq2MlEats1ipUNCSbGyRdh26eumt0tte+8Ly2poWKzqoQrBtQBHayGnObbMoo5PKBPHFnQG/CcvGnsciElVyXqKnXl4vmSsjxGVdM7+MuHk7lGyM1M0dq3fWmff3wrYKRmVrGpw5AfKvcdMtvxsfUZlrW8YhNUErs632bGBQkW/hOQAJ295sB/tZ/8sIll3kNIvPeeJ1hJCMcVaV+rRd0vLffyrg9GeL0IPVqiG/JttJlzdFojgIvezw+C3DUS5mNJSxXRyvZgMLx5c/UJGAqWZ21sZ/4+a1NeWgXDTPKJR7hzdMs7iNYuubP04K0S1UkSDW9TXR46TvkZCrLyhjS3HoDUidtZCMDWLMcfiiKLOKYW0LMSCG/xuVce1Pu64WZTSJKWZI0znatneV2vZivY9B42qI78uzN8qbqCsgQuD0nyZyYRzTqnVj32fPfOXtPWsQqSz5Eg+OLNJA55AN+44AoQ+l7t1YvyYG3dew+fUr0+BnFXl/7NU4t6hCRnUJJiS+Y7IqQySpmDaOqdWJGes5i8cjnrF5lIxUwlTp930hNa+qYlpOv+74z5Mlk+0cscDv+oMEa3+PbtH+ELXe0cOXo3F8v8KsHvAdeU/ZQGJoE/peJ2Gzbz/KKunXKAwNrxutu6wuO0KYCNysOYAcbfd265iQgTOz/O5TMjFHczaFtb/snY9Qi7RDODDlnBI4rL7cE2+MRfpg7X/U9h7e5fvXupCYTODEVxwSLIpFCFYtGa2VIo+ryLJ60HAYeR36pQM/OKSk93RYrCSVwvBWOnozFS+gjVC7AQxEVba/JK9PQYibyFTsyk/ljJ3SN9115dz6RfYc1J6ijofAzLZwcSuww8/MibyIQ4CUZjUq3K0RTMpewY2InxFbxuQVxFAAQ8/N9fhNqQJvH2WSqhku7sRAObQ0IWSb/DdqeaYZ8GYGN+o8otJkx+qk6ZiRCaT87warMkler6coAk1r7E0p8oyD5CoXG8zLu1R8CAUZhUohV4br2OupL7uQGrn26Uhkjbup8pmcBAL08SOb+Lf3xbBEOpIFzjFSBQGjNUpdq8J4VoDhEgSwJmd5IXG7Ekd40pQsolgFdbQM93GtjdYwV1oDi2CswzD6P2j9Xq1ickKl/e4G/aKc1LadD9xqH3mAxI7OklhMiqXATyPQvEi6gKR1bZlqioH8J/xxG6D4TzQo6/Aaqgb7RyXi+UYjzWFtVgM2FnWM9IBdlUZPUOv2zH449wmsXtGux9x+p07GM6mcYb8Bf6u41fh7Gc+ZirbuqIzsEjTLJvEMPpbor2xbU6bZDWXWTVRhpHbioaqDUi531ouW9+9IssCdckQ6GnPCPH5pYk0w7SUqvVOQqe5SJ9qVdBbxwNMkzr6RhQiXD6G0TbPpXC96zxHuhnYsIPabcchyNHVwCr2Jwnm+6UxdqJSkJCofK8veg725w5r0xBXu+GN225eF3vdufscvHpD0v4CDYekD1kzBHL2X/zEfgUcR+38igLNL7RHIutgub9o2OJ7zXTnMAXQsA+0RnHMi56gT5QlYi1D5pOyXMqolFmf6D8Xofh1/5q6J0R+7nhJhcwL+8U044X/x0Qz9zxC05GTTvYIRerYIT5SUEA59h6uS1PHNyiWpQW/3gis/U46kpjJ3dkPZ0iaTgBZOoEFwk0DRxIr3rO61cYd36TlgmEmXpwG5FO2fyYYZsQSuaxa2E8Q5HUiOViw6h8wsAF1JaTEt5Ja6V7GZbXHC4MmasnAkrW5PbUUkSWdgR8+cqjsbxkjoNKkCt8z5GOCXqNmA91JaILVbf0NKtRYWXXnfj/bQ/WGn5e/IANzq+MfgjjBatUtPcIxsyFPsVxh8m3cARaivfkz++SK6I1V1V0emIIwqAv2LfIxudx5JK8/DtXCs8ML1db0jHlnYgSTU8F7/D8K3Y/VrT38MbPUT0fKtopw8nJIYJ4yoG8Xf1XZS9jrGV7pkjr3MozPn5WGNYAAERB00uM0h/ZD8mClJWgBwTThtn8+T9Ot69gDO5q6bWU4Ng8i8UdSHRzaELmBrAgPzbTxIREtrbsAI0SvJr297Lqw92LIbQslWopDV0DmBxstXD19/kZGks5twKZi8ZW96TmKZEQLrOLrQas2PStISYZ++eqjzeWxVrAk+7zO+7/t9/xnwQoLcB0uPeAOIa72Chu9l1aVEVuhzJNTv9J/Ep6LoaO+uj/5HK5U+8SAmOirVyuGMZOfxpqWxx+z4tWIPZgAK2xCgIYani15zxTAe7bSCtGls+1VzOJXHKAPfRWl1qPgnAsEAZkd/Eimh7Qx2chgUT5+PKHxsPzkOdwKPilvNHsw+P4syeE90PpYwjlRnIvIqktk6HBoszUA1/8gN6d+8aEZwZM/dhiHFLyOTSXwB/LxyYxIgYcyd1QsmpbG2XWjrB3I9UjCPtYbbSgpbzwt4r9y/OwNsfxDWh39j6fAQtgRpIheh2A7PxgzK7zQn/tHjrO5Fhm4NtFre+UxXK3yQbonJBvhAN0XkjAgmotvoz2YqPbooicEk5CffpErYOLQYs4y7AOXS+LGCYWRRbRc9D7HSlvA24bV0mbpmp8fyw92nARbszVPpPvKenabTY0tWb3xYQ/9KokW9ob7KtI1lbnsVBg6MINCrfrYu3KfKvVbUlE4ufbWj5HCmAbU+JWEqlpkt9/lU3FdJXdzeGdmcE7WIqOHd61DjcBZQG4ACQsGcUU7YaWtaRK+5quLQUPM+4fBFNLGPRE1q5DRqF9m6nofMTYIKJMnhkBzE7rlIHlQLrQQCjO/W5mnwRAByuaMDR6sxXdwAYh/2oChf/0FNZmlcD+gVOwYn+Z70+aRPORGtW16ZPenAk5yzGdC1VolzrFwOCwFM1cC+6Ace2HeLkm0CDgkOdyp/yE7Q2UvDubm7vHNNZA7bGmMJxKb3nUqDOCfsz4JjGl5aHXBIfX6zCGeVPLYUorq79/g0KW6sUbWgWQYBqBBdWJhkV7SID/2i8lXH3z87s6B48uS3ZeG/7Qk8hvMHUl7hN7XSqgYXW8MX7Q10BjDW7UgAmSU6yxAYFnZ8QRkey9zEpWfvPON0+TSsGfIjc60dSIE50wwozFluLJM/vDVSLlsucr/Ef4+JwzlYsemHhutnF6VOZIq4zyz9afxIr8/XqyuwuryF5sxigwwXJdbizgI4qsOVac8pxJ4PDM14xP0PEaF/XdU5htEVYScg/fukOogHUBzi96rWaKJPLWKM59RRH5tnYze55OoCusjjDyPfiDpSQm2nZl+f6Z/l90fKmbb/pft4NodieU509qXq4enSkqprqS9HZCJkoeHHATrH+YFjUNZfn16ySqdIidN6VWfEECQ7Bc/9P/kXrxb++3q4EOW9kN7rkubZ9OtT6NvSLCHciX80TkfVP2BEsF40uqeY+znGgYH8CXWWlzPcoqYCZkqsfE61FklKY804SiMKdUaxOVTsZDqPyFXd3XAZzV9GMR4qvWtO8tPkX84+xbVb6FUUKn40ppPNRvPm13wSn6wJkbXbNzMFgA7b3n4rUTyZporvhwFsi7E4iuHj/pU8+biz9WiIw40r6TxRciB/breerXF8GaSjlDjIVnA+Rzs2O+VmRDOJtbv6FFXFrvsEBY8PwFWDn7LUVeKmEkelEJgNocpl9WqkPuQnO/YCxJilMJcmpOYG+HQ8AUK25ieLbDhRYn6LOqSAmaRx9RrZm7Um7MwLR0/OGityomeODJPIFbEvQ54G6ADzamUbCrDevg+XBqO8E97lTBRHaZ+aX06kq8bjZNtz3ca/Ekf6Q+ROS/SwiGh/uOcbJ/YHwbUxHNPfgY5nuaUcFtYserxKXn7QNZIqTywn7czZiGQ8X5LoVDauKaJagsKS943+4BrtKfEDKwKLN1OkIK5wy18iRqjAiEJlTJRqO3YR5tS2tMTEAlJ4jAJ/Zwm0oUov0Ji1LnccltXGIN6CLH8DHLyPVo8xP1gLhf4jUzaKIsFySLA+eyjUkGWoR5rVwnm2D+enfoORA3Uq1pfGVOAwIfpHCdYaDA7MV5M+llTnsOMzrcvNm9nYMkG063OtUJS1lSEI5bWAsvjUwki/d3a3rW599teEjEKwojAOunUYD7Rbh8IOd4DEyJxsXfxOEFeX18cUp7M8lWJuCBlET6zi8JhDgBGPEwWZoUz28mgWjYgIvS6iPjK75+0obycC8kD7ppcvjXlmFDg5EqcMc6chBYDsjNxFa4pzqOXinu3x6pVUtLvhlry/sj/IlXfSeZXr7W5Xl+CnTh0p4W6H2/DHpJBXOQ1USgGCX5PSk/l13dvDj+WySm14a73QEo+zZhNP7yhRU34q0lptOcp2ZOvmdmKqW8Fqqps8JlmrySI8/iAszs6413v/hAWeEhTsWTD1fj6Lutmm5gYxzBlDTHMgc68Fr37V85rbqfdloT1IjsT8at7jdFAbEx6bCFIY94nLFvDKuQjtz3lK//7utSZYAAALrjpvTTjgmcK0fCZIo+9o9xBo1KCp8D2zCTN3mNTUMIYBauAJUVc5aflzeoYZ0p5KPhCuqnMdCia/csOFddO2BgJNdNosUhtbdFQ7F1yOTiIv7hXyF2LwzVJyPtm5H7rTP9FuubqMjdIZ27E0RstsiKLtXwv5+J5LwFm+4Zed5g010FK8EAr3U25DVXRpUvJZxH8wfzrUJTTqaCeWdziEgL3LRvuFOiVDh86a6f3O6ot7dINp4FnMmG6UklqEGEjvc88wa4kQyIauhOSfmzf3xNhhInchRmwYuMHwmlb5mLNNljKtdVDAQAb8zw+7AphxJmL7UPacOsjgc6u/bHACdbK96JYnFRAEc4l+NNZWFaJOmXuy+tAFb2H27QrJ+wHAAAuglJ9f3153GzdFxp430Tz0wIRm1338tlJAdbMOkt9Cv87rXGAJhQtyf24HcKfaHdaX8xEzPGEgmpsVAQ0wJBzVbmI8U/Dl8YogVmbd9xxC6l5VLMDwWtuBy1118XN6M1IoLVlIqj/tB1VrCDOt6/7hriWIgN4QWPMlUmKCOenENwJhGY2WPG8aovupL7J5qy3QaEiw80K/qZQbjTjH1oR1yAOdxPJrHYWf78q81BHL/bBZ9tNJRv0aDyfdNh6ERbwCqR8ajA00ar412VFXXnE71pU2PBZ/7oEtSjevzndg0QHIXhc92PUsd13DA0w/CMn4hf/8xE2TiuD4uyvClKOuWKkQhh7yoFtc1ZM7igDyKErZSI+HJ8GSsjJ4MrgEoDbHIPuAzNUlLThbNcTyAd9GfDm3wEjAENSC6zqhmpw+C+WvVNmLf9ywnrQcUE2TdJg17ATFYDcO7etU+cwvJCn0PsS80pmmseIkumcVP9opinE5Dk9IFHc4u05pI5NHwMgGsMqebn/FOhVNR/6VaPrDl9wm/taEIskR8wNNltj+ecme3fTh+m0sBwfsXpiMUumkUABbFj4dlQArb8k8v3be/FtPCRYKBxffkVOi2i9EkocYdLDvEWRDf3rOYqlv8uYEY84s5ESmeOQIgxcV4rd4PVgAA'; + +export default WEB_CONTAINER_SPRITE_DATA_URI; diff --git a/docs/src/components/icons/AndroidLogo.tsx b/frontend/packages/components/src/icons/logos/vendor/AndroidLogo.tsx similarity index 91% rename from docs/src/components/icons/AndroidLogo.tsx rename to frontend/packages/components/src/icons/logos/vendor/AndroidLogo.tsx index ae74c24d35..e1581e14de 100644 --- a/docs/src/components/icons/AndroidLogo.tsx +++ b/frontend/packages/components/src/icons/logos/vendor/AndroidLogo.tsx @@ -1,8 +1,14 @@ // Copyright 2024 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 +import type {JSX} from 'react'; -export default function AndroidLogo({size = 64}: {size?: number}) { +export interface AndroidLogoProps { + size?: number; +} + +/** Android's full-color bugdroid mark, used to identify Android quickstart guides. */ +export default function AndroidLogo({size = 64}: AndroidLogoProps): JSX.Element { return ( diff --git a/frontend/packages/components/src/icons/logos/vendor/AppleIcon.tsx b/frontend/packages/components/src/icons/logos/vendor/AppleIcon.tsx new file mode 100644 index 0000000000..b77406bb2f --- /dev/null +++ b/frontend/packages/components/src/icons/logos/vendor/AppleIcon.tsx @@ -0,0 +1,20 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import type {JSX} from 'react'; + +export interface AppleIconProps { + size?: number; +} + +/** Apple's wordmark, used to identify iOS quickstart guides. */ +export default function AppleIcon({size = 20}: AppleIconProps): JSX.Element { + return ( + + + + ); +} diff --git a/frontend/packages/components/src/icons/logos/vendor/ExpressIcon.tsx b/frontend/packages/components/src/icons/logos/vendor/ExpressIcon.tsx new file mode 100644 index 0000000000..35cb276bd5 --- /dev/null +++ b/frontend/packages/components/src/icons/logos/vendor/ExpressIcon.tsx @@ -0,0 +1,26 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import type {JSX} from 'react'; + +export interface ExpressIconProps { + size?: number; +} + +/** Express.js's wordmark, used to identify Express quickstart guides. */ +export default function ExpressIcon({size = 20}: ExpressIconProps): JSX.Element { + return ( + + + + + + + + + + + + + ); +} diff --git a/docs/src/components/icons/FlutterLogo.tsx b/frontend/packages/components/src/icons/logos/vendor/FlutterLogo.tsx similarity index 80% rename from docs/src/components/icons/FlutterLogo.tsx rename to frontend/packages/components/src/icons/logos/vendor/FlutterLogo.tsx index d0d48b2825..6b8e3ff47f 100644 --- a/docs/src/components/icons/FlutterLogo.tsx +++ b/frontend/packages/components/src/icons/logos/vendor/FlutterLogo.tsx @@ -1,8 +1,14 @@ // Copyright 2026 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 +import type {JSX} from 'react'; -export default function FlutterLogo({size = 64}: {size?: number}) { +export interface FlutterLogoProps { + size?: number; +} + +/** Flutter's full-color twin-peaks mark, used to identify Flutter quickstart guides. */ +export default function FlutterLogo({size = 64}: FlutterLogoProps): JSX.Element { return ( diff --git a/frontend/packages/components/src/icons/logos/vendor/JavaScriptIcon.tsx b/frontend/packages/components/src/icons/logos/vendor/JavaScriptIcon.tsx new file mode 100644 index 0000000000..b12ab27b6e --- /dev/null +++ b/frontend/packages/components/src/icons/logos/vendor/JavaScriptIcon.tsx @@ -0,0 +1,25 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import type {JSX} from 'react'; + +export interface JavaScriptIconProps { + size?: number; +} + +/** The JavaScript logo, used to identify vanilla JavaScript quickstart guides. */ +export default function JavaScriptIcon({size = 20}: JavaScriptIconProps): JSX.Element { + return ( + + + + + ); +} diff --git a/frontend/packages/components/src/icons/logos/vendor/NextjsIcon.tsx b/frontend/packages/components/src/icons/logos/vendor/NextjsIcon.tsx new file mode 100644 index 0000000000..bdc3341b16 --- /dev/null +++ b/frontend/packages/components/src/icons/logos/vendor/NextjsIcon.tsx @@ -0,0 +1,17 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import type {JSX} from 'react'; + +export interface NextjsIconProps { + size?: number; +} + +/** Next.js's wordmark, used to identify Next.js quickstart guides. */ +export default function NextjsIcon({size = 20}: NextjsIconProps): JSX.Element { + return ( + + + + ); +} diff --git a/frontend/packages/components/src/icons/logos/vendor/NodeIcon.tsx b/frontend/packages/components/src/icons/logos/vendor/NodeIcon.tsx new file mode 100644 index 0000000000..bd55297651 --- /dev/null +++ b/frontend/packages/components/src/icons/logos/vendor/NodeIcon.tsx @@ -0,0 +1,53 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import type {JSX} from 'react'; + +export interface NodeIconProps { + size?: number; +} + +/** Node.js's hexagon logo, used to identify Node.js quickstart guides. */ +export default function NodeIcon({size = 20}: NodeIconProps): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/packages/components/src/icons/logos/vendor/NuxtIcon.tsx b/frontend/packages/components/src/icons/logos/vendor/NuxtIcon.tsx new file mode 100644 index 0000000000..79e63113e4 --- /dev/null +++ b/frontend/packages/components/src/icons/logos/vendor/NuxtIcon.tsx @@ -0,0 +1,20 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import type {JSX} from 'react'; + +export interface NuxtIconProps { + size?: number; +} + +/** Nuxt's wordmark, used to identify Nuxt quickstart guides. */ +export default function NuxtIcon({size = 20}: NuxtIconProps): JSX.Element { + return ( + + + + ); +} diff --git a/frontend/packages/components/src/icons/logos/vendor/PythonLogo.tsx b/frontend/packages/components/src/icons/logos/vendor/PythonLogo.tsx new file mode 100644 index 0000000000..acbc40c1a4 --- /dev/null +++ b/frontend/packages/components/src/icons/logos/vendor/PythonLogo.tsx @@ -0,0 +1,41 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import type {JSX} from 'react'; + +export interface PythonLogoProps { + size?: number; +} + +/** Python's full-color two-snake mark, used to identify Python quickstart guides. */ +export default function PythonLogo({size = 64}: PythonLogoProps): JSX.Element { + return ( + + + + + + + + + + + + + + + ); +} diff --git a/frontend/packages/components/src/icons/logos/vendor/ReactIcon.tsx b/frontend/packages/components/src/icons/logos/vendor/ReactIcon.tsx new file mode 100644 index 0000000000..4853b8f1c3 --- /dev/null +++ b/frontend/packages/components/src/icons/logos/vendor/ReactIcon.tsx @@ -0,0 +1,26 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import type {JSX} from 'react'; + +export interface ReactIconProps { + size?: number; +} + +/** React's atom logo, used to identify React quickstart guides. */ +export default function ReactIcon({size = 20}: ReactIconProps): JSX.Element { + return ( + + + + + ); +} diff --git a/frontend/packages/components/src/icons/logos/vendor/StackblitzIcon.tsx b/frontend/packages/components/src/icons/logos/vendor/StackblitzIcon.tsx new file mode 100644 index 0000000000..8f143e4fbd --- /dev/null +++ b/frontend/packages/components/src/icons/logos/vendor/StackblitzIcon.tsx @@ -0,0 +1,57 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import type {JSX} from 'react'; + +export interface StackblitzIconProps { + size?: number; +} + +/** StackBlitz's monotone wordmark, matching `assets/images/icons/stackblitz.svg`. */ +export default function StackblitzIcon({size = 18}: StackblitzIconProps): JSX.Element { + return ( + + + + + + + + + + + + + + ); +} diff --git a/frontend/packages/components/src/icons/logos/vendor/VueIcon.tsx b/frontend/packages/components/src/icons/logos/vendor/VueIcon.tsx new file mode 100644 index 0000000000..71c4a37580 --- /dev/null +++ b/frontend/packages/components/src/icons/logos/vendor/VueIcon.tsx @@ -0,0 +1,18 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import type {JSX} from 'react'; + +export interface VueIconProps { + size?: number; +} + +/** Vue.js's wordmark, used to identify Vue quickstart guides. */ +export default function VueIcon({size = 20}: VueIconProps): JSX.Element { + return ( + + + + + ); +} diff --git a/frontend/packages/components/src/icons/logos/vendor/__tests__/vendorIcons.test.tsx b/frontend/packages/components/src/icons/logos/vendor/__tests__/vendorIcons.test.tsx new file mode 100644 index 0000000000..b6f06ccfd5 --- /dev/null +++ b/frontend/packages/components/src/icons/logos/vendor/__tests__/vendorIcons.test.tsx @@ -0,0 +1,177 @@ +// Copyright 2026 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +import {render} from '@testing-library/react'; +import {describe, expect, it} from 'vitest'; +import AndroidLogo from '../AndroidLogo'; +import AppleIcon from '../AppleIcon'; +import ExpressIcon from '../ExpressIcon'; +import FlutterLogo from '../FlutterLogo'; +import GithubIcon from '../GithubIcon'; +import GoogleIcon from '../GoogleIcon'; +import HeidiIcon from '../HeidiIcon'; +import JavaScriptIcon from '../JavaScriptIcon'; +import LissiIcon from '../LissiIcon'; +import NextjsIcon from '../NextjsIcon'; +import NodeIcon from '../NodeIcon'; +import NuxtIcon from '../NuxtIcon'; +import PythonLogo from '../PythonLogo'; +import ReactIcon from '../ReactIcon'; +import StackblitzIcon from '../StackblitzIcon'; +import VueIcon from '../VueIcon'; + +function renderSvg(element: React.ReactElement): SVGSVGElement { + const {container} = render(element); + const svg = container.querySelector('svg'); + if (!svg) { + throw new Error('Expected component to render an element'); + } + return svg; +} + +function widthOf(svg: SVGSVGElement): number { + return Number(svg.getAttribute('width')); +} + +function heightOf(svg: SVGSVGElement): number { + return Number(svg.getAttribute('height')); +} + +interface SquareIconCase { + Component: (props: {size?: number}) => React.ReactElement; + defaultSize: number; + name: string; +} + +const SQUARE_ICONS: SquareIconCase[] = [ + {Component: AppleIcon, defaultSize: 20, name: 'AppleIcon'}, + {Component: ExpressIcon, defaultSize: 20, name: 'ExpressIcon'}, + {Component: GithubIcon, defaultSize: 20, name: 'GithubIcon'}, + {Component: GoogleIcon, defaultSize: 20, name: 'GoogleIcon'}, + {Component: JavaScriptIcon, defaultSize: 20, name: 'JavaScriptIcon'}, + {Component: NextjsIcon, defaultSize: 20, name: 'NextjsIcon'}, + {Component: NuxtIcon, defaultSize: 20, name: 'NuxtIcon'}, + {Component: ReactIcon, defaultSize: 20, name: 'ReactIcon'}, +]; + +describe.each(SQUARE_ICONS)('$name', ({Component, defaultSize}) => { + it('renders an svg with equal width and height at the default size', () => { + const svg = renderSvg(); + + expect(widthOf(svg)).toBe(defaultSize); + expect(heightOf(svg)).toBe(defaultSize); + }); + + it('scales width and height together when a custom size is passed', () => { + const svg = renderSvg(); + + expect(widthOf(svg)).toBe(48); + expect(heightOf(svg)).toBe(48); + }); +}); + +interface AspectIconCase { + Component: (props: {size?: number}) => React.ReactElement; + defaultSize: number; + heightFromSize: (size: number) => number; + name: string; + widthFromSize: (size: number) => number; +} + +const ASPECT_ICONS: AspectIconCase[] = [ + { + Component: AndroidLogo, + defaultSize: 64, + heightFromSize: (size) => size * 0.586, + name: 'AndroidLogo', + widthFromSize: (size) => size, + }, + { + Component: FlutterLogo, + defaultSize: 64, + heightFromSize: (size) => size * 1.238, + name: 'FlutterLogo', + widthFromSize: (size) => size, + }, + { + Component: NodeIcon, + defaultSize: 20, + heightFromSize: (size) => (size * 50) / 44, + name: 'NodeIcon', + widthFromSize: (size) => size, + }, + { + Component: PythonLogo, + defaultSize: 64, + heightFromSize: (size) => size * 1.018, + name: 'PythonLogo', + widthFromSize: (size) => size, + }, + { + Component: StackblitzIcon, + defaultSize: 18, + heightFromSize: (size) => size, + name: 'StackblitzIcon', + widthFromSize: (size) => (size * 89) / 18, + }, + { + Component: VueIcon, + defaultSize: 20, + heightFromSize: (size) => (size * 170.02) / 196.32, + name: 'VueIcon', + widthFromSize: (size) => size, + }, +]; + +describe.each(ASPECT_ICONS)('$name', ({Component, defaultSize, heightFromSize, widthFromSize}) => { + it('preserves its aspect ratio at the default size', () => { + const svg = renderSvg(); + + expect(widthOf(svg)).toBeCloseTo(widthFromSize(defaultSize), 5); + expect(heightOf(svg)).toBeCloseTo(heightFromSize(defaultSize), 5); + }); + + it('preserves its aspect ratio when a custom size is passed', () => { + const svg = renderSvg(); + + expect(widthOf(svg)).toBeCloseTo(widthFromSize(defaultSize * 2), 5); + expect(heightOf(svg)).toBeCloseTo(heightFromSize(defaultSize * 2), 5); + }); +}); + +describe('HeidiIcon', () => { + it('renders an svg sized by height at the default height', () => { + const svg = renderSvg(); + + expect(heightOf(svg)).toBe(24); + }); + + it('honors a custom height', () => { + const svg = renderSvg(); + + expect(heightOf(svg)).toBe(48); + }); +}); + +describe('LissiIcon', () => { + it('renders an svg sized by height at the default height', () => { + const svg = renderSvg(); + + expect(heightOf(svg)).toBe(24); + }); + + it('honors a custom height', () => { + const svg = renderSvg(); + + expect(heightOf(svg)).toBe(48); + }); +}); + +describe('JavaScriptIcon', () => { + it('exposes an accessible name via role and aria-label', () => { + const svg = renderSvg(); + + expect(svg.getAttribute('role')).toBe('img'); + expect(svg.getAttribute('aria-label')).toBe('JavaScript logo'); + }); +}); diff --git a/frontend/packages/components/src/index.ts b/frontend/packages/components/src/index.ts index 8ac321a0d7..c545161fd8 100644 --- a/frontend/packages/components/src/index.ts +++ b/frontend/packages/components/src/index.ts @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 // Components +export {default as CopyableField} from './CopyableField/CopyableField'; +export type {CopyableFieldProps} from './CopyableField/CopyableField'; export {default as ExternalLinkConfirmDialog} from './ExternalLinkConfirm/ExternalLinkConfirmDialog'; export type {ExternalLinkConfirmDialogProps} from './ExternalLinkConfirm/ExternalLinkConfirmDialog'; export {default as useExternalLinkConfirmation} from './ExternalLinkConfirm/useExternalLinkConfirmation'; @@ -19,6 +21,8 @@ export type {OrganizationUnitSummaryChipProps} from './OrganizationUnitSummaryCh export {default as PageLoader} from './PageLoader/PageLoader'; export {default as ReadErrorState} from './ReadErrorState/ReadErrorState'; export type {ReadErrorStateProps, ReadErrorMessageResolver} from './ReadErrorState/ReadErrorState'; +export {default as StackblitzQuickstartCard} from './StackblitzQuickstartCard/StackblitzQuickstartCard'; +export type {StackblitzQuickstartCardProps} from './StackblitzQuickstartCard/StackblitzQuickstartCard'; export {default as ToggleCard} from './ToggleCard/ToggleCard'; export type {ToggleCardProps} from './ToggleCard/ToggleCard'; @@ -28,6 +32,18 @@ export {default as GithubIcon} from './icons/logos/vendor/GithubIcon'; export {default as GoogleIcon} from './icons/logos/vendor/GoogleIcon'; export {default as HeidiIcon} from './icons/logos/vendor/HeidiIcon'; export {default as LissiIcon} from './icons/logos/vendor/LissiIcon'; +export {default as StackblitzIcon} from './icons/logos/vendor/StackblitzIcon'; +export {default as AppleIcon} from './icons/logos/vendor/AppleIcon'; +export {default as AndroidLogo} from './icons/logos/vendor/AndroidLogo'; +export {default as FlutterLogo} from './icons/logos/vendor/FlutterLogo'; +export {default as ReactIcon} from './icons/logos/vendor/ReactIcon'; +export {default as VueIcon} from './icons/logos/vendor/VueIcon'; +export {default as NextjsIcon} from './icons/logos/vendor/NextjsIcon'; +export {default as NuxtIcon} from './icons/logos/vendor/NuxtIcon'; +export {default as ExpressIcon} from './icons/logos/vendor/ExpressIcon'; +export {default as NodeIcon} from './icons/logos/vendor/NodeIcon'; +export {default as JavaScriptIcon} from './icons/logos/vendor/JavaScriptIcon'; +export {default as PythonLogo} from './icons/logos/vendor/PythonLogo'; /* -------------------------- LAB -------------------------- */ diff --git a/frontend/packages/i18n/src/locales/en-US.ts b/frontend/packages/i18n/src/locales/en-US.ts index aa35dbc3ad..7aa08216ca 100644 --- a/frontend/packages/i18n/src/locales/en-US.ts +++ b/frontend/packages/i18n/src/locales/en-US.ts @@ -129,7 +129,7 @@ const translations = { // External links learnMore: 'Learn more', 'externalLink.title': 'You are leaving {{productName}}', - 'externalLink.message': 'This link leads to an external site that {{productName}} does not control:', + 'externalLink.message': 'This link leads to an external site. Please verify the destination before proceeding:', // Navigation 'navigation.home': 'Home', @@ -2291,6 +2291,8 @@ const translations = { 'onboarding.configure.stack.technology.ios.description': 'Native iOS application (Swift or Objective-C)', 'onboarding.configure.stack.technology.android.title': 'Android', 'onboarding.configure.stack.technology.android.description': 'Native Android application (Kotlin or Java)', + 'onboarding.configure.stack.technology.flutter.title': 'Flutter', + 'onboarding.configure.stack.technology.flutter.description': 'Cross-platform mobile application built with Flutter', 'onboarding.configure.stack.technology.springboot.title': 'Spring Boot', 'onboarding.configure.stack.technology.springboot.description': 'Java backend application with Spring Boot', 'onboarding.configure.stack.technology.nodejs.title': 'Node.js', @@ -2632,7 +2634,7 @@ const translations = { 'edit.page.logoUpdate.label': 'Update Logo', 'edit.page.description.empty': 'No description', 'edit.page.description.placeholder': 'Add a description', - 'edit.page.tabs.overview': 'Guide', + 'edit.page.tabs.overview': 'Overview', 'edit.page.tabs.general': 'General', 'edit.page.tabs.flows': 'Flows', 'edit.page.tabs.customization': 'Customization', @@ -2659,11 +2661,45 @@ const translations = { 'edit.mcp.connect.clientUri.error.invalid': 'Please enter a valid URL', // Overview section - 'edit.overview.noGuides': 'No integration guides available for this application type.', + 'edit.overview.stackblitz.heading': 'Try the live quickstart', + 'edit.overview.stackblitz.subheading': 'Run {{name}} in StackBlitz', + 'edit.overview.stackblitz.cta': 'Open on StackBlitz', + 'edit.overview.readGuide.title': 'Read the quickstart guide', + 'edit.overview.readGuide.description': 'Step-by-step integration guide on the docs site.', + 'edit.overview.readGuide.titleFor': '{{label}} quickstart guide', + 'edit.overview.readGuide.descriptionFor': 'Step-by-step {{label}} integration guide on the docs site.', + 'edit.overview.readGuide.action': 'Open quickstart', + 'edit.overview.agentPrompt.title': 'Integrate with a coding agent', + 'edit.overview.agentPrompt.badge': 'AI', + 'edit.overview.agentPrompt.description': 'Copy a ready-made prompt for Claude, Cursor, or any agent.', + 'edit.overview.agentPrompt.action': 'Copy prompt', + 'edit.overview.signInPreview.title': 'Preview', + 'edit.overview.signInPreview.description': 'What users see when they sign in to {{name}}.', + 'edit.overview.signInPreview.themeLabel': 'Theme used', + 'edit.overview.signInPreview.defaultTheme': 'Default', + 'edit.overview.signInPreview.flowsLabel': 'Flows', + 'edit.overview.signInPreview.flows.signIn': 'Sign-in', + 'edit.overview.signInPreview.flows.signUp': 'Sign-up', + 'edit.overview.signInPreview.flows.recovery': 'Password recovery', + 'edit.overview.signInPreview.flows.signOut': 'Sign-out', + 'edit.overview.signInPreview.notConfigured': 'Not configured', + 'edit.overview.signInPreview.editCustomization': 'Edit in Customization', + 'edit.overview.signInPreview.editFlows': 'Edit in Flows', + 'edit.overview.appDetails.title': 'Application details', + 'edit.overview.appDetails.description': 'Identifiers used in your integration code.', + 'edit.overview.endpoints.appNativeTitle': 'Useful Endpoints', + 'edit.overview.endpoints.appNativeDescription': 'For driving sign-in and registration with a fully custom UI.', + 'edit.overview.endpoints.wellknown': 'OpenID configuration', + 'edit.overview.endpoints.authorization': 'Authorization endpoint', + 'edit.overview.endpoints.token': 'Token endpoint', + 'edit.overview.endpoints.userinfo': 'Userinfo endpoint', + 'edit.overview.endpoints.jwks': 'JWKS URI', + 'edit.overview.endpoints.flowExecute': 'Flow execution endpoint', + 'edit.overview.endpoints.flowMeta': 'Flow metadata endpoint', + 'edit.overview.endpoints.passkeyRegisterStart': 'Passkey registration (start)', + 'edit.overview.endpoints.passkeyRegisterFinish': 'Passkey registration (finish)', // General section - 'edit.general.sections.quickCopy': 'Quick Copy', - 'edit.general.sections.quickCopy.description': 'Copy application identifiers for use in your code.', 'edit.general.sections.access': 'Access', 'edit.general.sections.access.description': "Configure who can access this application, where it's hosted, etc.", 'edit.general.sections.contacts': 'Contacts', @@ -2673,8 +2709,6 @@ const translations = { 'edit.general.labels.allowedUserTypes': 'Allowed User Types', 'edit.general.labels.applicationUrl': 'Application URL', 'edit.general.labels.contacts': 'Contacts', - 'edit.general.applicationId.hint': 'Unique identifier for your application', - 'edit.general.clientId.hint': 'OAuth2 client identifier used for authentication', 'edit.general.noUserTypes': 'No user types configured', 'edit.general.contacts.placeholder': 'Type an email and press Enter', 'edit.general.contacts.hint': 'Type a valid email address and press <0>Enter to add it', diff --git a/install/openchoreo/thunderid-oc-resourcetype/samples/resource.yaml b/install/openchoreo/thunderid-oc-resourcetype/samples/resource.yaml index 12de762bfb..6b292d1d7f 100644 --- a/install/openchoreo/thunderid-oc-resourcetype/samples/resource.yaml +++ b/install/openchoreo/thunderid-oc-resourcetype/samples/resource.yaml @@ -1454,7 +1454,7 @@ spec: name: Default description: Default organization unit parent: null - logoUrl: avatar:shape=rounded,variant=anonymous_entity,content=pavilion,colors=0,bg=#2c4ed4 + logoUrl: avatar:shape=rounded,variant=anonymous_entity,content=pavilion,colors=0 createdAt: updatedAt: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cc1a0f9ce..4fba4e9168 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -473,18 +473,39 @@ importers: '@scalar/api-reference-react': specifier: 0.9.38 version: 0.9.38(jwt-decode@4.0.0)(nprogress@0.2.0)(qrcode@1.5.4)(react@19.2.3)(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3) + '@tanstack/react-query': + specifier: 'catalog:' + version: 5.90.5(react@19.2.3) + '@thunderid/components': + specifier: workspace:^ + version: link:../frontend/packages/components + '@thunderid/contexts': + specifier: workspace:^ + version: link:../frontend/packages/contexts '@thunderid/design': specifier: workspace:^ version: link:../frontend/packages/design '@thunderid/eslint-plugin': specifier: workspace:^ version: link:../frontend/packages/eslint-plugin + '@thunderid/hooks': + specifier: workspace:^ + version: link:../frontend/packages/hooks + '@thunderid/i18n': + specifier: workspace:^ + version: link:../frontend/packages/i18n '@thunderid/logger': specifier: workspace:^ version: link:../frontend/packages/logger '@thunderid/prettier-config': specifier: workspace:^ version: link:../frontend/packages/prettier-config + '@thunderid/react': + specifier: 'catalog:' + version: 0.11.3(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@thunderid/utils': + specifier: workspace:^ + version: link:../frontend/packages/utils '@wso2/oxygen-ui': specifier: 'catalog:' version: 0.13.0(@mui/system@7.3.11(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(@wso2/oxygen-ui-icons-react@0.13.0(react@19.2.3))(dayjs@1.11.21)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -512,6 +533,12 @@ importers: react-github-btn: specifier: 1.4.0 version: 1.4.0(react@19.2.3) + react-i18next: + specifier: 'catalog:' + version: 16.0.1(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + react-router: + specifier: 'catalog:' + version: 8.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) devDependencies: '@docusaurus/module-type-aliases': specifier: 3.9.2 @@ -2851,19 +2878,19 @@ importers: dependencies: '@langchain/anthropic': specifier: ^1.3.0 - version: 1.4.0(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0)) + version: 1.5.2(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0)) '@langchain/core': specifier: ^1.1.0 - version: 1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0) + version: 1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0) '@langchain/google-genai': specifier: ^2.1.0 - version: 2.1.31(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0)) + version: 2.2.0(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0)) '@langchain/langgraph': specifier: ^1.3.0 - version: 1.3.7(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vue@3.5.34(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) + version: 1.4.9(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.3(react@19.2.6))(react@19.2.6)(vue@3.5.34(typescript@5.9.3))(zod@4.4.3) '@langchain/mcp-adapters': specifier: ^1.1.0 - version: 1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))(@langchain/langgraph@1.3.7(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vue@3.5.34(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)) + version: 1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))(@langchain/langgraph@1.4.9(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.3(react@19.2.6))(react@19.2.6)(vue@3.5.34(typescript@5.9.3))(zod@4.4.3)) '@thunderid/javascript': specifier: 0.0.5 version: 0.0.5 @@ -2876,7 +2903,7 @@ importers: devDependencies: '@types/node': specifier: ^22.15.19 - version: 22.15.29 + version: 22.20.1 samples/apps/wayfinder-sample/backend: dependencies: @@ -3109,8 +3136,8 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@anthropic-ai/sdk@0.95.2': - resolution: {integrity: sha512-Egddwo3sheo1PzUrMkZnH6VkQYwS0h/b/i8vSK8Ta9M45UQipAMeDFH57dYuDAfXMEUUGeKw6CMlremgMZgrSQ==} + '@anthropic-ai/sdk@0.115.0': + resolution: {integrity: sha512-BJrFIVyjNuU8lfDyIJTvlRYzgQg+zEl78BxE7fq8esULsGz9IRQvGtW5spq3tydmtjQb/GFdooKGdGsetpx+lQ==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -3219,6 +3246,10 @@ packages: resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + '@babel/helper-remap-async-to-generator@7.27.1': resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} engines: {node: '>=6.9.0'} @@ -3606,14 +3637,14 @@ packages: peerDependencies: '@babel/core': '>=7.29.6 <8' - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': '>=7.29.6 <8' - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': '>=7.29.6 <8' @@ -4451,6 +4482,9 @@ packages: '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} @@ -4836,8 +4870,8 @@ packages: peerDependencies: vue: 3.5.34 - '@hono/node-server@2.0.11': - resolution: {integrity: sha512-bjD221KPLoJTWUwso1J6fGKiTXEUFedG/s0visavY4zakFPkeGURMRNly+FhBHs7T8Dz4qHaZIMX9ZoJHSJtKA==} + '@hono/node-server@2.1.0': + resolution: {integrity: sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==} engines: {node: '>=20'} peerDependencies: hono: ^4 @@ -5336,30 +5370,30 @@ packages: peerDependencies: tslib: '2' - '@langchain/anthropic@1.4.0': - resolution: {integrity: sha512-rs1yVydrHjyiD31uChdCnKZpmDuKa0Bpz8Raiy9GvqnqmfXPMe0oOrap/2paE+NRSinDbtax8mMpP/yv8EbO1A==} + '@langchain/anthropic@1.5.2': + resolution: {integrity: sha512-lYOHo5BpRbgmQVSggwPLhBNFtatiAFlVirY44tnfocx5tQKfeLYo5emqPwNwcNBuw236sr0tsDxZcmbLASN/GA==} engines: {node: '>=20'} peerDependencies: - '@langchain/core': ^1.1.47 + '@langchain/core': ^1.2.3 - '@langchain/core@1.1.49': - resolution: {integrity: sha512-7wkN3Qv/qZqsY0p3h48CNu6E6y5GMYatYxj+JrX4uVNBiqIVQm1Z528QrmayJWVW9SQTQicqRNoyTCzl+K9F8Q==} + '@langchain/core@1.2.4': + resolution: {integrity: sha512-GIrJktdsFPx8gM0C3VyikkeYGZAV7iRIzUJuS5tFatiKLExybou0LI0MuxH9kksN38quyfWdQDl20lIEAEVkVA==} engines: {node: '>=20'} - '@langchain/google-genai@2.1.31': - resolution: {integrity: sha512-lHIJGtZab0jqoufKRPXyHHg1nLXrE74LXd0ftgibWEACc1SpSLu6XwtA23+dX4l7Q/YeSgb9n40YJx5k00/fqw==} + '@langchain/google-genai@2.2.0': + resolution: {integrity: sha512-1mDqbmB6+iC6ZBQY15r5xJg9wPErnQ774inpKh6qi6BrrjadDwaPHoklJW5IXU94edKiDpm1akIzJCrQDWe6yA==} engines: {node: '>=20'} peerDependencies: - '@langchain/core': ^1.1.47 + '@langchain/core': ^1.2.0 - '@langchain/langgraph-checkpoint@1.1.1': - resolution: {integrity: sha512-gHqhO6e2dyZ7TTfyaFy25yjcRsavURc9XMGT4q+LUBTc0hT4JxKe3qvrMX2OFTzW8W/0kjV59haHmSRFZIGkvg==} + '@langchain/langgraph-checkpoint@1.1.3': + resolution: {integrity: sha512-wgzdQNeEsdw1e+4lvlj0tdq/RYR/k1vPin10g0ymGoehZDDgd9nvIllGXSXN4TFgF9sf5qQP/KTkOcLfeseIhA==} engines: {node: '>=18'} peerDependencies: '@langchain/core': ^1.1.48 - '@langchain/langgraph-sdk@1.9.20': - resolution: {integrity: sha512-waZQNUN6apVg3jvDApy16wUGwrCvJB8WXHbIPvVrrUZHObqx0w5tk7P/Ite5Qvu4abWrvNtrmqk2zmcqCx7jVw==} + '@langchain/langgraph-sdk@1.9.28': + resolution: {integrity: sha512-4j3XuM0PvtmAbL8mPfBS99ez3+ytRfgbOpAR/nOeaejTRF3Q9dNw2QnaGLGng8wLPtGLoSj+SYgUOVxy9Bv9vg==} peerDependencies: '@langchain/core': ^1.1.48 react: ^18 || ^19 @@ -5376,16 +5410,12 @@ packages: vue: optional: true - '@langchain/langgraph@1.3.7': - resolution: {integrity: sha512-6WNskiu3bpy3XFwiD9cm4n2QapnFVX7dqYeJzuEAUGp9R3vbxngRVyb5+Myp5Rz4GCFXGXuhcFSjLVRHPhsNtQ==} + '@langchain/langgraph@1.4.9': + resolution: {integrity: sha512-EvD9rS66Cya09y6rbMgD3Ir8miAkJQFo7FyJOPRPO736Kz3y5TeyeBDOS8ctff/jRc788bPijHx2NVFM79Qqig==} engines: {node: '>=18'} peerDependencies: '@langchain/core': ^1.1.48 zod: ^3.25.32 || ^4.2.0 - zod-to-json-schema: ^3.x - peerDependenciesMeta: - zod-to-json-schema: - optional: true '@langchain/mcp-adapters@1.1.3': resolution: {integrity: sha512-OPHIQNkTUJjnRj1pr+cp2nguMBZeF3Q1pVT1hCbgU7BrHgV7lov99wbU8po8Cm4zZzmeRtVO/T9X1SrDD1ogtQ==} @@ -5394,8 +5424,8 @@ packages: '@langchain/core': ^1.0.0 '@langchain/langgraph': ^1.0.0 - '@langchain/protocol@0.0.16': - resolution: {integrity: sha512-ws+J7MaHyhO5dG7f0vdyHQiUn9hoCnki0f3crJPa4MCTGzcRC39jYSCghyrGtBPYQnZbUQiGyRVpW3z3M8IpJg==} + '@langchain/protocol@0.0.18': + resolution: {integrity: sha512-XW1egQtPfsGI41w2AMZNFZrUIwFSQHTjVMZs0OaTpCAvht/QLoaPN8FQcsysMVypOhupG28J29yOorrc70otBQ==} '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} @@ -5516,16 +5546,6 @@ packages: '@mermaid-js/parser@1.2.0': resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==} - '@modelcontextprotocol/sdk@1.29.0': - resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - '@modelcontextprotocol/sdk@1.30.0': resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} engines: {node: '>=18'} @@ -6938,8 +6958,8 @@ packages: '@types/node@17.0.45': resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - '@types/node@22.15.29': - resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} '@types/node@24.12.4': resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} @@ -9123,8 +9143,8 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - express-rate-limit@8.5.2: - resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + express-rate-limit@8.6.1: + resolution: {integrity: sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -9615,8 +9635,8 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono@4.12.34: - resolution: {integrity: sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==} + hono@4.13.0: + resolution: {integrity: sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ==} engines: {node: '>=16.9.0'} hookable@6.1.1: @@ -10284,8 +10304,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - langsmith@0.7.5: - resolution: {integrity: sha512-OeD6+yKtWwy6sAboq25kD5DICzYv7j2KgtV2n4LsJ8nU2LpEdt1UwbjA6BON/zTggmS/YjV2TtLTHd7VEJhtEA==} + langsmith@0.8.9: + resolution: {integrity: sha512-6ikTB5SO+3SlBQ/+oH8K/JzhglxB8AM4RAe5bHzgWSEIHTgTe+b71bSjf1fDvlYuEXoiPn6uP10BbczrbznUnA==} peerDependencies: '@opentelemetry/api': '*' '@opentelemetry/exporter-trace-otlp-proto': '*' @@ -11158,8 +11178,8 @@ packages: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} - p-queue@9.3.0: - resolution: {integrity: sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==} + p-queue@9.3.3: + resolution: {integrity: sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==} engines: {node: '>=20'} p-retry@6.2.1: @@ -11848,11 +11868,6 @@ packages: peerDependencies: react: ^19.2.3 - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} - peerDependencies: - react: ^19.2.7 - react-error-boundary@6.1.1: resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==} peerDependencies: @@ -11979,10 +11994,6 @@ packages: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} - engines: {node: '>=0.10.0'} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -13914,7 +13925,7 @@ snapshots: package-manager-detector: 1.8.0 tinyexec: 1.1.2 - '@anthropic-ai/sdk@0.95.2(zod@3.25.76)': + '@anthropic-ai/sdk@0.115.0(zod@3.25.76)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.0.0 @@ -13982,8 +13993,8 @@ snapshots: '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -14080,6 +14091,8 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -14499,15 +14512,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.7)': dependencies: @@ -14746,9 +14759,9 @@ snapshots: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.3 + '@babel/parser': 7.29.7 '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -16327,6 +16340,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 @@ -16707,9 +16725,9 @@ snapshots: '@tanstack/vue-virtual': 3.13.24(vue@3.5.34(typescript@5.9.3)) vue: 3.5.34(typescript@5.9.3) - '@hono/node-server@2.0.11(hono@4.12.34)': + '@hono/node-server@2.1.0(hono@4.13.0)': dependencies: - hono: 4.12.34 + hono: 4.13.0 '@hookform/resolvers@5.2.2(react-hook-form@7.65.0(react@19.2.3))(zod@4.3.6)': dependencies: @@ -16841,7 +16859,7 @@ snapshots: '@img/sharp-wasm32@0.35.3': dependencies: - '@emnapi/runtime': 1.11.3 + '@emnapi/runtime': 1.11.2 optional: true '@img/sharp-webcontainers-wasm32@0.35.3': @@ -17151,18 +17169,18 @@ snapshots: '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) tslib: 2.8.1 - '@langchain/anthropic@1.4.0(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))': + '@langchain/anthropic@1.5.2(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))': dependencies: - '@anthropic-ai/sdk': 0.95.2(zod@3.25.76) - '@langchain/core': 1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0) + '@anthropic-ai/sdk': 0.115.0(zod@3.25.76) + '@langchain/core': 1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0) zod: 3.25.76 - '@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0)': + '@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0)': dependencies: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 js-tiktoken: 1.0.21 - langsmith: 0.7.5(@opentelemetry/api@1.9.0)(ws@8.21.0) + langsmith: 0.8.9(@opentelemetry/api@1.9.0)(ws@8.21.0) mustache: 4.2.0 p-queue: 6.6.2 zod: 3.25.76 @@ -17173,50 +17191,46 @@ snapshots: - openai - ws - '@langchain/google-genai@2.1.31(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))': + '@langchain/google-genai@2.2.0(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))': dependencies: '@google/generative-ai': 0.24.1 - '@langchain/core': 1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0) + '@langchain/core': 1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0) - '@langchain/langgraph-checkpoint@1.1.1(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))': + '@langchain/langgraph-checkpoint@1.1.3(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))': dependencies: - '@langchain/core': 1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0) + '@langchain/core': 1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0) - '@langchain/langgraph-sdk@1.9.20(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vue@3.5.34(typescript@5.9.3))': + '@langchain/langgraph-sdk@1.9.28(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.3(react@19.2.6))(react@19.2.6)(vue@3.5.34(typescript@5.9.3))': dependencies: - '@langchain/core': 1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0) - '@langchain/protocol': 0.0.16 + '@langchain/core': 1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0) + '@langchain/protocol': 0.0.18 '@types/json-schema': 7.0.15 - p-queue: 9.3.0 + p-queue: 9.3.3 p-retry: 7.1.1 - uuid: 11.1.1 optionalDependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.6 + react-dom: 19.2.3(react@19.2.6) vue: 3.5.34(typescript@5.9.3) - '@langchain/langgraph@1.3.7(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vue@3.5.34(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': + '@langchain/langgraph@1.4.9(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.3(react@19.2.6))(react@19.2.6)(vue@3.5.34(typescript@5.9.3))(zod@4.4.3)': dependencies: - '@langchain/core': 1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0) - '@langchain/langgraph-checkpoint': 1.1.1(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0)) - '@langchain/langgraph-sdk': 1.9.20(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vue@3.5.34(typescript@5.9.3)) - '@langchain/protocol': 0.0.16 + '@langchain/core': 1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0) + '@langchain/langgraph-checkpoint': 1.1.3(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0)) + '@langchain/langgraph-sdk': 1.9.28(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.3(react@19.2.6))(react@19.2.6)(vue@3.5.34(typescript@5.9.3)) + '@langchain/protocol': 0.0.18 '@standard-schema/spec': 1.1.0 - uuid: 11.1.1 zod: 4.4.3 - optionalDependencies: - zod-to-json-schema: 3.25.2(zod@4.4.3) transitivePeerDependencies: - react - react-dom - svelte - vue - '@langchain/mcp-adapters@1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))(@langchain/langgraph@1.3.7(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vue@3.5.34(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))': + '@langchain/mcp-adapters@1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))(@langchain/langgraph@1.4.9(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.3(react@19.2.6))(react@19.2.6)(vue@3.5.34(typescript@5.9.3))(zod@4.4.3))': dependencies: - '@langchain/core': 1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0) - '@langchain/langgraph': 1.3.7(@langchain/core@1.1.49(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vue@3.5.34(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) - '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + '@langchain/core': 1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0) + '@langchain/langgraph': 1.4.9(@langchain/core@1.2.4(@opentelemetry/api@1.9.0)(ws@8.21.0))(react-dom@19.2.3(react@19.2.6))(react@19.2.6)(vue@3.5.34(typescript@5.9.3))(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) debug: 4.4.3 zod: 3.25.76 optionalDependencies: @@ -17225,7 +17239,7 @@ snapshots: - '@cfworker/json-schema' - supports-color - '@langchain/protocol@0.0.16': {} + '@langchain/protocol@0.0.18': {} '@leichtgewicht/ip-codec@2.0.5': {} @@ -17478,33 +17492,9 @@ snapshots: dependencies: '@chevrotain/types': 11.1.2 - '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)': - dependencies: - '@hono/node-server': 2.0.11(hono@4.12.34) - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.8 - express: 5.2.1 - express-rate-limit: 8.5.2(express@5.2.1) - hono: 4.12.34 - jose: 6.2.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.2(zod@3.25.76) - optionalDependencies: - '@cfworker/json-schema': 4.1.1 - transitivePeerDependencies: - - supports-color - '@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)': dependencies: - '@hono/node-server': 2.0.11(hono@4.12.34) + '@hono/node-server': 2.1.0(hono@4.13.0) ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) content-type: 1.0.5 @@ -17513,8 +17503,8 @@ snapshots: eventsource: 3.0.7 eventsource-parser: 3.0.8 express: 5.2.1 - express-rate-limit: 8.5.2(express@5.2.1) - hono: 4.12.34 + express-rate-limit: 8.6.1(express@5.2.1) + hono: 4.13.0 jose: 6.2.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -17551,7 +17541,7 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 '@mui/core-downloads-tracker': 7.3.11 - '@mui/system': 7.1.0(@emotion/react@11.11.0(@types/react@19.2.14)(react@19.2.3))(@emotion/styled@11.11.0(@emotion/react@11.11.0(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3) + '@mui/system': 7.3.11(@emotion/react@11.11.0(@types/react@19.2.14)(react@19.2.3))(@emotion/styled@11.11.0(@emotion/react@11.11.0(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3) '@mui/types': 7.4.12(@types/react@19.2.14) '@mui/utils': 7.3.11(@types/react@19.2.14)(react@19.2.3) '@popperjs/core': 2.11.8 @@ -17640,6 +17630,22 @@ snapshots: '@emotion/styled': 11.11.0(@emotion/react@11.11.0(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3) '@types/react': 19.2.14 + '@mui/system@7.3.11(@emotion/react@11.11.0(@types/react@19.2.14)(react@19.2.3))(@emotion/styled@11.11.0(@emotion/react@11.11.0(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@babel/runtime': 7.29.2 + '@mui/private-theming': 7.3.11(@types/react@19.2.14)(react@19.2.3) + '@mui/styled-engine': 7.3.10(@emotion/react@11.11.0(@types/react@19.2.14)(react@19.2.3))(@emotion/styled@11.11.0(@emotion/react@11.11.0(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + '@mui/types': 7.4.12(@types/react@19.2.14) + '@mui/utils': 7.3.11(@types/react@19.2.14)(react@19.2.3) + clsx: 2.1.1 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 19.2.3 + optionalDependencies: + '@emotion/react': 11.11.0(@types/react@19.2.14)(react@19.2.3) + '@emotion/styled': 11.11.0(@emotion/react@11.11.0(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3) + '@types/react': 19.2.14 + '@mui/system@7.3.11(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.3))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3))(@types/react@19.2.14)(react@19.2.3)': dependencies: '@babel/runtime': 7.29.2 @@ -18649,7 +18655,7 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 entities: 4.5.0 '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': @@ -18897,24 +18903,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/body-parser@1.19.6': dependencies: @@ -19166,7 +19172,7 @@ snapshots: '@types/node@17.0.45': {} - '@types/node@22.15.29': + '@types/node@22.20.1': dependencies: undici-types: 6.21.0 @@ -19582,8 +19588,8 @@ snapshots: '@vitejs/plugin-react@5.2.0(vite@8.0.16(@types/node@25.0.2)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.47.1)(tsx@4.22.4)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.7 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 @@ -20733,7 +20739,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.23) postcss-modules-values: 4.0.0(postcss@8.5.23) postcss-value-parser: 4.2.0 - semver: 7.8.1 + semver: 7.8.5 optionalDependencies: webpack: 5.105.0(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.23))(html-minifier-terser@7.2.0)(postcss@8.5.23) @@ -21849,10 +21855,13 @@ snapshots: expect-type@1.3.0: {} - express-rate-limit@8.5.2(express@5.2.1): + express-rate-limit@8.6.1(express@5.2.1): dependencies: + debug: 4.4.3 express: 5.2.1 ip-address: 10.4.0 + transitivePeerDependencies: + - supports-color express@4.22.2: dependencies: @@ -22508,7 +22517,7 @@ snapshots: dependencies: react-is: 16.13.1 - hono@4.12.34: {} + hono@4.13.0: {} hookable@6.1.1: {} @@ -23184,7 +23193,7 @@ snapshots: kleur@3.0.3: {} - langsmith@0.7.5(@opentelemetry/api@1.9.0)(ws@8.21.0): + langsmith@0.8.9(@opentelemetry/api@1.9.0)(ws@8.21.0): dependencies: p-queue: 6.6.2 optionalDependencies: @@ -23376,7 +23385,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.8.1 + semver: 7.8.5 markdown-extensions@2.0.0: {} @@ -24337,7 +24346,7 @@ snapshots: eventemitter3: 4.0.7 p-timeout: 3.2.0 - p-queue@9.3.0: + p-queue@9.3.3: dependencies: eventemitter3: 5.0.4 p-timeout: 7.0.1 @@ -24637,7 +24646,7 @@ snapshots: cosmiconfig: 8.3.6(typescript@5.9.3) jiti: 1.21.7 postcss: 8.5.23 - semver: 7.8.1 + semver: 7.8.5 webpack: 5.105.0(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.23))(html-minifier-terser@7.2.0)(postcss@8.5.23) transitivePeerDependencies: - typescript @@ -25078,9 +25087,9 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 - react-dom@19.2.7(react@19.2.7): + react-dom@19.2.3(react@19.2.6): dependencies: - react: 19.2.7 + react: 19.2.6 scheduler: 0.27.0 optional: true @@ -25199,9 +25208,6 @@ snapshots: react@19.2.6: {} - react@19.2.7: - optional: true - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -25780,7 +25786,7 @@ snapshots: semver-diff@4.0.0: dependencies: - semver: 7.8.1 + semver: 7.8.5 semver@6.3.1: {} @@ -27235,11 +27241,6 @@ snapshots: dependencies: zod: 3.25.76 - zod-to-json-schema@3.25.2(zod@4.4.3): - dependencies: - zod: 4.4.3 - optional: true - zod-validation-error@4.0.2(zod@4.4.3): dependencies: zod: 4.4.3 diff --git a/tests/e2e/pages/applications/applications.page.ts b/tests/e2e/pages/applications/applications.page.ts index 343c236111..cac3a81114 100644 --- a/tests/e2e/pages/applications/applications.page.ts +++ b/tests/e2e/pages/applications/applications.page.ts @@ -40,8 +40,10 @@ export class ApplicationsPage extends BasePage { // Edit page — shared readonly saveButton: Locator; - // Edit page — General tab + // Edit page — Overview tab readonly applicationIdField: Locator; + + // Edit page — General tab readonly clientIdField: Locator; readonly applicationUrlInput: Locator; readonly addUriButton: Locator; @@ -87,8 +89,8 @@ export class ApplicationsPage extends BasePage { // Edit page — shared this.saveButton = page.getByRole("button", { name: /^save$/i }).or(page.locator('button:has-text("Save")')); - // Edit page — General tab - this.applicationIdField = page.locator("#application-id-input").or(page.getByLabel("Application ID")); + // Edit page — Overview tab + this.applicationIdField = page.getByText("Application ID", { exact: true }); this.clientIdField = page.getByLabel("Client ID"); this.applicationUrlInput = page.locator("#application-url-input").or(page.getByLabel("Application URL")); this.addUriButton = page.getByRole("button", { name: /add uri/i }).or(page.locator('button:has-text("Add URI")')); diff --git a/tests/e2e/tests/applications/application-edit.spec.ts b/tests/e2e/tests/applications/application-edit.spec.ts index 56941c3477..a9af096bbb 100644 --- a/tests/e2e/tests/applications/application-edit.spec.ts +++ b/tests/e2e/tests/applications/application-edit.spec.ts @@ -101,25 +101,25 @@ test.describe("Application Edit", () => { } }); - /** TC006: Edit page - General tab is default and shows Quick Copy fields */ - test("TC006: Edit page - General tab is default and shows Quick Copy fields", async ({ applicationsPage }) => { + /** TC006: Edit page - Overview tab shows application details */ + test("TC006: Edit page - Overview tab shows application details", async ({ applicationsPage }) => { await test.step("Navigate to application edit page", async () => { console.log(`Navigating to edit page for app: ${testAppId}`); await applicationsPage.gotoEdit(testAppId); await applicationsPage.screenshot("tc006-edit-page"); }); - await test.step("Verify General tab is selected by default", async () => { - const generalTab = applicationsPage.page.getByRole("tab", { name: /general/i }); - await expect(generalTab).toBeVisible(); - await expect(generalTab).toHaveAttribute("aria-selected", "true"); - console.log("General tab is active by default"); + await test.step("Click Overview tab", async () => { + await applicationsPage.clickTab("Overview"); + const overviewTab = applicationsPage.page.getByRole("tab", { name: /overview/i }); + await expect(overviewTab).toHaveAttribute("aria-selected", "true"); + console.log("Overview tab is active"); }); - await test.step("Verify Application ID field is visible in Quick Copy section", async () => { + await test.step("Verify Application ID field is visible in application details", async () => { await expect(applicationsPage.applicationIdField).toBeVisible(); console.log("Application ID field visible"); - await applicationsPage.screenshot("tc006-quick-copy-fields"); + await applicationsPage.screenshot("tc006-application-details"); }); }); @@ -132,6 +132,10 @@ test.describe("Application Edit", () => { await applicationsPage.screenshot("tc007-edit-page"); }); + await test.step("Click General tab", async () => { + await applicationsPage.clickTab("General"); + }); + await test.step("Add a redirect URI", async () => { console.log("Adding redirect URI:", testUri); await applicationsPage.addRedirectUri(testUri); @@ -158,6 +162,10 @@ test.describe("Application Edit", () => { await applicationsPage.gotoEdit(testAppId); }); + await test.step("Click General tab", async () => { + await applicationsPage.clickTab("General"); + }); + await test.step("Type an invalid URL into Application URL field", async () => { await applicationsPage.applicationUrlInput.waitFor({ state: "visible" }); await applicationsPage.applicationUrlInput.fill("not-a-valid-url"); @@ -315,6 +323,10 @@ test.describe("Application Edit", () => { await applicationsPage.screenshot("tc012-before-delete"); }); + await test.step("Click General tab", async () => { + await applicationsPage.clickTab("General"); + }); + await test.step("Click Delete Application and confirm", async () => { await applicationsPage.clickDeleteApplication(); await applicationsPage.confirmDelete();