Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mintlify/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"group": "Overview",
"pages": [
"global-accounts/index",
"global-accounts/demo",
"global-accounts/implementation-overview"
]
},
Expand Down
12 changes: 12 additions & 0 deletions mintlify/global-accounts/demo.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: "Playground"
description: "Create a Grid Global Account and watch the API calls fire in real time"
icon: "/images/icons/phone.svg"
"og:image": "/images/og/og-global-accounts.webp"
---

import { WalletDemoEmbed } from '/snippets/global-accounts/wallet-demo-embed.mdx';

<div id="wallet-demo-container"></div>

<WalletDemoEmbed />
3 changes: 3 additions & 0 deletions mintlify/images/icons/phone.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
87 changes: 87 additions & 0 deletions mintlify/snippets/global-accounts/wallet-demo-embed.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
export const WalletDemoEmbed = () => {
React.useEffect(() => {
Comment on lines +1 to +2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 partner React.useEffect is called directly on the React namespace, but React is never imported in this file. Mintlify's MDX pipeline uses the automatic JSX transform which does not inject React as a named global — only JSX pragma calls are shielded. If this runs in an environment where React is not globally polyfilled, the component will throw ReferenceError: React is not defined on mount. Destructure useEffect explicitly to be safe across all Mintlify versions.

Suggested change
export const WalletDemoEmbed = () => {
React.useEffect(() => {
import { useEffect } from 'react';
export const WalletDemoEmbed = () => {
useEffect(() => {
Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/snippets/global-accounts/wallet-demo-embed.mdx
Line: 1-2

Comment:
`React.useEffect` is called directly on the `React` namespace, but `React` is never imported in this file. Mintlify's MDX pipeline uses the automatic JSX transform which does not inject `React` as a named global — only JSX pragma calls are shielded. If this runs in an environment where `React` is not globally polyfilled, the component will throw `ReferenceError: React is not defined` on mount. Destructure `useEffect` explicitly to be safe across all Mintlify versions.

```suggestion
import { useEffect } from 'react';
export const WalletDemoEmbed = () => {
  useEffect(() => {
```

How can I resolve this? If you propose a fix, please make it concise.

const isLocal = ['localhost', '127.0.0.1'].includes(window.location.hostname);
const base = isLocal ? 'http://localhost:4000' : 'https://grid-wallet-demo.vercel.app';
const isDark = () => document.documentElement.classList.contains('dark');

let host = document.getElementById('wallet-demo-host');
if (!host) {
host = document.createElement('div');
host.id = 'wallet-demo-host';
host.style.cssText = 'position:fixed;z-index:1;overflow:hidden;';
const iframe = document.createElement('iframe');
iframe.id = 'wallet-demo-iframe';
iframe.src = base + '/?embed=true&theme=' + (isDark() ? 'dark' : 'light');
iframe.title = 'Grid Global Accounts — live demo';
iframe.setAttribute('allow', 'publickey-credentials-create *; publickey-credentials-get *; clipboard-write');
iframe.style.cssText = 'width:100%;height:100%;border:0;display:block;';
Comment on lines +10 to +17

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 security No sandbox attribute on a third-party iframe

The iframe embeds an external Vercel app (grid-wallet-demo.vercel.app) with no sandbox restrictions, giving it full access to window.opener, top-level navigation, form submission to any target, and more. Adding sandbox="allow-scripts allow-same-origin allow-forms allow-popups" (plus allow-popups-to-escape-sandbox if the app opens OAuth windows) would limit the blast radius if the hosted demo is ever compromised or replaced. Passkey allow directives work independently of sandbox and can be left as-is.

Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/snippets/global-accounts/wallet-demo-embed.mdx
Line: 10-17

Comment:
**No `sandbox` attribute on a third-party iframe**

The iframe embeds an external Vercel app (`grid-wallet-demo.vercel.app`) with no `sandbox` restrictions, giving it full access to `window.opener`, top-level navigation, form submission to any target, and more. Adding `sandbox="allow-scripts allow-same-origin allow-forms allow-popups"` (plus `allow-popups-to-escape-sandbox` if the app opens OAuth windows) would limit the blast radius if the hosted demo is ever compromised or replaced. Passkey `allow` directives work independently of sandbox and can be left as-is.

How can I resolve this? If you propose a fix, please make it concise.

host.appendChild(iframe);
document.body.appendChild(host);
}
if (window.__wdHideTimer) { clearTimeout(window.__wdHideTimer); window.__wdHideTimer = null; }
host.style.display = 'block';

let raf = 0;
let last = { left: -1, top: -1, width: -1, height: -1 };
const sync = () => {
const el = document.getElementById('wallet-demo-container');
if (el && host) {
const r = el.getBoundingClientRect();
if (r.left !== last.left || r.top !== last.top || r.width !== last.width || r.height !== last.height) {
host.style.left = r.left + 'px';
host.style.top = r.top + 'px';
host.style.width = r.width + 'px';
host.style.height = r.height + 'px';
last = { left: r.left, top: r.top, width: r.width, height: r.height };
}
}
raf = requestAnimationFrame(sync);
};
Comment on lines +25 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Unbounded requestAnimationFrame loop runs at ~60 fps while demo is visible

The sync function schedules itself unconditionally with requestAnimationFrame on every frame, creating a loop that runs continuously at ~60 fps the entire time the demo page is open. While the change-guard (r.left !== last.left || …) avoids style mutations when nothing moves, the RAF callback itself still fires and reads getBoundingClientRect() (which forces layout) every frame. On a static layout this is a steady tax on the main thread. Consider using a ResizeObserver plus a scroll event listener instead, which fire only on actual geometry changes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/snippets/global-accounts/wallet-demo-embed.mdx
Line: 25-39

Comment:
**Unbounded `requestAnimationFrame` loop runs at ~60 fps while demo is visible**

The `sync` function schedules itself unconditionally with `requestAnimationFrame` on every frame, creating a loop that runs continuously at ~60 fps the entire time the demo page is open. While the change-guard (`r.left !== last.left || …`) avoids style mutations when nothing moves, the RAF callback itself still fires and reads `getBoundingClientRect()` (which forces layout) every frame. On a static layout this is a steady tax on the main thread. Consider using a `ResizeObserver` plus a `scroll` event listener instead, which fire only on actual geometry changes.

How can I resolve this? If you propose a fix, please make it concise.

raf = requestAnimationFrame(sync);

let ignoreNextMutation = false;
const sendTheme = () => {
const t = isDark() ? 'dark' : 'light';
const iframe = document.getElementById('wallet-demo-iframe');
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage({ type: 'theme-sync', theme: t }, '*');
}
};

const handleMessage = (e) => {
if (e.data && e.data.type === 'theme-request') {
sendTheme();
return;
}
if (e.data && e.data.type === 'theme-sync') {
const wantsDark = e.data.theme === 'dark';
if (isDark() !== wantsDark) {
ignoreNextMutation = true;
document.documentElement.classList.toggle('dark');
}
}
};
Comment on lines +50 to +63

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Unvalidated postMessage origin allows any page to manipulate theme

The handleMessage handler responds to theme-sync and theme-request messages from any window without checking e.origin. This means any third-party site can send { type: 'theme-sync', theme: 'dark' } and silently toggle the docs page's dark/light mode class on document.documentElement. In a cross-site scenario (e.g., a user opens a malicious link that sends a postMessage to this page), the docs UI appearance can be flipped without user consent. At minimum, validate that e.origin matches base before acting on messages.

Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/snippets/global-accounts/wallet-demo-embed.mdx
Line: 50-63

Comment:
**Unvalidated postMessage origin allows any page to manipulate theme**

The `handleMessage` handler responds to `theme-sync` and `theme-request` messages from any window without checking `e.origin`. This means any third-party site can send `{ type: 'theme-sync', theme: 'dark' }` and silently toggle the docs page's dark/light mode class on `document.documentElement`. In a cross-site scenario (e.g., a user opens a malicious link that sends a postMessage to this page), the docs UI appearance can be flipped without user consent. At minimum, validate that `e.origin` matches `base` before acting on messages.

How can I resolve this? If you propose a fix, please make it concise.

window.addEventListener('message', handleMessage);

const obs = new MutationObserver(() => {
if (ignoreNextMutation) { ignoreNextMutation = false; return; }
sendTheme();
});
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });

sendTheme();
requestAnimationFrame(sendTheme);

return () => {
cancelAnimationFrame(raf);
window.removeEventListener('message', handleMessage);
obs.disconnect();
window.__wdHideTimer = setTimeout(() => {
const h = document.getElementById('wallet-demo-host');
if (h) h.style.display = 'none';
}, 150);
};
}, []);

return null;
};
131 changes: 131 additions & 0 deletions mintlify/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -4076,3 +4076,134 @@ html.dark:has(#flow-builder-container) #navbar button[class*="h-14"][class*="tex
color: var(--ls-gray-700);
}


/* ===========================================
Wallet demo page (global-accounts/demo)
Docs sidebar stays; content column is iframe-only, edge-to-edge.
=========================================== */

#content-container:has(#wallet-demo-container) {
max-width: none !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
}

/* Mintlify wraps content in max-w-5xl (1024px) — bust it for the demo */
#content-container:has(#wallet-demo-container) > div.max-w-5xl,
#content-container:has(#wallet-demo-container) > div[class*="max-w-5xl"] {
max-width: none !important;
width: 100% !important;
margin-left: 0 !important;
margin-right: 0 !important;
gap: 0 !important;
}

#content-area:has(#wallet-demo-container) {
width: 100% !important;
flex: 1 1 auto !important;
min-width: 0 !important;
contain: none !important;
display: flex !important;
flex-direction: column !important;
padding: 0 !important;
overflow: hidden !important;
/* navbar (64px bar + 48px tabs = 112px) */
min-height: calc(100dvh - 112px) !important;
}

