Repository: ithacaxyz/porto
Pull request #139: feat: add details section to grant admin modal
Author: login: jshsmth
Created: 2025-04-14T02:29:56Z | Merged: 2025-04-15T00:37:43Z


![rem](https://github.com/user-attachments/assets/e289c700-8e2d-4da3-a32e-c4775a70e4de)


Added the more details section to the `grantAdmin` modal 



Diff:
diff --git a/apps/dialog/src/routes/-components/GrantAdmin.tsx b/apps/dialog/src/routes/-components/GrantAdmin.tsx
index 0404a53d6..a9fa8336e 100644
--- a/apps/dialog/src/routes/-components/GrantAdmin.tsx
+++ b/apps/dialog/src/routes/-components/GrantAdmin.tsx
@@ -1,16 +1,78 @@
+import { PortoConfig } from '@porto/apps'
 import { Button } from '@porto/apps/components'
+import { useQuery } from '@tanstack/react-query'
 import { Hex } from 'ox'
+import type * as Address from 'ox/Address'
+import { Key, Relay } from 'porto/internal'
 import { Hooks } from 'porto/remote'
-
+import * as React from 'react'
+import * as Dialog from '~/lib/Dialog'
 import { porto } from '~/lib/Porto'
+import * as Price from '~/lib/Price'
 import { Layout } from '~/routes/-components/Layout'
 import { StringFormatter } from '~/utils'
+import TriangleAlert from '~icons/lucide/triangle-alert'
 import WalletIcon from '~icons/lucide/wallet-cards'
+import { useQuote } from './ActionRequest'
 
 export function GrantAdmin(props: GrantAdmin.Props) {
   const { authorizeKey, loading, onApprove, onReject } = props
 
   const account = Hooks.useAccount(porto)
+  const client = Hooks.useClient(porto)
+  const chain = Hooks.useChain(porto)
+  const origin = Dialog.useStore((state) => state.referrer?.origin)
+
+  const feeToken = React.useMemo(() => {
+    if (props.feeToken) return props.feeToken
+    if (!chain) return undefined
+    return PortoConfig.feeToken[chain.id]
+  }, [chain, props.feeToken])
+
+  const prepareCalls = useQuery({
+    enabled: !!account && !!chain,
+    queryFn: async () => {
+      if (!account || !chain) throw new Error('Account and chain required')
+
+      const adminKey = account.keys?.find(
+        (key) => key.role === 'admin' && key.privateKey,
+      )
+      if (!adminKey) throw new Error('Admin key not found')
+
+      const { context } = await Relay.prepareCalls(client, {
+        account,
+        authorizeKeys: [Key.from(authorizeKey)],
+        feeToken,
+        key: adminKey,
+      })
+
+      return context
+    },
+    queryKey: [
+      'prepareCalls',
+      account?.address,
+      authorizeKey.publicKey,
+      client.uid,
+    ],
+  })
+
+  const quote = useQuote(porto, {
+    chainId: chain?.id,
+    context: prepareCalls.data,
+  })
+
+  const fiatFee = Price.useFiatPrice({
+    value: quote?.fee.native.value,
+  })
+
+  const status = React.useMemo(() => {
+    if (prepareCalls.isPending) return 'pending'
+    if (prepareCalls.isError) return 'error'
+    if (prepareCalls.isSuccess) return 'success'
+    return 'idle'
+  }, [prepareCalls.isError, prepareCalls.isPending, prepareCalls.isSuccess])
+
+  const warning = status === 'error'
 
   return (
     <Layout loading={loading} loadingTitle="Authorizing...">
@@ -22,38 +84,113 @@ export function GrantAdmin(props: GrantAdmin.Props) {
               lost.
             </div>
           }
+          icon={warning ? TriangleAlert : undefined}
           title="Add backup method"
+          variant={warning ? 'warning' : 'default'}
         />
       </Layout.Header>
       <Layout.Content>
-        <div className="flex items-center justify-center rounded-md bg-surface p-2">
-          {account?.address && (
-            <div className="flex items-center gap-2">
-              <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-jade4">
-                <WalletIcon className="h-4 w-4 text-jade9" />
+        <div className="space-y-3">
+          {status === 'error' && (
+            <div className="rounded-lg bg-warningTint px-3 py-2 text-warning">
+              <div className="font-medium text-[14px]">Error</div>
+              <div className="space-y-2 text-[14px] text-primary">
+                <p>
+                  An error occurred while calculating fees. This may be due to
+                  network issues or insufficient funds.
+                </p>
+                <p>
+                  Contact{' '}
+                  <span className="font-medium">{origin?.hostname}</span> if
+                  this issue persists.
+                </p>
               </div>
-              <span className="font-medium font-mono text-base">
-                {StringFormatter.truncate(authorizeKey.publicKey)}
-              </span>
             </div>
           )}
+
+          <div className="flex items-center justify-center rounded-md bg-surface p-2">
+            {account?.address && (
+              <div className="flex items-center gap-2">
+                <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-jade4">
+                  <WalletIcon className="h-4 w-4 text-jade9" />
+                </div>
+                <span className="font-medium font-mono text-base">
+                  {StringFormatter.truncate(authorizeKey.publicKey)}
+                </span>
+              </div>
+            )}
+          </div>
+        </div>
+      </Layout.Content>
+
+      <Layout.Content>
+        <p className="mb-1 text-[14px] text-secondary">More details</p>
+        <div className="space-y-2 rounded-md bg-surface p-3">
+          <div className="flex h-[38px] items-start justify-between">
+            <span className="text-[14px] text-secondary leading-4">
+              Fees (est.)
+            </span>
+            <div className="text-right text-[14px]">
+              {fiatFee.isFetched ? (
+                <>
+                  <div className="font-medium leading-4">
+                    {fiatFee.data?.display}
+                  </div>
+                  {quote?.fee && (
+                    <div>
+                      <span className="text-secondary text-xs">
+                        {quote.fee.display}
+                      </span>
+                    </div>
+                  )}
+                </>
+              ) : (
+                <span className="font-medium text-secondary">Loading...</span>
+              )}
+            </div>
+          </div>
+          <div className="flex items-center justify-between">
+            <span className="text-[14px] text-secondary">Duration (est.)</span>
+            <span className="font-medium text-[14px]">2 seconds</span>
+          </div>
+          <div className="flex items-center justify-between">
+            <span className="text-[14px] text-secondary">Network</span>
+            <span className="font-medium text-[14px]">{chain?.name}</span>
+          </div>
         </div>
       </Layout.Content>
 
       <Layout.Footer>
         <Layout.Footer.Actions>
-          <Button className="flex-1" onClick={onReject} type="button">
-            Cancel
-          </Button>
-
-          <Button
-            className="flex-1"
-            onClick={onApprove}
-            type="button"
-            variant="accent"
-          >
-            Add
-          </Button>
+          {status === 'error' ? (
+            <>
+              <Button className="flex-1" onClick={onReject} type="button">
+                Cancel
+              </Button>
+              <Button
+                className="flex-1"
+                onClick={onApprove}
+                type="button"
+                variant="default"
+              >
+                Attempt anyway
+              </Button>
+            </>
+          ) : (
+            <>
+              <Button className="flex-1" onClick={onReject} type="button">
+                Cancel
+              </Button>
+              <Button
+                className="flex-1"
+                onClick={onApprove}
+                type="button"
+                variant="accent"
+              >
+                Add
+              </Button>
+            </>
+          )}
         </Layout.Footer.Actions>
 
         {account?.address && (
@@ -68,7 +205,9 @@ export declare namespace GrantAdmin {
   type Props = {
     authorizeKey: {
       publicKey: Hex.Hex
+      type: 'address' | 'p256' | 'secp256k1' | 'webauthn-p256'
     }
+    feeToken?: Address.Address | undefined
     loading: boolean
     onApprove: () => void
     onReject: () => void
diff --git a/apps/dialog/src/routes/dialog/experimental_grantAdmin.tsx b/apps/dialog/src/routes/dialog/experimental_grantAdmin.tsx
index 01c863b4a..bade5f30f 100644
--- a/apps/dialog/src/routes/dialog/experimental_grantAdmin.tsx
+++ b/apps/dialog/src/routes/dialog/experimental_grantAdmin.tsx
@@ -29,6 +29,7 @@ function RouteComponent() {
     <div>
       <GrantAdmin
         authorizeKey={parameters.key}
+        feeToken={parameters.capabilities?.feeToken}
         loading={respond.isPending}
         onApprove={() => respond.mutate()}
         onReject={() => Actions.reject(porto, request)}


Reviews:
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T02:43:22Z
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T02:43:30Z
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T02:43:40Z
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T02:43:50Z
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T02:54:22Z
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T03:42:49Z
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T03:43:20Z
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T03:44:43Z
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T03:45:31Z
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T04:27:56Z
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T04:29:54Z
user: login: jxom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T04:30:17Z

Review comments:
diff_hunk: @@ -40,6 +82,33 @@ export function GrantAdmin(props: GrantAdmin.Props) {
         </div>
       </Layout.Content>
 
+      <Layout.Content>
+        <p className="mb-1 text-base text-gray-500">More details</p>
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: ```suggestion
        <p className="mb-1 text-base text-secondary">More details</p>
```
created_at: 2025-04-14T02:43:22Z
author_association: COLLABORATOR
diff_hunk: @@ -40,6 +82,33 @@ export function GrantAdmin(props: GrantAdmin.Props) {
         </div>
       </Layout.Content>
 
+      <Layout.Content>
+        <p className="mb-1 text-base text-gray-500">More details</p>
+        <div className="space-y-2 rounded-md bg-surface p-2">
+          <div className="flex items-center justify-between">
+            <span className="text-gray-500">Fees (est.)</span>
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: ```suggestion
            <span className="text-secondary">Fees (est.)</span>
```
created_at: 2025-04-14T02:43:29Z
author_association: COLLABORATOR
diff_hunk: @@ -40,6 +82,33 @@ export function GrantAdmin(props: GrantAdmin.Props) {
         </div>
       </Layout.Content>
 
+      <Layout.Content>
+        <p className="mb-1 text-base text-gray-500">More details</p>
+        <div className="space-y-2 rounded-md bg-surface p-2">
+          <div className="flex items-center justify-between">
+            <span className="text-gray-500">Fees (est.)</span>
+            <span>
+              {fiatFee.isFetched ? fiatFee.data?.display : 'Loading...'}
+            </span>
+          </div>
+          <div className="flex items-center justify-between">
+            <span className="text-gray-500">Duration (est.)</span>
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: ```suggestion
            <span className="text-secondary">Duration (est.)</span>
```
created_at: 2025-04-14T02:43:40Z
author_association: COLLABORATOR
diff_hunk: @@ -40,6 +82,33 @@ export function GrantAdmin(props: GrantAdmin.Props) {
         </div>
       </Layout.Content>
 
+      <Layout.Content>
+        <p className="mb-1 text-base text-gray-500">More details</p>
+        <div className="space-y-2 rounded-md bg-surface p-2">
+          <div className="flex items-center justify-between">
+            <span className="text-gray-500">Fees (est.)</span>
+            <span>
+              {fiatFee.isFetched ? fiatFee.data?.display : 'Loading...'}
+            </span>
+          </div>
+          <div className="flex items-center justify-between">
+            <span className="text-gray-500">Duration (est.)</span>
+            <span>2 seconds</span>
+          </div>
+          <div className="flex items-center justify-between">
+            <span className="text-gray-500">Network</span>
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: ```suggestion
            <span className="text-secondary">Network</span>
```
created_at: 2025-04-14T02:43:49Z
author_association: COLLABORATOR
diff_hunk: @@ -40,6 +90,46 @@ export function GrantAdmin(props: GrantAdmin.Props) {
         </div>
       </Layout.Content>
 
+      <Layout.Content>
+        <p className="mb-1 text-base text-secondary">More details</p>
+        <div className="space-y-2 rounded-md bg-surface p-2">
+          <div className="flex items-start justify-between">
+            <span className="pt-1 text-secondary">Fees (est.)</span>
+            <div className="text-right">
+              {fiatFee.isFetched ? (
+                <>
+                  <div className="font-medium">{fiatFee.data?.display}</div>
+                  {quote?.fee && (
+                    <div>
+                      <span className="text-secondary text-xs">
+                        {quote.fee.display}
+                      </span>
+                    </div>
+                  )}
+                </>
+              ) : (
+                <span className="font-medium text-secondary">Loading...</span>
+              )}
+            </div>
+          </div>
+          <div className="flex items-center justify-between">
+            <span className="text-secondary">Duration (est.)</span>
+            <span>2 seconds</span>
+          </div>
+          <div className="flex items-center justify-between">
+            <span className="text-secondary">Network</span>
+            <span className="flex items-center gap-1">
+              <img
+                alt={chain?.name}
+                className="h-4 w-4"
+                src={`/icons/${chain?.nativeCurrency.symbol.toLowerCase()}.svg`}
+              />
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: don't worry about this for now

```suggestion
```
created_at: 2025-04-14T02:54:22Z
author_association: COLLABORATOR
diff_hunk: @@ -1,16 +1,66 @@
+import { PortoConfig } from '@porto/apps'
 import { Button } from '@porto/apps/components'
+import { useQuery } from '@tanstack/react-query'
 import { Hex } from 'ox'
+import type * as Address from 'ox/Address'
+import { Key, Relay } from 'porto/internal'
 import { Hooks } from 'porto/remote'
-
+import * as React from 'react'
 import { porto } from '~/lib/Porto'
+import * as Price from '~/lib/Price'
 import { Layout } from '~/routes/-components/Layout'
 import { StringFormatter } from '~/utils'
 import WalletIcon from '~icons/lucide/wallet-cards'
+import { useQuote } from './ActionRequest'
 
 export function GrantAdmin(props: GrantAdmin.Props) {
   const { authorizeKey, loading, onApprove, onReject } = props
 
   const account = Hooks.useAccount(porto)
+  const client = Hooks.useClient(porto)
+  const chain = Hooks.useChain(porto)
+
+  const feeToken = React.useMemo(() => {
+    if (!chain) return undefined
+    const address = PortoConfig.feeToken[chain.id]
+    return address
+  }, [chain])
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: We should also consider the `feeToken` that comes from `experimental_grantAdmin` parameters (`capabilities.feeToken`). Should be passed as a prop to this component.
created_at: 2025-04-14T03:42:49Z
author_association: COLLABORATOR
diff_hunk: @@ -1,16 +1,66 @@
+import { PortoConfig } from '@porto/apps'
 import { Button } from '@porto/apps/components'
+import { useQuery } from '@tanstack/react-query'
 import { Hex } from 'ox'
+import type * as Address from 'ox/Address'
+import { Key, Relay } from 'porto/internal'
 import { Hooks } from 'porto/remote'
-
+import * as React from 'react'
 import { porto } from '~/lib/Porto'
+import * as Price from '~/lib/Price'
 import { Layout } from '~/routes/-components/Layout'
 import { StringFormatter } from '~/utils'
 import WalletIcon from '~icons/lucide/wallet-cards'
+import { useQuote } from './ActionRequest'
 
 export function GrantAdmin(props: GrantAdmin.Props) {
   const { authorizeKey, loading, onApprove, onReject } = props
 
   const account = Hooks.useAccount(porto)
+  const client = Hooks.useClient(porto)
+  const chain = Hooks.useChain(porto)
+
+  const feeToken = React.useMemo(() => {
+    if (!chain) return undefined
+    const address = PortoConfig.feeToken[chain.id]
+    return address
+  }, [chain])
+
+  const prepareCalls = useQuery({
+    enabled: !!account && !!chain,
+    queryFn: async () => {
+      if (!account || !chain) throw new Error('Account and chain required')
+
+      const adminKey = account.keys?.find(
+        (key) => key.role === 'admin' && key.privateKey,
+      )
+      if (!adminKey) throw new Error('Admin key not found')
+
+      const { context } = await Relay.prepareCalls(client, {
+        account,
+        authorizeKeys: [Key.from({ ...authorizeKey })],
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: ```suggestion
        authorizeKeys: [Key.from(authorizeKey)],
```
created_at: 2025-04-14T03:43:20Z
author_association: COLLABORATOR
diff_hunk: @@ -1,16 +1,66 @@
+import { PortoConfig } from '@porto/apps'
 import { Button } from '@porto/apps/components'
+import { useQuery } from '@tanstack/react-query'
 import { Hex } from 'ox'
+import type * as Address from 'ox/Address'
+import { Key, Relay } from 'porto/internal'
 import { Hooks } from 'porto/remote'
-
+import * as React from 'react'
 import { porto } from '~/lib/Porto'
+import * as Price from '~/lib/Price'
 import { Layout } from '~/routes/-components/Layout'
 import { StringFormatter } from '~/utils'
 import WalletIcon from '~icons/lucide/wallet-cards'
+import { useQuote } from './ActionRequest'
 
 export function GrantAdmin(props: GrantAdmin.Props) {
   const { authorizeKey, loading, onApprove, onReject } = props
 
   const account = Hooks.useAccount(porto)
+  const client = Hooks.useClient(porto)
+  const chain = Hooks.useChain(porto)
+
+  const feeToken = React.useMemo(() => {
+    if (!chain) return undefined
+    const address = PortoConfig.feeToken[chain.id]
+    return address
+  }, [chain])
+
+  const prepareCalls = useQuery({
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: Let's try handle the failure case here too. If this call fails, then we should show an error message in the UI and have "Attempt anyway" like Action Requests.
created_at: 2025-04-14T03:44:43Z
author_association: COLLABORATOR
diff_hunk: @@ -1,16 +1,66 @@
+import { PortoConfig } from '@porto/apps'
 import { Button } from '@porto/apps/components'
+import { useQuery } from '@tanstack/react-query'
 import { Hex } from 'ox'
+import type * as Address from 'ox/Address'
+import { Key, Relay } from 'porto/internal'
 import { Hooks } from 'porto/remote'
-
+import * as React from 'react'
 import { porto } from '~/lib/Porto'
+import * as Price from '~/lib/Price'
 import { Layout } from '~/routes/-components/Layout'
 import { StringFormatter } from '~/utils'
 import WalletIcon from '~icons/lucide/wallet-cards'
+import { useQuote } from './ActionRequest'
 
 export function GrantAdmin(props: GrantAdmin.Props) {
   const { authorizeKey, loading, onApprove, onReject } = props
 
   const account = Hooks.useAccount(porto)
+  const client = Hooks.useClient(porto)
+  const chain = Hooks.useChain(porto)
+
+  const feeToken = React.useMemo(() => {
+    if (!chain) return undefined
+    const address = PortoConfig.feeToken[chain.id]
+    return address
+  }, [chain])
+
+  const prepareCalls = useQuery({
+    enabled: !!account && !!chain,
+    queryFn: async () => {
+      if (!account || !chain) throw new Error('Account and chain required')
+
+      const adminKey = account.keys?.find(
+        (key) => key.role === 'admin' && key.privateKey,
+      )
+      if (!adminKey) throw new Error('Admin key not found')
+
+      const { context } = await Relay.prepareCalls(client, {
+        account,
+        authorizeKeys: [Key.from({ ...authorizeKey })],
+        feeToken,
+        key: adminKey,
+      })
+
+      return context
+    },
+    queryKey: [
+      'prepareCalls',
+      'grantAdmin',
+      account?.address,
+      authorizeKey.publicKey,
+    ],
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: ```suggestion
    queryKey: [
      'prepareCalls',
      account?.address,
      authorizeKey.publicKey,
      client.uid
    ],
```
created_at: 2025-04-14T03:45:31Z
author_association: COLLABORATOR
diff_hunk: @@ -1,16 +1,84 @@
+import { PortoConfig } from '@porto/apps'
 import { Button } from '@porto/apps/components'
+import { useQuery } from '@tanstack/react-query'
 import { Hex } from 'ox'
+import type * as Address from 'ox/Address'
+import { Key, Relay } from 'porto/internal'
 import { Hooks } from 'porto/remote'
-
+import * as React from 'react'
+import * as Dialog from '~/lib/Dialog'
 import { porto } from '~/lib/Porto'
+import * as Price from '~/lib/Price'
 import { Layout } from '~/routes/-components/Layout'
 import { StringFormatter } from '~/utils'
+import TriangleAlert from '~icons/lucide/triangle-alert'
 import WalletIcon from '~icons/lucide/wallet-cards'
+import { useQuote } from './ActionRequest'
 
 export function GrantAdmin(props: GrantAdmin.Props) {
-  const { authorizeKey, loading, onApprove, onReject } = props
+  const {
+    authorizeKey,
+    loading,
+    onApprove,
+    onReject,
+    feeToken: propFeeToken,
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: ```suggestion
```
created_at: 2025-04-14T04:26:58Z
author_association: COLLABORATOR
diff_hunk: @@ -1,16 +1,84 @@
+import { PortoConfig } from '@porto/apps'
 import { Button } from '@porto/apps/components'
+import { useQuery } from '@tanstack/react-query'
 import { Hex } from 'ox'
+import type * as Address from 'ox/Address'
+import { Key, Relay } from 'porto/internal'
 import { Hooks } from 'porto/remote'
-
+import * as React from 'react'
+import * as Dialog from '~/lib/Dialog'
 import { porto } from '~/lib/Porto'
+import * as Price from '~/lib/Price'
 import { Layout } from '~/routes/-components/Layout'
 import { StringFormatter } from '~/utils'
+import TriangleAlert from '~icons/lucide/triangle-alert'
 import WalletIcon from '~icons/lucide/wallet-cards'
+import { useQuote } from './ActionRequest'
 
 export function GrantAdmin(props: GrantAdmin.Props) {
-  const { authorizeKey, loading, onApprove, onReject } = props
+  const {
+    authorizeKey,
+    loading,
+    onApprove,
+    onReject,
+    feeToken: propFeeToken,
+  } = props
 
   const account = Hooks.useAccount(porto)
+  const client = Hooks.useClient(porto)
+  const chain = Hooks.useChain(porto)
+  const origin = Dialog.useStore((state) => state.referrer?.origin)
+
+  const feeToken = React.useMemo(() => {
+    if (propFeeToken) return propFeeToken
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: ```suggestion
    if (props.feeToken) return props.feeToken
```
created_at: 2025-04-14T04:27:34Z
author_association: COLLABORATOR
diff_hunk: @@ -22,38 +89,114 @@ export function GrantAdmin(props: GrantAdmin.Props) {
               lost.
             </div>
           }
+          icon={warning ? TriangleAlert : undefined}
           title="Add backup method"
+          variant={warning ? 'warning' : 'default'}
         />
       </Layout.Header>
       <Layout.Content>
-        <div className="flex items-center justify-center rounded-md bg-surface p-2">
-          {account?.address && (
-            <div className="flex items-center gap-2">
-              <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-jade4">
-                <WalletIcon className="h-4 w-4 text-jade9" />
+        <div className="space-y-3">
+          {status === 'error' && (
+            <div className="rounded-lg bg-warningTint px-3 py-2 text-warning">
+              <div className="font-medium text-[14px]">Error</div>
+              <div className="space-y-2 text-[14px] text-primary">
+                <p>
+                  An error occurred while preparing the action. Proceed with
+                  caution.
+                </p>
+                <p>
+                  Contact{' '}
+                  <span className="font-medium">{origin?.hostname}</span> for
+                  more information.
+                </p>
               </div>
-              <span className="font-medium font-mono text-base">
-                {StringFormatter.truncate(authorizeKey.publicKey)}
-              </span>
             </div>
           )}
+
+          <div className="flex items-center justify-center rounded-md bg-surface p-2">
+            {account?.address && (
+              <div className="flex items-center gap-2">
+                <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-jade4">
+                  <WalletIcon className="h-4 w-4 text-jade9" />
+                </div>
+                <span className="font-medium font-mono text-base">
+                  {StringFormatter.truncate(authorizeKey.publicKey)}
+                </span>
+              </div>
+            )}
+          </div>
+        </div>
+      </Layout.Content>
+
+      <Layout.Content>
+        <p className="mb-1 text-base text-secondary">More details</p>
+        <div className="space-y-2 rounded-md bg-surface p-2">
+          <div className="flex items-start justify-between">
+            <span className="pt-1 text-secondary">Fees (est.)</span>
+            <div className="text-right">
+              {fiatFee.isFetched ? (
+                <>
+                  <div className="font-medium">{fiatFee.data?.display}</div>
+                  {quote?.fee && (
+                    <div>
+                      <span className="text-secondary text-xs">
+                        {quote.fee.display}
+                      </span>
+                    </div>
+                  )}
+                </>
+              ) : (
+                <span className="font-medium text-secondary">Loading...</span>
+              )}
+            </div>
+          </div>
+          <div className="flex items-center justify-between">
+            <span className="text-secondary">Duration (est.)</span>
+            <span>2 seconds</span>
+          </div>
+          <div className="flex items-center justify-between">
+            <span className="text-secondary">Network</span>
+            <span className="flex items-center gap-1">{chain?.name}</span>
+          </div>
         </div>
       </Layout.Content>
 
       <Layout.Footer>
         <Layout.Footer.Actions>
-          <Button className="flex-1" onClick={onReject} type="button">
-            Cancel
-          </Button>
-
-          <Button
-            className="flex-1"
-            onClick={onApprove}
-            type="button"
-            variant="accent"
-          >
-            Add
-          </Button>
+          {status === 'error' ? (
+            <>
+              <Button
+                className="flex-1"
+                onClick={onReject}
+                type="button"
+                variant="destructive"
+              >
+                Deny
+              </Button>
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: ```suggestion
              <Button className="flex-1" onClick={onReject} type="button">
                Cancel
              </Button>
```
created_at: 2025-04-14T04:29:54Z
author_association: COLLABORATOR
diff_hunk: @@ -22,38 +89,114 @@ export function GrantAdmin(props: GrantAdmin.Props) {
               lost.
             </div>
           }
+          icon={warning ? TriangleAlert : undefined}
           title="Add backup method"
+          variant={warning ? 'warning' : 'default'}
         />
       </Layout.Header>
       <Layout.Content>
-        <div className="flex items-center justify-center rounded-md bg-surface p-2">
-          {account?.address && (
-            <div className="flex items-center gap-2">
-              <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-jade4">
-                <WalletIcon className="h-4 w-4 text-jade9" />
+        <div className="space-y-3">
+          {status === 'error' && (
+            <div className="rounded-lg bg-warningTint px-3 py-2 text-warning">
+              <div className="font-medium text-[14px]">Error</div>
+              <div className="space-y-2 text-[14px] text-primary">
+                <p>
+                  An error occurred while preparing the action. Proceed with
+                  caution.
+                </p>
+                <p>
+                  Contact{' '}
+                  <span className="font-medium">{origin?.hostname}</span> for
+                  more information.
+                </p>
               </div>
-              <span className="font-medium font-mono text-base">
-                {StringFormatter.truncate(authorizeKey.publicKey)}
-              </span>
             </div>
           )}
+
+          <div className="flex items-center justify-center rounded-md bg-surface p-2">
+            {account?.address && (
+              <div className="flex items-center gap-2">
+                <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-jade4">
+                  <WalletIcon className="h-4 w-4 text-jade9" />
+                </div>
+                <span className="font-medium font-mono text-base">
+                  {StringFormatter.truncate(authorizeKey.publicKey)}
+                </span>
+              </div>
+            )}
+          </div>
+        </div>
+      </Layout.Content>
+
+      <Layout.Content>
+        <p className="mb-1 text-base text-secondary">More details</p>
+        <div className="space-y-2 rounded-md bg-surface p-2">
+          <div className="flex items-start justify-between">
+            <span className="pt-1 text-secondary">Fees (est.)</span>
+            <div className="text-right">
+              {fiatFee.isFetched ? (
+                <>
+                  <div className="font-medium">{fiatFee.data?.display}</div>
+                  {quote?.fee && (
+                    <div>
+                      <span className="text-secondary text-xs">
+                        {quote.fee.display}
+                      </span>
+                    </div>
+                  )}
+                </>
+              ) : (
+                <span className="font-medium text-secondary">Loading...</span>
+              )}
+            </div>
+          </div>
+          <div className="flex items-center justify-between">
+            <span className="text-secondary">Duration (est.)</span>
+            <span>2 seconds</span>
+          </div>
+          <div className="flex items-center justify-between">
+            <span className="text-secondary">Network</span>
+            <span className="flex items-center gap-1">{chain?.name}</span>
+          </div>
         </div>
       </Layout.Content>
 
       <Layout.Footer>
         <Layout.Footer.Actions>
-          <Button className="flex-1" onClick={onReject} type="button">
-            Cancel
-          </Button>
-
-          <Button
-            className="flex-1"
-            onClick={onApprove}
-            type="button"
-            variant="accent"
-          >
-            Add
-          </Button>
+          {status === 'error' ? (
+            <>
+              <Button
+                className="flex-1"
+                onClick={onReject}
+                type="button"
+                variant="destructive"
+              >
+                Deny
+              </Button>
+              <Button
+                className="flex-1"
+                onClick={onApprove}
+                type="button"
+                variant="default"
+              >
+                Approve anyway
path: apps/dialog/src/routes/-components/GrantAdmin.tsx
user: login: jxom
body: ```suggestion
                Attempt anyway
```
created_at: 2025-04-14T04:30:17Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-14T02:30:00Z
body: ###  ⚠️  No Changeset found

Latest commit: 0c1c545323e22550857725d66fe4f9e28e88050e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/jshsmth/porto/new/feat/add-details-section?filename=.changeset/metal-carrots-love.md&value=---%0A%22dialog%22%3A%20patch%0A---%0A%0Afeat%3A%20add%20details%20section%20to%20grant%20admin%20modal%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-14T02:30:05Z
body: [vc]: #8oNZQ3MTfP/2ck/O/8Xmg9jCzGRmhTYyM96IPO2+CYc=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJpZC5wb3J0byIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3Byb3h5IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2l0aGFjYXh5ei9pZC5wb3J0by9FYUs5dXBZUW1DOFJ2azREbW1UR1N1VEF1SzdqIiwicHJldmlld1VybCI6ImlkcG9ydG8tZ2l0LWZvcmstanNoc210aC1mZWF0LWFkZC1kZXRhaWxzLXNlY3Rpb24taXRoYWNheHl6LnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiaWRwb3J0by1naXQtZm9yay1qc2hzbXRoLWZlYXQtYWRkLWRldGFpbHMtc2VjdGlvbi1pdGhhY2F4eXoudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJwbGF5Z3JvdW5kLnBvcnRvIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvcGxheWdyb3VuZCIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9pdGhhY2F4eXovcGxheWdyb3VuZC5wb3J0by9IY1Q1cENVRVJhcG54NW9IVWtFOTJyTlR2Vk1zIiwicHJldmlld1VybCI6InBsYXlncm91bmRwb3J0by1naXQtZm9yay1qc2hzbXRoLWZlYXQtYWRkLWRldGEtYjcwNmM4LWl0aGFjYXh5ei52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InBsYXlncm91bmRwb3J0by1naXQtZm9yay1qc2hzbXRoLWZlYXQtYWRkLWRldGEtYjcwNmM4LWl0aGFjYXh5ei52ZXJjZWwuYXBwIn19LHsibmFtZSI6ImRvY3MucG9ydG8iLCJyb290RGlyZWN0b3J5IjoiYXBwcy9kb2NzIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2l0aGFjYXh5ei9kb2NzLnBvcnRvLzEzOXRNRGJiU3lBQlhBanZxMk41RXU2M253TXoiLCJwcmV2aWV3VXJsIjoiZG9jc3BvcnRvLWdpdC1mb3JrLWpzaHNtdGgtZmVhdC1hZGQtZGV0YWlscy1zZWN0aW9uLWl0aGFjYXh5ei52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImRvY3Nwb3J0by1naXQtZm9yay1qc2hzbXRoLWZlYXQtYWRkLWRldGFpbHMtc2VjdGlvbi1pdGhhY2F4eXoudmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **docs.porto** | ✅ Ready ([Inspect](https://vercel.com/ithacaxyz/docs.porto/139tMDbbSyABXAjvq2N5Eu63nwMz)) | [Visit Preview](https://docsporto-git-fork-jshsmth-feat-add-details-section-ithacaxyz.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/docsporto-git-fork-jshsmth-feat-add-details-section-ithacaxyz.vercel.app?via=pr-comment-feedback-link) | Apr 15, 2025 0:44am |
| **id.porto** | ✅ Ready ([Inspect](https://vercel.com/ithacaxyz/id.porto/EaK9upYQmC8Rvk4DmmTGSuTAuK7j)) | [Visit Preview](https://idporto-git-fork-jshsmth-feat-add-details-section-ithacaxyz.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/idporto-git-fork-jshsmth-feat-add-details-section-ithacaxyz.vercel.app?via=pr-comment-feedback-link) | Apr 15, 2025 0:44am |
| **playground.porto** | ✅ Ready ([Inspect](https://vercel.com/ithacaxyz/playground.porto/HcT5pCUERapnx5oHUkE92rNTvVMs)) | [Visit Preview](https://playgroundporto-git-fork-jshsmth-feat-add-deta-b706c8-ithacaxyz.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/playgroundporto-git-fork-jshsmth-feat-add-deta-b706c8-ithacaxyz.vercel.app?via=pr-comment-feedback-link) | Apr 15, 2025 0:44am |




author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-14T02:30:07Z
body: @jshsmth is attempting to deploy a commit to the **Ithaca** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=Ithaca&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%225dcda2c84a5aeeb368e37b8b8135b51de8b023f9%22%7D%2C%22id%22%3A%22QmS6j9t7FfG7XSdwmWuPuqBDpeiJ6nosj6sNFt1c1oDU8m%22%2C%22org%22%3A%22ithacaxyz%22%2C%22prId%22%3A139%2C%22repo%22%3A%22porto%22%7D).


author_association: NONE