/* Hide everything in the content column except the embed */
#content-area:has(#wallet-demo-container) > header#header,
#content-area:has(#wallet-demo-container) > #pagination,
#content-area:has(#wallet-demo-container) > footer#footer,
#content-area:has(#wallet-demo-container) > div.sticky {
display: none !important;
Comment on lines +4108 to +4120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Hardcoded navbar offsets (112px / 120px) are fragile

The 112px value (commented as "64px bar + 48px tabs") and the responsive 120px are baked into three CSS rules and the JS sync loop indirectly (the container height drives the iframe height). If Mintlify changes its navbar or tab strip heights in a future update, the iframe will be clipped or leave a gap without any obvious error. Consider driving these via a CSS custom property (--demo-navbar-offset: 112px) defined in one place so adjustments require a single change.

Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/style.css
Line: 4108-4120

Comment:
**Hardcoded navbar offsets (`112px` / `120px`) are fragile**

The `112px` value (commented as "64px bar + 48px tabs") and the responsive `120px` are baked into three CSS rules and the JS `sync` loop indirectly (the container height drives the iframe height). If Mintlify changes its navbar or tab strip heights in a future update, the iframe will be clipped or leave a gap without any obvious error. Consider driving these via a CSS custom property (`--demo-navbar-offset: 112px`) defined in one place so adjustments require a single change.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

height: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
visibility: hidden !important;
}

#content-container:has(#wallet-demo-container) #content-side-layout {
display: none !important;
}

#content-area:has(#wallet-demo-container) > #content {
flex: 1 1 auto !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
max-width: none !important;
min-height: 0 !important;
contain: none !important;
}

#content:has(#wallet-demo-container) .prose,
.prose:has(#wallet-demo-container) {
max-width: none !important;
margin: 0 !important;
padding: 0 !important;
}

/* Placeholder that defines the embed's box. The actual iframe lives in
#wallet-demo-host on <body> (so theme toggles don't reload it) and is glued
to this box's position by the embed script. */
#wallet-demo-container {
width: 100%;
height: calc(100dvh - 112px);
margin: 0;
padding: 0;
line-height: 0;
background: var(--ls-gray-100);
}

html.dark #wallet-demo-container {
background: var(--ls-gray-975);
}

/* Persistent iframe host — body-mounted, positioned over the placeholder. */
#wallet-demo-host {
background: var(--ls-gray-100);
}

html.dark #wallet-demo-host {
background: var(--ls-gray-975);
}

#wallet-demo-iframe {
width: 100% !important;
height: 100% !important;
min-height: 0 !important;
border: none !important;
display: block !important;
aspect-ratio: unset !important;
}

/* Flatten Mintlify's iframe aspect-ratio wrapper */
#wallet-demo-container > div,
#wallet-demo-container > [style*="aspect-ratio"] {
aspect-ratio: unset !important;
width: 100% !important;
height: 100% !important;
max-width: none !important;
margin: 0 !important;
padding: 0 !important;
}

@media (max-width: 1023px) {
#content-area:has(#wallet-demo-container) {
min-height: calc(100dvh - 120px) !important;
}

#wallet-demo-container {
height: calc(100dvh - 120px);
}
}

/* Prevent outer page scroll when the wallet demo is present */
html:has(#wallet-demo-container),
html:has(#wallet-demo-container) body {
overflow: hidden !important;
}

Loading