Skip to content

feat: VeChain whitelabel cross-app connect host#620

Merged
Agilulfo1820 merged 106 commits into
mainfrom
feat/cross-app-connect-whitelabel
May 19, 2026
Merged

feat: VeChain whitelabel cross-app connect host#620
Agilulfo1820 merged 106 commits into
mainfrom
feat/cross-app-connect-whitelabel

Conversation

@Agilulfo1820
Copy link
Copy Markdown
Member

@Agilulfo1820 Agilulfo1820 commented May 15, 2026

Summary

Introduces a whitelabel host for Privy's cross-app connect & transact flows under cross-app-connect/, and rewires the kit so social logins (Google, Apple, X, Discord, GitHub, TikTok, LINE) work without the consuming dApp owning a Privy account. The popup runs on VeChain branding, decodes transactions into plain language, and is fully translated into the kit's 17 languages.

Net effect for dApps integrating the kit: drop { method: 'google' } (or apple, twitter, …) in loginMethods with no privy prop, ship social login. Users get one VeChain identity that follows them across every kit-integrated dApp.

What's in this PR

cross-app-connect/ — new package

A Next.js 16 static export that serves the two popup routes:

  • /cross-app/connect — first-time and returning login; resolves the user via Privy OAuth/SMS and posts PRIVY_CROSS_APP_CONNECT_RESPONSE back to the opener.
  • /cross-app/transact — signing surface; decrypts the request, decodes calldata into a plain-language summary, signs, posts PRIVY_CROSS_APP_ACTION_RESPONSE.

Key features:

  • VeChain branding end-to-end — logo, palette, dark/light, no Privy chrome.
  • Known-action decoder — hand-written detectors for ERC-20 transfer/approve, VeChain domain ops, VeBetterDAO governance (vote / endorse / allocate), voter & allocation rewards, B3TR↔VOT3 conversion, NFT transfers, DEX swaps via BetterSwap / VeTrade. Unknown contracts surface an "Unverified" warning.
  • App Hub lookupvechain/app-hub manifest cached at build time so we render Confirm token swap on Nubila instead of a raw URL.
  • 17 languages matching the kit (en, de, it, fr, es, zh, ja, ru, ro, vi, nl, ko, sv, tw, tr, hi, pt), detected from navigator.language.
  • Stale-connection recovery — when decrypt fails (TTL expired, account mismatch, first-time visitor), the popup shows a clear "Reconnection needed" screen and posts a PRIVY_CROSS_APP_ACTION_ERROR with a vk:cross-app-no-connection marker on close. The kit catches it and auto-runs logout + reopen-connect-modal.
  • Headless login — drives Privy's useLoginWithOAuth and useLoginWithSms hooks behind our own picker. Users never see Privy's modal, even on session expiry inside /transact.
  • Recent-provider hint — last-used OAuth provider + last identity stored locally so the picker pre-highlights it and the "session expired" screen greets the user by name.

Stack rationale (Chakra / TanStack Query / vechain-kit dropped in favor of CSS Modules + direct VeChain SDK): spelled out in cross-app-connect/README.md. TL;DR: bundle weight, first-paint latency, no data layer to manage on a single-decision surface.

packages/vechain-kit/ — wiring changes

  • useLoginWithOAuth — when the consumer doesn't pass a privy prop, routes Google/Apple/Twitter/Discord/GitHub/TikTok/LINE through the whitelabel popup via the cross-app intent param (?intent=google etc.) instead of throwing.
  • PrivyCrossAppProviderappendIntent helper widens the connect URL with the pre-selected provider so the popup skips its picker and opens straight to OAuth. Also intercepts stale-connection errors on signTypedData/signMessage and re-dispatches them as the recovery event.
  • CrossAppErrorRecovery — render-less component mounted inside ModalProvider that listens for vk:cross-app-no-connection messages and runs useWallet().disconnect() + openConnectModal() automatically.
  • VECHAIN_PRIVY_APP_ID fallback — when no privy prop is supplied, the kit's PrivyProvider now boots with VeChain's app ID so the embedded Privy SDK can authenticate against the same account the popup uses.
  • Login-method buttons{ method: 'google' }, 'apple', 'twitter', 'discord', 'github', 'tiktok', 'line' render even when privy isn't set (previously hidden / threw).

CI & deploy

  • .github/workflows/cross-app-connect-check.yamlpull_request_target with the existing safe-to-build label gate, scoped to cross-app-connect/**, packages/vechain-kit/**, yarn.lock. Runs yarn workspace cross-app-connect typecheck + build.
  • .github/workflows/cross-app-connect-deploy.yamlpush to main and workflow_dispatch. Static-builds the popup, adds .nojekyll, uploads via actions/upload-pages-artifact, deploys via actions/deploy-pages. concurrency: pages queues instead of cancelling. Pages/id-token permissions scoped to the deploy job only (zizmor-clean).
  • .github/workflows/cross-app-connect-refresh-app-hub.yaml — daily cron at 04:13 UTC (plus workflow_dispatch). Runs yarn generate-app-hub against vechain/app-hub; if the resulting app-hub.json differs from the committed copy, opens (or updates) a single chore PR on chore/refresh-app-hub-cache. Keeps the popup's "Confirm token swap on Nubila" lookup current without manual regeneration.

Deployment target: GitHub Pages with a custom domain (e.g. connect.vechain.org) configured later. See the deploy checklist and the inline notes in the deploy workflow.

Repo-level docs

  • Main README.md updated to list the no-Privy social providers and link to the popup README.
  • cross-app-connect/README.md added — full architecture, rationale, dev setup, deploy notes.

Associated PRs

Out-of-repo docs need to be kept in sync with the new reality (Privy is no longer "required" for social login):

Test plan

  • Local dev: yarn dev:cross-app-connect boots both kit watch and popup on :3001
  • Open a kit-using example app, click "Continue with VeChain" → popup opens to picker → pick Google → returns address
  • Same app, click { method: 'google' } direct button → popup opens straight to Google OAuth (intent param honored)
  • Sign a transaction: popup shows decoded plain-language summary, verified-contract chips, correct subtitle
  • Trigger stale-connection: clear privy-caw:*:connection from popup localStorage between two sign-ins → popup shows "Reconnection needed" → click Close → kit auto-logs-out and reopens the connect modal
  • Switch browser language to Italian → popup renders Italian copy; same for the other 15 locales
  • Dark mode: pre-paint script flips before React mounts, no flash
  • CI: cross-app-connect-check.yaml runs on a test PR; type-check + build pass
  • CI: merge to main triggers cross-app-connect-deploy.yaml; artifact uploads; GH Pages serves it
  • CI: trigger cross-app-connect-refresh-app-hub.yaml via workflow_dispatch on a copy of main with the cache deleted; verify it opens a PR with the regenerated file

One-time GitHub setup after merge

  • Settings → Pages → Source: GitHub Actions
  • Settings → Variables: NEXT_PUBLIC_PRIVY_APP_ID, NEXT_PUBLIC_PRIVY_CLIENT_ID, NEXT_PUBLIC_PRIVY_DOMAIN
  • (Pre-custom-domain) Settings → Variables: NEXT_PUBLIC_BASE_PATH=/vechain-kit so the popup serves at vechain.github.io/vechain-kit/. Remove once a custom domain is wired.
  • Privy dashboard: add the final popup origin to allowed origins for VECHAIN_PRIVY_APP_ID.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Adds VeChain whitelabel cross-app host for OAuth and transaction signing, plus connect/transact popup UIs and identity/address components.
    • OAuth providers (Google, Apple, X, Discord, GitHub, TikTok, LINE) now work without a host Privy config.
    • PIN input, app-hub lookup, stale-connection recovery, and improved transaction decoding/labels.
  • Localization

    • Adds 16 language locales for cross-app flows.
  • Chores

    • New CI workflows for build/deploy/automation and monorepo/dev script updates.
  • Documentation

    • README and docs updated with cross-app host details and env examples.

Review Change Stack

Agilulfo1820 and others added 30 commits May 15, 2026 11:38
New /cross-app-connect workspace serving Privy's whitelabel /cross-app/connect
and /cross-app/transact pages so VeChain Kit consumers no longer route through
Privy's hosted popup. Lets us add login intents, restyle the flow, decode
VeChain transactions against our ABIs, and block suspicious requests.

Provider tree mounts VeChainKitProvider only (it already owns Privy), and the
transact page guards the EIP-712 typed data against the user's smart-account
address before signing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0.2.2 hardcoded the Privy default URL for cross-app popups; from 0.3.x
onward the connector fetches the provider's connect/transact URLs from
the Privy backend, which means the dashboard's Custom URLs (Global wallet
-> Advanced) now actually take effect. Required for the whitelabel
cross-app-connect host to receive popup traffic.

The kit only imports toPrivyWalletConnector from the /rainbow-kit
subpath; that function's signature is backward compatible in 0.5.8
(only adds optional fields), so no kit code changes are needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the "Continue -> Privy modal" intermediate step with an inline
sign-in panel that renders one button per provider and triggers each
auth flow directly:

- OAuth providers (Google, Apple, X, Discord) use useLoginWithOAuth's
  initOAuth, which redirects straight to the provider's auth page.
- Email uses useLoginWithEmail's headless sendCode + loginWithCode with
  a Chakra PinInput for the OTP, so users never see a Privy modal at all.

The ?intent=<provider> URL param auto-fires the matching flow on mount
(intent=email pre-selects the email panel without sending a code).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useLoginWithVeChain now accepts an optional intent argument:

  const { login } = useLoginWithVeChain();
  await login({ intent: 'google' });

When set, usePrivyCrossAppSdk resolves the registered whitelabel connect
URL via createPrivyCrossAppClient.getProviderConnectUrl() and creates a
fresh wagmi connector with overrideConnectUrl set to "<url>?intent=...".
The whitelabel host already reads the intent URL param and jumps straight
into the matching OAuth/email flow, so the user skips the provider picker.

Resolving the URL dynamically avoids hardcoding the whitelabel domain in
the kit. The no-intent path is unchanged (still uses the connector from
the wagmi config).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the kit threw a configuration error if a consumer listed
'google', 'apple', or 'email' in loginMethods without providing a privy
prop, because those buttons triggered the kit's Privy-backed flows.

With the whitelabel cross-app-connect host these methods can now be
routed through useLoginWithVeChain({ intent }) instead:

- LoginWithGoogleButton / LoginWithAppleButton: when no privy prop,
  call loginViaCrossApp({ intent }) instead of Privy's initOAuth.
- EmailLoginButton: when no privy prop, render a "Continue with Email"
  button that hands the email/OTP flow off to the whitelabel host
  (intent: 'email') instead of showing an inline email input that would
  hit a dummy Privy app.
- Validation in VeChainKitProvider now only blocks 'github', 'passkey',
  and 'more' without privy (those have no cross-app fallback yet).

Consumer dApps without their own Privy config can now ship a
"Login with Google" button that one-clicks straight to Google via
VeChain's whitelabel host.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useLoginModalContent was the second gate hiding these buttons in
the no-privy branch (line 114). Now that they fall back to the
whitelabel cross-app flow, only passkey/github/more stay hidden
without privy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the connect page opened with ?intent=google, Privy's initOAuth
redirected to Google. After auth, Google redirected back to the same
URL (still ?intent=google), the page remounted with fresh component
state, and the useEffect fired initOAuth a second time -- bouncing
the user back to Google forever.

Persist the "already attempted" marker in sessionStorage so the flag
survives the OAuth redirect. Also gate on the OAuth loading state to
avoid firing while Privy is still processing the callback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Once the user is authenticated, the embedded wallet exists, and the
connection request was parsed, accept the connection automatically
instead of waiting for a Connect button click.

The user already opted in by clicking the login button on the
requester dApp; a separate Accept click was redundant. Skipped after
a failure so the manual button stays available as a fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hook now checks for a privy prop itself and routes
google/apple/twitter/discord through useLoginWithVeChain({ intent })
when there isn't one, instead of erroring against the dummy privy
app id. github (and any other provider with no cross-app fallback)
still throws a clear error.

Cleans up the per-button manual branching in LoginWithGoogleButton
and LoginWithAppleButton -- they just call initOAuth and let the
hook pick the right path.

This also fixes the playground's "OAuth Login Examples" section,
which calls useLoginWithOAuth directly and was hanging without
a privy config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ated

Returning users with a live Privy session got the popup snapped
open-and-shut with no chance to see who was asking to connect.
Now: capture whether the user was authenticated on first ready=true.

- If yes (returning), don't auto-accept -- show the Connect button
  so they can confirm the requester.
- If no (logged in via OAuth/email during this popup session),
  auto-accept once everything is in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expand CrossAppLoginIntent and the cross-app fallback set in
useLoginWithOAuth to cover every Privy-supported OAuth provider
(google, apple, twitter, discord, github, spotify, instagram,
tiktok, line, linkedin) plus email. Only passkey and 'more' still
require a privy prop on VeChainKitProvider.

Updates the validation in VeChainKitProvider and useLoginModalContent
to match: github / twitter / discord / etc. now work without a
consumer-supplied privy config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-in panel

SignInPanel now renders a 2-column icon grid covering google, apple,
twitter (X), discord, github, spotify, instagram, tiktok, line, and
linkedin. Email keeps its own row underneath. Each button calls
useLoginWithOAuth.initOAuth({ provider }) directly so the user
redirects straight to that provider's OAuth page.

The ?intent=<provider> URL param auto-fires the matching flow for
any of these providers (intent=email still pre-selects the email
panel without sending a code).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Playground and homepage now render a 3-column grid of buttons for
every Privy OAuth provider (google, apple, twitter (X), discord,
github, spotify, instagram, tiktok, line, linkedin) instead of just
google + github.

Updates the "Note" copy underneath: with the new kit fallback, these
buttons no longer require a Privy app configured by the consumer.
Without a privy prop, the hook routes through the VeChain whitelabel
cross-app host.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n privy

Earlier commits added all 10 of Privy's documented OAuth providers,
but only 7 are enabled in VeChain's Privy dashboard: google, apple,
twitter, discord, github, tiktok, line. Calling the others would
fail at the provider with a useless error.

Drop spotify / instagram / linkedin from CrossAppLoginIntent, the
useLoginWithOAuth fallback set, the cross-app-connect sign-in panel,
and the OAuth Login Examples in the playground and homepage.

Farcaster and WhatsApp are also enabled in the dashboard but use
non-OAuth flows (Farcaster SIWF, WhatsApp OTP) -- noted in code
comments as TODOs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Most Privy-backed login methods (google, apple, email, twitter, etc.)
no longer require a host-supplied privy config -- the kit routes them
through the VeChain whitelabel cross-app host when privy is missing.
Update the "Without Privy" diagram, the Method values table, and the
migration notes to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the popup opened with ?intent=github but the user was already
authenticated (e.g. logged in with Google in a previous popup session),
the host showed the existing Google session and silently ignored the
github intent. Now: detect the intent/auth mismatch and logout first
so the requested provider's OAuth flow runs cleanly.

Guard against the post-OAuth-redirect case via the existing
sessionStorage marker -- if we already initiated an OAuth attempt
for this intent, we're back from the redirect and the current session
is the one the user just authenticated with; no logout needed.

Also adjusts the auto-accept gate: when a user came with an explicit
intent they already consented on the requester dApp, so accept
regardless of initial auth state once the (now-correct) provider's
session is in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related issues:

1. The previous "auto-accept whenever intent is set" rule removed the
   Connect button for returning users who came with an intent that
   already matches a linked account. They expected to see their
   account + a Connect button, not a silent close.

2. The flow logout -> wait -> oauth flashed the old session's
   Connect panel briefly before switching to a spinner.

Restructure as a phase machine computed up-front:

- loading | no_params | parse_error: existing error / spinner UIs.
- switching_provider: intent set, authenticated, but user does NOT
  have the intent provider linked -> spinner + trigger logout.
- auth_pending: intent set, not authenticated -> spinner + trigger
  initOAuth (sessionStorage marker guards the post-redirect reload).
- show_picker: no intent (or intent='email'), not authenticated ->
  the all-providers SignInPanel.
- show_connect: ready to accept. Auto-accepts only if the user
  authenticated during this popup session (initialAuth was false);
  otherwise renders the manual Connect button.

`hasLinkedProvider(user, intent)` checks user.google / user.github /
etc. to tell "matching" from "stale" sessions. Auto-OAuth trigger
moved up to the parent so the spinner phases don't unmount it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the next-template-derived placeholder theme with an opinionated
VeChain-branded Chakra theme:

- Brand tokens (cross-app-connect/src/app/theme/brand.ts) pulled from
  the official guidelines at https://files.vechain.org/branding:
    purple #7266FF, dark purple #0C0A1F, cool gray #F0F0F5,
    almost white #FCFCFD; Satoshi/Inter type stack.
- Semantic color tokens (page-bg, card-bg, text-strong / -muted /
  -subtle, btn-row-*, chip-*, brand-accent) map to light or dark
  values via Chakra's _light / _dark scheme. initialColorMode +
  useSystemColorMode follow the user's OS preference.
- Two Button variants ('brand' for primary CTAs, 'row' for the
  stacked sign-in list) plus a Card base style with brand corner
  radius.
- Inter is loaded from Google Fonts; Satoshi falls back to Inter for
  now (would need self-hosting from files.vechain.org/branding/Fonts).
- VechainHeader component drops the logo (light/dark wordmark from
  the official zip, stored under /public/brand) + title + subtitle
  on every page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Redesign the no-intent SignInPanel to mirror the Privy hosted UI
(images shared by the user):

- Stacked full-width rows instead of a 2-column icon grid.
- Recent provider surfaces on top of the list with a "Recent"
  badge. Tracked in localStorage per Privy app id via
  cross-app/_lib/recent.ts; written when OAuth / email is
  initiated. Survives popup lifetime so the next session opens
  with the same provider on top.
- Less-used providers (Apple, GitHub, TikTok, LINE) collapse under
  a single "Other socials" row that expands inline. Primary tier
  (Google, X, Discord) and email stay visible by default.
- Monochrome glyphs (Apple, GitHub, TikTok, X) flip with the color
  mode so they stay legible in both themes.
- VechainHeader replaces the inline "Sign in to VeChain" heading,
  with the requester origin shown as subtitle ("Sign in to your
  VeChain wallet to grant <origin> access").
- Landing page wired through the same header for consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the dark-only hardcoded styles (whiteAlpha, gray.400, blue
button) with semantic brand tokens so the transact page picks up
light or dark mode automatically. VechainHeader replaces the
inline card header. Approve button switches to the brand variant;
the primaryType badge now uses the chip token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oggle

Chakra mutates body.className after hydration to apply the resolved
color mode ('chakra-ui-light' / 'chakra-ui-dark'). With system color
detection enabled this differs from the SSR-rendered HTML and triggers
React's hydration warning. Add suppressHydrationWarning on <body>
(same recipe Chakra docs use) -- safe because only the className is
mutated, not user-visible content.

Also drop a floating ColorModeToggle in the bottom-right corner for
debug: visible in dev by default, and toggleable in production via
NEXT_PUBLIC_SHOW_COLOR_MODE_TOGGLE=true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues:
- Toggle was inside VechainKitProviderWrapper which is loaded with
  ssr:false, so it didn't render until that wrapper hydrated and
  appeared missing on initial paint of /cross-app/connect.
- bg=card-bg + border=card-border meant near-zero contrast on both
  light (white-on-white) and dark surfaces.

Move it out of the VechainKit wrapper (still under ChakraProvider for
theme access), use the brand purple as bg with white icon and a soft
shadow so it's obvious in either mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Less likely to overlap form CTAs and the auto-accept spinner that sits
center-bottom of the card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VeChain's Privy app has email disabled and prefers phone over email
for OTP-based logins. Reorder + reshape the SignInPanel:

Primary (always visible):
  Google, Apple, X, Phone

Other socials (expandable):
  Discord, GitHub, TikTok, Farcaster, LINE

- Drop the email row + the email/OTP form. Email is no longer a
  cross-app intent.
- Add a Phone row that opens an inline SMS OTP form powered by
  useLoginWithSms. Mirrors the previous email flow (phone input ->
  6-digit PinInput -> verify), including the back button and the
  sendCode loading/awaiting/submitting states.
- Add a Farcaster row that flips the panel to a "coming soon"
  placeholder. Farcaster needs SIWF via Warpcast QR/deeplink, which
  isn't wired up here yet -- left as a TODO so the row is visible
  but doesn't silently fail.
- Internal view state is now a PanelView union ('picker' | 'phone'
  | 'farcaster') instead of a boolean showEmail flag.
- Recent badge / Other socials expandable behave as before; the
  hasLinkedProvider helper learned about user.phone and
  user.farcaster.
- The intent passthrough understands 'phone' and 'farcaster' (they
  flip the SignInPanel into the matching view on mount); only OAuth
  intents auto-redirect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VeChain's Privy app has email disabled, so the whitelabel
cross-app-connect host can't accept email-based logins. Roll back
the earlier "email falls back to cross-app when no privy" change:

- CrossAppLoginIntent: drop 'email', add 'phone' and 'farcaster'
  (matching the new SignInPanel options on the host).
- VeChainKitProvider validation: 'email' is back in the
  require-Privy list alongside 'passkey' and 'more'. Listing
  'email' in loginMethods without a privy prop throws again, with
  the same error message as before.
- useLoginModalContent: showEmailLogin forced to false when there
  is no privy, so the connect modal stops rendering the email row
  for consumers that aren't running their own Privy app.
- EmailLoginButton: drop the EmailLoginCrossAppButton fallback that
  was routing to intent='email'. The component now only handles
  the Privy-backed inline email + OTP flow; useLoginModalContent
  already hides it when privy is absent.

Phone is now a first-class cross-app intent and Farcaster is
reserved for the eventual SIWF integration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two theme bugs visible in dark mode:

- VechainHeader picked the wordmark src via useColorMode(), which
  returns the SSR default before hydration. The wrong-color logo
  flashed for a beat after every reload. Render both wordmarks and
  let Chakra's _light / _dark CSS pseudo selectors swap them -- no
  React state, no flicker.

- The Cancel button used variant="ghost", which falls back to
  Chakra's default gray.700 / whiteAlpha.700 text. Almost invisible
  on the VeChain dark-purple background. Override the ghost variant
  in the theme so it uses text-muted with text-strong on hover --
  applies everywhere (Cancel rows, "Back" buttons, etc.) without
  per-button color props.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous theme leaned heavily on VeChain Purple as the primary
button color, which doesn't match what the kit actually ships. The
kit's defaults (packages/vechain-kit/src/theme/tokens.ts) are clean
monochrome with a blue accent:

  Light: white modal, off-white cards (#F5F5F5), near-black text
         (#2E2E2E), dark pill primary (#272A2E -> white), blue
         accent (#3B82F6).
  Dark:  charcoal modal (#151515), translucent black cards, off-white
         text (rgb(223,223,221)), white pill primary (white -> black),
         lighter blue accent (#60A5FA).
  Radii: 8 / 12 / 16 / 24 / pill.
  Type:  Satoshi heading, Inter body, 15px / 600 on login rows,
         -0.005em letter spacing.

Rewrite the cross-app-connect theme around those tokens. Drop the
"brand-accent / brand-accent-hover" semantic tokens (which forced
the purple onto every CTA) in favour of "primary-btn-bg /
primary-btn-color" (monochrome) and "accent" (blue, for spinners
/ focus rings). The provider rows now match the kit's loginIn
variant: 52px tall, 16px radius, 15px/600 label, subtle border.
The primary CTA picks up the kit's vechainKitPrimary look: 60px
tall, pill-shaped, hover-by-opacity.

Brand identity stays via the VeChain wordmark + the purple "Recent"
chip; everything else aligns with the kit so the host doesn't feel
like a different app dropped into the flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five small interaction fixes:

- Wrap the requester identity in a RequesterChip component: lock icon
  for HTTPS, favicon via Google's S2 service, hostname stripped of
  scheme / default ports. Replaces the bare URL-in-a-sentence pattern
  that read like a dev console.
- Replace the "Other socials" chevron-row with a "+N more options"
  text link below the picker stack. The previous chevron-inside-row
  pattern collided two interactions; the link separates them clearly.
- Swap the purple "Recent" pill for a small green dot + tooltip
  ("Last used"). Same recall affordance, far less visual noise next
  to the primary CTA.
- Provider row labels are now "Continue with X" instead of just "X"
  -- sets up the verb so the row reads as an action.
- Ghost button variant now ships a baseline border + pill shape +
  48px height so "Cancel" reads as an outlined button at rest rather
  than stray text. Added a separate `link` variant for the new
  "more options" affordance.
- VechainHeader takes an optional requesterUrl prop that renders the
  RequesterChip under the title; the loading / picker / confirm
  screens use it consistently instead of inlining the origin into
  the subtitle string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apple, GitHub, and X are intentionally monochrome (their identity is
black/white) and continue to flip with the color mode. The other
providers now use their brand hex so the picker has the same
chromatic feel as Privy's hosted UI:

  Discord  -> #5865F2 (Blurple)
  TikTok   -> #FE2C55 (signature pink)
  LINE     -> #06C755 (LINE green)
  Phone    -> #34C759 (iMessage-style green for the SMS row)

Google keeps its own multi-color glyph (FcGoogle is already colored).
Brand colors live in a small BRAND_GLYPH_COLOR map; ProviderRow
consults it before falling back to the monochrome flip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The transact screen previously showed a raw EXECUTEBATCHWITHAUTHORIZATION
badge plus truncated hex blobs -- meaningless to non-crypto users and
gave every clause the same visual weight regardless of risk. Rewrite
the screen to translate calldata into human-readable actions.

Three decode tiers (cross-app-connect/src/app/cross-app/_lib/decoder.ts):

  1. Native VET transfer (no calldata, value > 0)
       -> "Send 1.5 VET"

  2. ERC-20 transfer / approve via 4-byte selector match
       -> "Send 10 B3TR" / "Allow spending up to 100 USDC" /
          "Allow unlimited B3TR spending"
     Tokens looked up in a static address book that mirrors the kit's
     mainnet / testnet / solo configs (B3TR, VOT3, VTHO across each).
     Unknown ERC-20s fall back to "tokens" with 18 decimals as a guess.

  3. Anything else -> b32 lookup at https://b32.vecha.in/
       -> "Run swap exact tokens for tokens on a contract" /
          "Interact with a contract" when even b32 doesn't know.
     Results cached in-memory so re-renders don't refetch.

UI now leads with a "This app wants to:" list of action rows: icon +
plain-language summary + recipient. Warning banners surface when:
  - any clause is undecodable ("we couldn't double-check every step")
  - an unlimited approve is requested
The Continue CTA reads "Continue anyway" when the unknown-clause
warning is showing, to slow the user down before signing something
that couldn't be verified. Existing safety guards (smart-account
mismatch, chain id mismatch, unsupported primaryType) still block
the Continue button outright.

Technical details (smart-account address, network, primaryType, raw
clauses with to/value/data) move behind an "Inspect details"
collapsible at the bottom -- present for power users but out of the
way for the 90% of users who shouldn't have to read calldata.

Adds @vechain/dapp-kit-react, @vechain/sdk-network, viem as direct
deps of cross-app-connect (already in the kit's transitive tree).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Agilulfo1820 Agilulfo1820 changed the title Feat/cross app connect whitelabel feat: VeChain whitelabel cross-app connect host May 19, 2026
Agilulfo1820 and others added 7 commits May 19, 2026 12:25
Daily cron (04:13 UTC) and manual workflow_dispatch run
yarn generate-app-hub against vechain/app-hub. If the resulting
app-hub.json differs from the committed copy, peter-evans/
create-pull-request opens (or updates) a single chore PR on the
branch chore/refresh-app-hub-cache with a diff to review.

Uses the workflow's GITHUB_TOKEN for the registry fetch so we hit
the 5000/hr authenticated API limit instead of 60/hr anonymous.
Concurrency group "cross-app-connect-refresh-app-hub" prevents
overlapping runs from leaving the refresh branch in a half-state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eed them

zizmor flagged pages: write / id-token: write on the deploy workflow
and contents: write / pull-requests: write on the refresh workflow
as too broad at the workflow level. Move them onto the specific job
that uses them (deploy / refresh) and keep contents: read as the
workflow-level default. Same intent, narrower blast radius.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`more` was previously rejected by VeChainKitProvider's config validator
unless the consuming dApp passed a `privy` prop. That made sense when
the More sub-view only surfaced Privy-driven socials, but it's been
wrong since v2.7: the sub-view also lists dapp-kit wallet overflow
(VeWorld / Sync2 / WalletConnect) that need no Privy, and there's now
a free whitelabel cross-app picker on top of that.

Two changes:

- VeChainKitProvider: drop `more` from the no-Privy invalid-methods
  list. Email and passkey still need self-hosted Privy (no whitelabel
  equivalent), so they stay.

- MoreOptionsContent: render a "Login with VeChain" section with a
  "Continue with VeChain" row that drives `useLoginWithVeChain`
  (cross-app picker via VECHAIN_PRIVY_APP_ID — works regardless of
  whether `privy` is set). The row is shown whenever `vechain` isn't
  on the main grid, so an opt-out dApp with
  `[google, apple, more]` now gets `[wallets overflow, VeChain
  picker]` in the sub-view instead of a configuration error.

Adds the two new English strings ("Login with VeChain", "Continue
with VeChain") to languages/en.json; `yarn translate` will fill the
sibling locale files at the next release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups to the previous "allow `more` without Privy" change:

- `useLoginModalContent` was forcing `showMoreLogin: false` whenever
  the consuming dApp didn't pass a `privy` prop, so the More-options
  link never rendered in the main grid even though the validator
  now permits the method. Drop that override; `more` follows the
  normal main-grid logic and the sub-view degrades gracefully.

- `MoreOptionsContent` gated the Ecosystem section on `!!privy`,
  which was incorrect: the cross-app SDK doesn't need the consuming
  dApp to own a Privy account, and `useFetchAppInfo` pulls the
  name/logo from Privy's public app-info endpoint. Gate on
  `privyEcosystemAppIDS?.length > 0` instead, matching the actual
  data dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- VeWorldLogoLight has a hardcoded SVG fill (rgb(235,241,246)) so the
  `color` prop has no effect. In dark mode the icon-tile background
  is white (brandInverseBg), so the near-white logo became invisible.
  Pick VeWorldLogoDark when isDark instead.

- Move "Other wallets" to the top of the More sub-view (dapp-kit
  overflow is the closest thing to the main grid in spirit, so it
  should come first), and put the "Continue with VeChain" picker
  underneath it as its own section.

- Rename the VeChain section label from "Login with VeChain" to
  "View more socials" — that's what tapping the row actually
  surfaces (the whitelabel popup's multi-provider picker), and it
  reads better in context of the overall sub-view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Continue with VeChain" read awkwardly under the "View more socials"
section heading — VeChain isn't a social provider, it's the picker
that lists them. Reuse the kit's existing copy "Login with social"
(same string the main-grid VeChain button uses) so the row labels
the actual outcome of the click.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The VeChain logo + brand-inverted tile read as a separate brand
button, which clashed with the "View more socials" framing. The row
isn't a brand entry — it's an overflow affordance. Swap to the
ellipsis icon on the neutral secondary background (matching the
"More options" pattern used elsewhere in this sub-view) and shorten
the label to "View more".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Agilulfo1820 Agilulfo1820 marked this pull request as ready for review May 19, 2026 10:53
The "filled inverted + recommended dot" look was hardcoded to
VeWorldButton, so {google, apple, more} grids ended up with no
recommended button at all (and the dev couldn't move that emphasis
to whichever provider made sense for their app).

- Extract the filled-inverted styling into a shared `primaryButtonStyle`
  helper (still intentionally not theme-driven — devs that themed
  `theme.buttons.primaryButton` shouldn't end up with a brand-tinted
  VeWorld or Google button).
- Add an `isPrimary` prop to VeWorld / Google / Apple / GitHub buttons.
  When true: filled-inverted surface + RecommendedDot. When false:
  outline secondary (existing look). VeWorld also picks the logo
  variant (`Light` vs `Dark`) that contrasts with the chosen surface.
- ConnectionOptionsStack scans `loginMethods`, finds the first whose
  `show*` flag is true (skipping `more`, which is a footer link), and
  passes `isPrimary` to the matching button case.

Net effect: with `loginMethods=[google, apple, more]`, Google now
renders as the primary CTA. With the default `[veworld, google,
apple, more]`, VeWorld stays primary (status quo). With `[apple,
veworld, …]`, Apple is primary and VeWorld degrades to outline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cross-app-connect/src/app/cross-app/_lib/appConfig.ts (1)

107-113: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize NEXT_PUBLIC_NETWORK_TYPE before branching to prevent silent contract address misselection.

The TypeScript assertion as 'main' | 'test' provides no runtime validation. If the environment variable contains an unexpected value (e.g., "staging"), it bypasses the nullish coalescing check and reaches the ternary operator. In appConfig.ts, this causes NETWORK_TYPE === 'main' to evaluate false, silently selecting the merged testnet contracts instead of mainnet. The same vulnerable pattern exists in thor.ts.

Implement explicit runtime normalization as suggested to ensure invalid values safely default to mainnet:

Suggested patch
-const NETWORK_TYPE = (process.env.NEXT_PUBLIC_NETWORK_TYPE ?? 'main') as
-    | 'main'
-    | 'test';
+const rawNetworkType = process.env.NEXT_PUBLIC_NETWORK_TYPE;
+const NETWORK_TYPE: 'main' | 'test' =
+    rawNetworkType === 'test' ? 'test' : 'main';

Also apply the same fix to cross-app-connect/src/app/cross-app/_lib/thor.ts (lines 15–17) where the identical pattern exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/_lib/appConfig.ts` around lines 107 -
113, Normalize and validate NEXT_PUBLIC_NETWORK_TYPE at runtime before using it
in branching: replace the current casted NETWORK_TYPE with logic that reads
process.env.NEXT_PUBLIC_NETWORK_TYPE, lowercases/trims it, and only accepts the
literal 'test' (otherwise default to 'main'); then use that normalized
NETWORK_TYPE when computing knownContracts (so MAINNET is chosen by default for
any invalid value). Apply the same runtime-normalization change to the analogous
variable in thor.ts to prevent silent selection of TESTNET addresses.
🟡 Minor comments (12)
cross-app-connect/src/app/cross-app/transact/transact.module.css-275-282 (1)

275-282: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace deprecated word-break: break-word with modern alternative.

Stylelint correctly flags word-break: break-word as deprecated at lines 279, 302, and 332. Use overflow-wrap: break-word for equivalent behavior.

🧹 Proposed fix
 .messageBody {
     font-size: 14px;
     color: var(--text-strong);
     white-space: pre-wrap;
-    word-break: break-word;
+    overflow-wrap: break-word;
     margin: 0;
 }

 .code {
     font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
     font-size: 12px;
     color: var(--text-strong);
     white-space: pre-wrap;
-    word-break: break-word;
+    overflow-wrap: break-word;
     background: transparent;
     margin: 0;
 }

 .typedFieldValue {
     font-size: 14px;
     color: var(--text-strong);
-    word-break: break-word;
+    overflow-wrap: break-word;
     min-width: 0;
 }

Also applies to: 297-306, 329-335

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/transact/transact.module.css` around
lines 275 - 282, Replace the deprecated CSS property word-break: break-word with
the modern equivalent overflow-wrap: break-word in the affected selectors (e.g.,
.messageBody and the other classes flagged in this diff) to preserve the same
wrapping behavior; locate each occurrence of word-break: break-word and swap it
to overflow-wrap: break-word, removing the deprecated declaration while keeping
the rest of the rules (font-size, color, white-space, margin) unchanged.
cross-app-connect/src/app/cross-app/_lib/decoder.ts-27-32 (1)

27-32: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused KnownAction import.

ESLint correctly flags that KnownAction is imported but never used. Only KnownActionCategory and KnownActionData are referenced in the code.

🧹 Proposed fix
 import {
     recognizeKnownAction,
-    type KnownAction,
     type KnownActionCategory,
     type KnownActionData,
 } from './knownActions';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/_lib/decoder.ts` around lines 27 - 32,
The import list in decoder.ts includes an unused symbol KnownAction; remove
KnownAction from the import statement so only recognizeKnownAction,
KnownActionCategory, and KnownActionData are imported. Locate the import block
that currently references KnownAction and delete that identifier (and any
trailing comma if necessary) to satisfy ESLint and keep the remaining imports
unchanged.
cross-app-connect/src/app/cross-app/_lib/contracts.ts-18-51 (1)

18-51: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Known router contracts are currently unlabelled.

knownContracts includes DEX router addresses, but they’re missing from APP_CONFIG_LABELS, so those known addresses resolve as unverified.

Suggested patch
     stargateContractAddress: 'Stargate',
     stargateNftContractAddress: 'Stargate NFT',
     veDelegate: 'veDelegate',
     veDelegateVotes: 'veDelegate Votes',
+    betterSwapRouterAddress: 'BetterSwap Router',
+    veTradeRouterAddress: 'VeTrade Router',
+    veTradeCustomRouterAddress: 'VeTrade Custom Router',
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/_lib/contracts.ts` around lines 18 - 51,
APP_CONFIG_LABELS is missing human-friendly labels for the DEX router entries
that exist in knownContracts, so those addresses show up as unverified; update
APP_CONFIG_LABELS (the Partial<Record<keyof KnownContracts, string>> constant)
to include entries for each router key present in KnownContracts (any keys like
*RouterAddress or containing "router") and supply descriptive labels (e.g.,
"Uniswap V2 Router", "SushiSwap Router", etc.) matching the router keys so those
known router addresses resolve as verified.
cross-app-connect/src/app/cross-app/connect/PinInput.tsx-128-128 (1)

128-128: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the digit aria-label text.

aria-label={Digit ${i + 1}} is hardcoded in English; route it through i18n so screen-reader text matches the selected locale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/connect/PinInput.tsx` at line 128, The
aria-label string in the PinInput component is hardcoded ("Digit {i+1}") —
update the input's aria-label to use your i18n system (e.g., t('pin.digitLabel',
{ number: i + 1 }) or intl.formatMessage({ id: 'pin.digitLabel' }, { number: i +
1 })) instead of the literal template; add a translation key like
"pin.digitLabel" with a placeholder (e.g., "Digit {number}") in locale files so
screen-reader text is localized, and replace the current aria-label={`Digit ${i
+ 1}`} in the PinInput render with the i18n call.
cross-app-connect/src/app/cross-app/connect/ConnectClient.tsx-27-27 (1)

27-27: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused imports.

OAUTH_PROVIDERS and OAuthProvider are imported but never used in this file.

Proposed fix
-import { OAUTH_PROVIDERS, type OAuthProvider } from '../../components/socials';
+import type { OAuthProvider } from '../../components/socials';

Actually, if OAuthProvider is also unused, remove the entire import:

-import { OAUTH_PROVIDERS, type OAuthProvider } from '../../components/socials';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/connect/ConnectClient.tsx` at line 27,
The import line bringing in OAUTH_PROVIDERS and OAuthProvider is unused in
ConnectClient (symbols OAUTH_PROVIDERS and OAuthProvider); remove those unused
imports (either delete the entire import statement or remove those identifiers
from the import) so the file no longer imports OAUTH_PROVIDERS and OAuthProvider
unnecessarily.
docs/login-modal.md-35-39 (1)

35-39: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix doc behavior matrix for no-privy mode (email/more).

These lines conflict with the current implementation: email is still rejected without privy, while more is allowed without privy. Please update the defaults/table/migration note accordingly to avoid integration mistakes.

Also applies to: 76-77, 151-151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/login-modal.md` around lines 35 - 39, The documentation incorrectly
states that Social methods and Email work without a host-supplied privy prop;
update the doc text and any behavior matrix entries referencing "email" and
"more" so they match the implementation: mark "email" as rejected/requires privy
and "more" as allowed/does not require privy (while keeping "passkey" as
requiring privy); search for occurrences of the terms "privy", "email", "more",
and "passkey" in this doc and the other referenced locations and adjust the
defaults/table/migration note to reflect these correct defaults.
examples/playground/tsconfig.json-38-50 (1)

38-50: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

include entries under dist/** are neutralized by exclude: ["dist"].

Line 44 adds dist/dev/types/**/*.ts, but Line 49 excludes the whole dist directory, so these files won't be compiled as a result of the include pattern. (They could still be included if imported by another file, but the direct include glob is ineffective.)

Remove "dist" from the exclude list or restructure to be more specific.

Proposed fix
   "exclude": [
     "node_modules",
-    ".next",
-    "dist"
+    ".next"
   ],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/playground/tsconfig.json` around lines 38 - 50, The tsconfig.json
has an include pattern "dist/dev/types/**/*.ts" but the exclude array contains
"dist", which prevents those files from being picked up; fix this by either
removing "dist" from the "exclude" array or narrowing the exclude (e.g., exclude
specific folders like "dist/build") so that "dist/dev/types/**/*.ts" remains
included — update the "exclude" entry accordingly to ensure the include pattern
for "dist/dev/types/**/*.ts" is effective.
cross-app-connect/README.md-127-131 (1)

127-131: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language to the fenced env-vars code block.

Line 127 uses an untyped fenced block, which triggers MD040.

Suggested change
-```
+```dotenv
 NEXT_PUBLIC_PRIVY_APP_ID=...
 NEXT_PUBLIC_PRIVY_CLIENT_ID=...
 NEXT_PUBLIC_PRIVY_DOMAIN=https://privy.your-app.privy.dev
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @cross-app-connect/README.md around lines 127 - 131, The fenced environment
variable block in README.md is missing a language tag (causing MD040); update
the code fence that contains NEXT_PUBLIC_PRIVY_APP_ID,
NEXT_PUBLIC_PRIVY_CLIENT_ID, and NEXT_PUBLIC_PRIVY_DOMAIN so the opening fence
is annotated (e.g., change todotenv) to mark it as a dotenv block and
satisfy the markdown lint rule.


</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/i18n/I18nProvider.tsx-32-36 (1)</summary><blockquote>

`32-36`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Await language change before unblocking render.**

`changeLanguage()` returns a Promise and must be awaited to ensure the language is fully applied before `setMounted(true)` allows children to render. Currently, the effect calls `setMounted(true)` immediately after `changeLanguage()`, creating a race condition where the i18n provider renders before translation state is ready.

<details>
<summary>💡 Suggested fix</summary>

```diff
 useEffect(() => {
-    const detected = resolveLanguage();
-    if (detected && i18n.language !== detected) {
-        i18n.changeLanguage(detected);
-    }
-    setMounted(true);
+    let cancelled = false;
+    const run = async () => {
+        const detected = resolveLanguage();
+        if (detected && i18n.language !== detected) {
+            await i18n.changeLanguage(detected);
+        }
+        if (!cancelled) setMounted(true);
+    };
+    void run();
+    return () => {
+        cancelled = true;
+    };
 }, []);
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/i18n/I18nProvider.tsx` around lines 32 - 36, In the
useEffect that checks `detected`, await the Promise returned by
`i18n.changeLanguage(detected)` before calling `setMounted(true)` so children
only render after the language is applied; update the effect body in
I18nProvider (the useEffect that references `detected`, `i18n.changeLanguage`,
and `setMounted`) to either make the inner function async and await
`i18n.changeLanguage(detected)` or chain `.then()`/`.catch()` to handle
success/failure and only call `setMounted(true)` after the change completes
(include error handling/logging on rejection).
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/i18n/locales/hi.json-19-21 (1)</summary><blockquote>

`19-21`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Use a governance-accurate translation for abstain.**

Line 21 currently uses “अनुपस्थित” (absent), which changes the vote meaning. Use an abstention term instead (e.g., “मतदान से विरत” / “तटस्थ” depending on your glossary).  
 
<details>
<summary>💬 Proposed fix</summary>

```diff
-        "abstain": "अनुपस्थित"
+        "abstain": "मतदान से विरत"
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/i18n/locales/hi.json` around lines 19 - 21, The
translation for the "abstain" key is incorrect: replace the current Hindi value
"अनुपस्थित" with a governance-accurate term (e.g., "मतदान से विरत" or "तटस्थ")
so the key "abstain" preserves vote meaning; update the "abstain" entry in the
same JSON object alongside "for" and "against" to use the chosen
glossary-approved string.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/i18n/locales/ja.json-181-183 (1)</summary><blockquote>

`181-183`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Fix mistranslation of “Clause” in transaction details.**

Lines 181-183 use “クローズ”, which means “close,” not “clause.” This can mislead users during signing review.  
 
<details>
<summary>📝 Proposed fix</summary>

```diff
-            "clauseSingular": "クローズ",
-            "clausePlural": "クローズ ({{count}})",
-            "clauseLabel": "クローズ {{index}} / {{total}} · 宛先",
+            "clauseSingular": "条項",
+            "clausePlural": "条項 ({{count}})",
+            "clauseLabel": "条項 {{index}} / {{total}} · 宛先",
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/i18n/locales/ja.json` around lines 181 - 183, The
translations for clauseSingular, clausePlural, and clauseLabel incorrectly use
"クローズ" (meaning "close"); update those keys to use the correct Japanese term for
"Clause" (e.g., "条項") so the UI reads correctly: set clauseSingular to "条項",
clausePlural to "条項({{count}})" (or "条項 ({{count}})" to match project
punctuation), and clauseLabel to "条項 {{index}} / {{total}} · 宛先" so the signing
review displays the proper word.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/i18n/locales/ko.json-181-183 (1)</summary><blockquote>

`181-183`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Correct “Clause” terminology in Korean transaction details.**

Lines 181-183 use “클로즈” (close) instead of “clause.” Please replace with a proper legal/transaction term (e.g., “절”).  
 
<details>
<summary>🛠️ Proposed fix</summary>

```diff
-            "clauseSingular": "클로즈",
-            "clausePlural": "클로즈 ({{count}})",
-            "clauseLabel": "클로즈 {{index}} / {{total}} · 대상",
+            "clauseSingular": "절",
+            "clausePlural": "절 ({{count}})",
+            "clauseLabel": "절 {{index}} / {{total}} · 대상",
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/i18n/locales/ko.json` around lines 181 - 183, The
keys clauseSingular, clausePlural, and clauseLabel currently use the
English-derived "클로즈" and should be updated to the proper Korean legal term "절";
replace the values for "clauseSingular" -> "절", "clausePlural" -> "절
({{count}})" and "clauseLabel" -> "절 {{index}} / {{total}} · 대상", preserving all
placeholders ({{count}}, {{index}}, {{total}}) and punctuation exactly as in the
originals.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (15)</summary><blockquote>

<details>
<summary>cross-app-connect/src/app/cross-app/transact/TransactClient.tsx (1)</summary><blockquote>

`451-458`: _💤 Low value_

**Missing `t` in useCallback dependency array.**

The `onApprove` callback uses `t()` for localized strings but doesn't include `t` in the dependency array. While `t` is typically stable, including it follows React best practices.


<details>
<summary>♻️ Proposed fix</summary>

```diff
     }, [
         client,
         verified,
         parsed,
         signMessage,
         signTypedData,
         getAccessToken,
+        t,
     ]);
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/transact/TransactClient.tsx` around lines
451 - 458, The onApprove callback (defined with useCallback) references the
localization function t() but t is missing from the dependency array; update the
dependency array for that useCallback (the onApprove callback in
TransactClient.tsx) to include t so React correctly tracks localization changes
(add t alongside client, verified, parsed, signMessage, signTypedData,
getAccessToken).
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/cross-app/_lib/knownActions.ts (1)</summary><blockquote>

`237-242`: _💤 Low value_

**Inconsistent `endorse` flag for `unendorseApp`.**

`unendorseApp` sets `data: { endorse: true }` which seems semantically incorrect for an un-endorse action. If the intent is to flag "endorsement-related action occurred", consider renaming to `endorseAction: true` or using separate flags.


<details>
<summary>♻️ Suggested fix</summary>

```diff
         unendorseApp: () => ({
             summary: t('action.governance.unendorse'),
             category: 'governance',
-            data: { endorse: true },
+            data: { endorse: false },
         }),
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/_lib/knownActions.ts` around lines 237 -
242, The unendorseApp action currently sets data: { endorse: true } which is
semantically wrong; update the unendorseApp entry in knownActions.ts (the
unendorseApp factory) to use a correctly named flag such as data: {
endorseAction: true } or set endorse: false, depending on intent, and ensure any
consumers reading the data key (e.g., handlers expecting endorse) are updated
accordingly so the flag name/boolean meaning is consistent across the codebase.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/scripts/fetch-app-hub.mjs (1)</summary><blockquote>

`23-33`: _⚡ Quick win_

**Add a timeout to outbound GitHub requests.**

`fetch()` here has no timeout, so a hanging upstream connection can block the workflow indefinitely (Line 24). Add `AbortSignal.timeout(...)` (and optionally a small retry wrapper).

 

<details>
<summary>Suggested patch</summary>

```diff
 async function ghFetch(url) {
     const res = await fetch(url, {
+        signal: AbortSignal.timeout(15_000),
         headers: {
             accept: 'application/vnd.github+json',
             // Optional: GITHUB_TOKEN env var raises the rate limit from
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/scripts/fetch-app-hub.mjs` around lines 23 - 33, The
ghFetch function currently calls fetch without a timeout; update ghFetch to
create an AbortSignal via AbortSignal.timeout(timeoutMs) (e.g., 10_000 ms) and
pass signal to fetch options so hung GitHub requests are aborted, and ensure the
created timer is used only for that request; additionally, wrap the fetch call
in a simple retry loop (2-3 attempts) handling AbortError/temporary network
errors and rethrow other errors so transient failures are retried while
permanent errors bubble up.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/cross-app/_lib/contracts.ts (1)</summary><blockquote>

`11-11`: _⚡ Quick win_

**Prefer alias import in src modules.**

Please use a path alias import here to match repository conventions.

 

As per coding guidelines, `**/src/**/*.{ts,tsx}`: Use path aliases for imports: `@/*` for src root, `@hooks` for hooks, `@components` for components, `@utils` for utils.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/_lib/contracts.ts` at line 11, The import
in contracts.ts currently pulls knownContracts and KnownContracts from a local
relative module; change it to use the repository path-alias convention instead.
Replace the relative import so that knownContracts and KnownContracts are
imported via a '`@/`...' alias (e.g., import { knownContracts, type KnownContracts
} from '`@/`...') to match src module aliasing and coding guidelines; ensure the
aliased path resolves to the same module that exports
knownContracts/KnownContracts and update any surrounding imports if necessary.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/cross-app/_lib/app-hub.ts (1)</summary><blockquote>

`9-9`: _⚡ Quick win_

**Use a src alias import instead of a relative import.**

Please switch this import to the configured alias form to match the repository import policy.

 

As per coding guidelines, `**/src/**/*.{ts,tsx}`: Use path aliases for imports: `@/*` for src root, `@hooks` for hooks, `@components` for components, `@utils` for utils.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/_lib/app-hub.ts` at line 9, Replace the
relative JSON import in app-hub.ts (import data from './app-hub.json') with the
repository path-alias form rooted at src; update the import to use the `@/`...
alias that points to src (e.g. import from '`@/app/cross-app/_lib/app-hub.json`')
so it follows the repo policy for src imports and still resolves JSON modules
via the existing TS/webpack config.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/cross-app/_lib/useAddressInfo.ts (1)</summary><blockquote>

`4-10`: _⚡ Quick win_

**Switch to alias-based import for src code.**

Use the configured src alias instead of relative import in this module.

 

As per coding guidelines, `**/src/**/*.{ts,tsx}`: Use path aliases for imports: `@/*` for src root, `@hooks` for hooks, `@components` for components, `@utils` for utils.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/_lib/useAddressInfo.ts` around lines 4 -
10, Replace the relative import from './thor' with the project alias-based src
import (use the configured `@/` alias) so the same exports — getAvatarForAddress,
getDomainOfAddress, picassoFallback and the DomainInfo type — are imported via
the src alias (e.g. import { getAvatarForAddress, getDomainOfAddress,
picassoFallback, type DomainInfo } from '`@/`.../thor') instead of './thor';
ensure the alias path points to the same thor module in the src tree.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/cross-app/_components/SignInPanel.tsx (1)</summary><blockquote>

`14-17`: _⚡ Quick win_

**Use path aliases for internal imports in `src/`.**

Please replace relative internal imports with `@/*`/`@components`/`@utils` aliases for consistency.
 
As per coding guidelines "Use path aliases for imports: `@/*` for src root, `@hooks` for hooks, `@components` for components, `@utils` for utils".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/cross-app/_components/SignInPanel.tsx` around lines
14 - 17, Replace the relative internal imports in SignInPanel (the imports of
socials, getRecentProvider/setRecentProvider, PinInput, and connect.module.css)
with the project's path aliases: use `@components` for components (e.g. socials,
PinInput), `@/`* or `@utils` for utility modules (e.g. the recent helpers), and `@/`*
or `@styles` for CSS modules as per the guidelines; update the import specifiers
referencing '../../components/socials', '../_lib/recent', '../connect/PinInput',
and '../connect/connect.module.css' to the corresponding alias paths so the
module resolution uses `@components/`@utils/@/* aliases consistently.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/components/VechainHeader.tsx (1)</summary><blockquote>

`4-5`: _⚡ Quick win_

**Use project path aliases instead of relative imports.**

Please switch these imports to the configured aliases to keep import style consistent across `src/`.
 
As per coding guidelines "Use path aliases for imports: `@/*` for src root, `@hooks` for hooks, `@components` for components, `@utils` for utils".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/components/VechainHeader.tsx` around lines 4 - 5,
Replace the relative imports with the project's path aliases: change the
RequesterChip import to use the `@components` alias (e.g. import RequesterChip
from '`@components/RequesterChip`') and change the CSS import to use the src-root
alias (e.g. import styles from '`@/app/components/VechainHeader.module.css`');
update the import statements in VechainHeader.tsx so they reference
RequesterChip and VechainHeader.module.css via the configured aliases instead of
relative paths.
```

</details>

</blockquote></details>
<details>
<summary>packages/vechain-kit/src/providers/CrossAppErrorRecovery.tsx (1)</summary><blockquote>

`5-5`: _⚡ Quick win_

**Use src path alias import instead of relative import.**

Prefer alias-based imports in `src` for consistency with project conventions.

 

As per coding guidelines, `**/src/**/*.{ts,tsx}`: Use path aliases for imports: `@/*` for src root, `@hooks` for hooks, `@components` for components, `@utils` for utils.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vechain-kit/src/providers/CrossAppErrorRecovery.tsx` at line 5,
Replace the relative import of useModal from './ModalProvider' with the project
path-alias import (use the `@components` alias for component/providers): update
the import that references useModal in CrossAppErrorRecovery.tsx to import from
the appropriate alias-based path (e.g. `@components/ModalProvider` or the
project's equivalent) so it follows the src path-alias convention instead of a
relative import.
```

</details>

</blockquote></details>
<details>
<summary>packages/vechain-kit/src/providers/VeChainKitProvider.tsx (1)</summary><blockquote>

`55-55`: _⚡ Quick win_

**Replace relative provider import with alias import.**

Use the configured `src` alias instead of `./CrossAppErrorRecovery`.

 

As per coding guidelines, `**/src/**/*.{ts,tsx}`: Use path aliases for imports: `@/*` for src root, `@hooks` for hooks, `@components` for components, `@utils` for utils.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vechain-kit/src/providers/VeChainKitProvider.tsx` at line 55, In
VeChainKitProvider.tsx replace the relative import "import {
CrossAppErrorRecovery } from './CrossAppErrorRecovery';" with the configured src
alias form so the module is imported via the project alias (e.g. import {
CrossAppErrorRecovery } from '`@/providers/CrossAppErrorRecovery`';), ensuring you
reference the CrossAppErrorRecovery symbol by the alias instead of a relative
path.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/layout.tsx (2)</summary><blockquote>

`2-4`: _⚡ Quick win_

**Use `@/` aliases for local `src` imports.**

Please replace relative imports with configured path aliases in this layout module.

 

As per coding guidelines, "Use path aliases for imports: `@/*` for src root, `@hooks` for hooks, `@components` for components, `@utils` for utils".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/layout.tsx` around lines 2 - 4, Replace the
relative imports in this module with the configured path aliases: change the
import of PrivyProviderWrapper to use the src alias (e.g., import
PrivyProviderWrapper from '`@/app/providers/PrivyProviderWrapper`' or the
appropriate `@/` path to the provider), change I18nProvider to use its alias
(e.g., '`@/app/i18n/I18nProvider`' or '`@i18n`' if configured), and update the
globals.css import to use '`@/globals.css`' so all imports (PrivyProviderWrapper,
I18nProvider, and globals.css) use the project's `@/`* alias convention.
```

</details>

---

`25-29`: _⚡ Quick win_

**Move viewport settings to Next.js viewport API.**

In Next.js App Router, use the exported `viewport` object in `app/layout.tsx` instead of a manual `<meta name="viewport">` tag. This prevents duplication with Next.js default viewport handling and ensures consistent configuration.

Example:
```typescript
export const viewport: Viewport = {
    width: 'device-width',
    initialScale: 1,
};
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/layout.tsx` around lines 25 - 29, Remove the manual
<meta name="viewport"> tag from app/layout.tsx and instead export a Next.js App
Router viewport configuration by adding an exported constant named viewport
(type Viewport) with width: 'device-width' and initialScale: 1; ensure the
export is at module scope in layout.tsx so Next.js picks it up and avoid
duplicating viewport handling.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/i18n/config.ts (1)</summary><blockquote>

`6-23`: _⚡ Quick win_

**Switch locale imports to `@/` aliases.**

These relative imports should use the project alias convention to keep import patterns consistent.

 

As per coding guidelines, "Use path aliases for imports: `@/*` for src root, `@hooks` for hooks, `@components` for components, `@utils` for utils".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/i18n/config.ts` around lines 6 - 23, Replace the
relative locale imports in config.ts with the project path-alias form (use
"`@/`..." rooted at src) so all locale modules (en, de, it, fr, es, zh, ja, ru,
ro, vi, nl, ko, sv, tw, tr, hi, pt) are imported via the alias instead of
'./locales/...'; update each import statement (e.g., the en import and the other
locale imports) to use "`@/app/i18n/locales/`<locale>.json" (or the correct alias
path to the i18n locales directory) so the file follows the project's alias
convention.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/i18n/I18nProvider.tsx (1)</summary><blockquote>

`5-5`: _⚡ Quick win_

**Use project path aliases for `src` imports.**

Please replace the relative import with the configured alias pattern for consistency across the app shell code.

 

As per coding guidelines, "Use path aliases for imports: `@/*` for src root, `@hooks` for hooks, `@components` for components, `@utils` for utils".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/i18n/I18nProvider.tsx` at line 5, Replace the
relative import in I18nProvider.tsx that imports from './config' with the
project path alias that points to the same module (use the '`@/`*' src-root
alias), e.g. import the config module via the alias path (targeting the same
file exported by config) so the line importing i18n and resolveLanguage uses the
configured alias instead of a relative path; update the import statement in the
I18nProvider component accordingly.
```

</details>

</blockquote></details>
<details>
<summary>cross-app-connect/src/app/page.tsx (1)</summary><blockquote>

`4-5`: _⚡ Quick win_

**Use path aliases instead of relative imports.**

Line 4 and Line 5 use relative imports; this violates the src alias rule and makes cross-folder refactors harder.

<details>
<summary>♻️ Proposed fix</summary>

```diff
-import { VechainHeader } from './components/VechainHeader';
-import styles from './page.module.css';
+import { VechainHeader } from '`@/app/components/VechainHeader`';
+import styles from '`@/app/page.module.css`';
```
</details>

 

As per coding guidelines, `**/src/**/*.{ts,tsx}`: "Use path aliases for imports: `@/*` for src root, `@hooks` for hooks, `@components` for components, `@utils` for utils".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cross-app-connect/src/app/page.tsx` around lines 4 - 5, Replace the relative
imports in page.tsx with path aliases: change the VechainHeader import to use
the `@components` alias (import VechainHeader from '`@components/VechainHeader`')
and change the CSS module import (styles from './page.module.css') to use the
src root alias (e.g., styles from '`@/app/page.module.css`' or the appropriate
path under `@/`* for your repo layout) so both VechainHeader and styles use
configured path aliases instead of relative paths.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/cross-app-connect-check.yaml:

  • Around line 15-17: The concurrency group currently uses the branch/ref (${ {
    github.head_ref || github.ref_name } }) which lets different PRs with the same
    branch name cancel each other; update the concurrency.group expression in the
    concurrency block to include the pull request identifier (e.g., use
    github.event.pull_request.number when available) so the key is PR-scoped
    (falling back to head_ref/ref_name for non-PR runs), ensuring unrelated PR runs
    do not cancel each other.

In @cross-app-connect/package.json:

  • Around line 20-34: The devDependencies eslint-config-next and
    @next/eslint-plugin-next must match the Next.js version used ("next":
    "~16.2.3"); update the package.json entries for "eslint-config-next" and
    "@next/eslint-plugin-next" to the same 16.2.3 release (or a semver range that
    preserves major.minor 16.2) so parser/rule compatibility is ensured, then
    reinstall deps (npm/yarn) and re-run lint to confirm no rule/parser mismatches.

In @cross-app-connect/src/app/components/AddressTag.tsx:

  • Around line 5-8: The current relative imports for resolveContractLabel,
    useAddressInfo, and truncateAddress should use repo path aliases instead of ../
    paths; update the three import statements in AddressTag.tsx to use the src-root
    alias (e.g. replace "../cross-app/_lib/contracts" with
    "@/app/cross-app/_lib/contracts", "../cross-app/_lib/useAddressInfo" with
    "@/app/cross-app/_lib/useAddressInfo", and "../cross-app/_lib/format" with
    "@/app/cross-app/_lib/format") so they follow the "@/..." convention per the
    coding guidelines.

In @cross-app-connect/src/app/components/IdentityRow.tsx:

  • Line 53: The current walletPending = !walletAddress || isLoading makes the
    pendingLabel/placeholder branch unreachable when walletAddress is absent; change
    walletPending to depend only on isLoading (e.g., walletPending = isLoading) and
    adjust downstream logic that shows the connect/missing-wallet UI to use
    walletAddress/connected state explicitly so the placeholder branch (the code
    around pendingLabel rendering and the UI in the 93-107 block) can execute when
    loading vs when no wallet is present; update any conditionals that reference
    walletPending to use walletAddress checks where appropriate.
  • Around line 5-8: Replace relative imports in IdentityRow.tsx with repo path
    aliases: change useAddressInfo import to use the hooks alias (e.g. import {
    useAddressInfo } from '@hooks/cross-app/_lib/useAddressInfo'), change
    truncateAddress to use the utils alias (e.g. import { truncateAddress } from
    '@utils/cross-app/_lib/format'), change linkedSocials to the components alias
    (e.g. import { linkedSocials } from '@components/socials'), and change the CSS
    import to the components alias (e.g. import styles from
    '@components/IdentityRow.module.css'); keep the same exported symbols
    (useAddressInfo, truncateAddress, linkedSocials, styles) and only update the
    module paths to the @/* style.

In @cross-app-connect/src/app/components/RequesterChip.tsx:

  • Around line 5-6: Replace the relative imports with path-alias imports: change
    the import of lookupAppByUrl (symbol: lookupAppByUrl) from
    '../cross-app/_lib/app-hub' to the source-root alias (e.g.
    '@/cross-app/_lib/app-hub'), and change the CSS module import (symbol: styles)
    from './RequesterChip.module.css' to the components alias (e.g.
    '@components/RequesterChip.module.css') so imports follow the repo conventions
    (@/*, @components).

In @cross-app-connect/src/app/cross-app/_components/SignInPanel.tsx:

  • Around line 245-257: The PinInput.onComplete handler currently calls
    loginWithCode and also calls setCode, causing duplicate OTP submissions with the
    Verify button's onSubmitCode; remove the loginWithCode call (and its .catch)
    from the onComplete callback so it only calls setCode(v) and relies on
    onSubmitCode to perform loginWithCode({ code }) while preserving the
    submittingCode gating in onSubmitCode to prevent concurrent submissions.

In @cross-app-connect/src/app/cross-app/_lib/client.ts:

  • Around line 17-18: Replace the non-null assertion on NEXT_PUBLIC_PRIVY_APP_ID
    used when calling createClient by explicitly validating
    process.env.NEXT_PUBLIC_PRIVY_APP_ID beforehand: check that the value exists and
    matches the same pattern/constraints used for privyDomain, and if not throw a
    descriptive Error (e.g., "Missing or invalid NEXT_PUBLIC_PRIVY_APP_ID") before
    calling createClient so initialization fails fast; update the createClient call
    to use the validated appId variable instead of
    process.env.NEXT_PUBLIC_PRIVY_APP_ID!.

In @cross-app-connect/src/app/cross-app/_lib/lastIdentity.ts:

  • Around line 2-11: The code currently writes the raw label (email/phone/Privy
    DID) to localStorage indefinitely; change the storage logic used where
    usePrivy().user is non-null so that you store a short-lived, obfuscated record
    instead: add a TTL (expiry timestamp) and store only the provider plus a masked
    hint (e.g., first char + last char or hashed/truncated hint) rather than the
    full label, and ensure reads check the TTL and reject expired entries; update
    the functions/logic that read/write the last-identity (the code paths
    referencing label, usePrivy().user, and localStorage keys) to enforce
    masking and TTL handling.

In @cross-app-connect/src/app/cross-app/_lib/thor.ts:

  • Around line 15-32: The code currently asserts NEXT_PUBLIC_NETWORK_TYPE into
    NETWORK_TYPE and immediately indexes NETWORK, which can produce undefined and
    crash; update the logic around NETWORK_TYPE/NETWORK lookup (symbols:
    NETWORK_TYPE, NETWORK, networkConfig, thor, ThorClient.at) to validate that
    process.env.NEXT_PUBLIC_NETWORK_TYPE is one of the allowed keys ('main'|'test')
    before using it to index NETWORK, default to 'main' or throw a clear error if
    invalid, and ensure thor is constructed from a guaranteed non-undefined
    networkConfig.nodeUrl; apply the identical validation/fallback change to the
    corresponding code in appConfig (the same pattern at lines referenced in the
    review).

In @cross-app-connect/src/app/cross-app/transact/TransactClient.tsx:

  • Around line 302-322: The JSON.parse call inside TransactClient.tsx's
    eth_signTypedData_v4 branch can throw on malformed input; wrap parsing of raw
    (params[1]) in a try/catch, handle parse errors by returning null (or the
    existing safe fallback) and optionally logging the error, and ensure typedData
    is only used when parsing succeeds (affecting the typedData, primaryType,
    isSmartAccountAuth, and parseClauses logic).
  • Around line 400-410: The code is currently signing parsed.message (decoded
    UTF-8) instead of the original bytes; update the call to signMessage to pass
    parsed.raw as the message input and include an encoding option determined from
    parsed.raw (if parsed.raw startsWith("0x") use encoding: "hex", otherwise
    encoding: "utf-8") so Privy signs the exact original bytes per EIP-191; adjust
    the call site in TransactClient.tsx where signMessage is invoked and ensure
    signature = result.signature remains unchanged.

In @cross-app-connect/src/app/page.tsx:

  • Around line 1-31: Update the React peer dependencies in the cross-app-connect
    package by changing the declared versions for react and react-dom in
    package.json to "^18.2.0" (currently "^18"), so they meet Next.js 16.2.3's
    requirement; after editing package.json, run your package manager
    (npm/yarn/pnpm) to reinstall/update lockfile and verify the build. Ensure both
    "react" and "react-dom" entries are updated together to the exact same "^18.2.0"
    spec to avoid version mismatches.

In @cross-app-connect/src/app/providers/PrivyProviderWrapper.tsx:

  • Around line 22-23: The code uses a non-null assertion for
    NEXT_PUBLIC_PRIVY_APP_ID in PrivyProviderWrapper
    (appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}) which can cause an opaque
    runtime failure; update PrivyProviderWrapper to explicitly validate
    process.env.NEXT_PUBLIC_PRIVY_APP_ID (and optionally
    NEXT_PUBLIC_PRIVY_CLIENT_ID) at startup and throw a descriptive Error (e.g.,
    "Missing required env NEXT_PUBLIC_PRIVY_APP_ID") so the app fails fast with a
    clear message rather than using the non-null assertion.

In @examples/playground/src/app/providers/VechainKitProviderWrapper.tsx:

  • Around line 240-242: Remove the non-null assertion on
    NEXT_PUBLIC_DELEGATOR_URL and conditionally pass feeDelegation only when the
    environment variable is defined: check process.env.NEXT_PUBLIC_DELEGATOR_URL
    before constructing the feeDelegation object used by the VechainKit provider
    (the feeDelegation prop and its delegatorUrl field), and omit or set
    feeDelegation to undefined/null when the env var is missing so runtime flows
    don't receive an undefined delegatorUrl.

In @packages/vechain-kit/src/providers/CrossAppErrorRecovery.tsx:

  • Around line 29-38: The handler currently trusts any postMessage and forces
    disconnect/openConnectModal when event.data.type ===
    'vk:cross-app-no-connection'; update the handler in CrossAppErrorRecovery (the
    block that reads const type = (event.data as { type?: unknown } | null)?.type
    and then calls disconnect() and openConnectModal()) to first verify event.origin
    is an allowed/trusted origin (and if available, that event.source matches the
    expected popup reference) before performing the disconnect and modal flow; if
    the origin/source check fails, silently ignore the message. Ensure the same
    origin/source validation is applied to the other identical handler at lines
    45-46.

Outside diff comments:
In @cross-app-connect/src/app/cross-app/_lib/appConfig.ts:

  • Around line 107-113: Normalize and validate NEXT_PUBLIC_NETWORK_TYPE at
    runtime before using it in branching: replace the current casted NETWORK_TYPE
    with logic that reads process.env.NEXT_PUBLIC_NETWORK_TYPE, lowercases/trims it,
    and only accepts the literal 'test' (otherwise default to 'main'); then use that
    normalized NETWORK_TYPE when computing knownContracts (so MAINNET is chosen by
    default for any invalid value). Apply the same runtime-normalization change to
    the analogous variable in thor.ts to prevent silent selection of TESTNET
    addresses.

Minor comments:
In @cross-app-connect/README.md:

  • Around line 127-131: The fenced environment variable block in README.md is
    missing a language tag (causing MD040); update the code fence that contains
    NEXT_PUBLIC_PRIVY_APP_ID, NEXT_PUBLIC_PRIVY_CLIENT_ID, and
    NEXT_PUBLIC_PRIVY_DOMAIN so the opening fence is annotated (e.g., change ``` to

In `@cross-app-connect/src/app/cross-app/_lib/contracts.ts`:
- Around line 18-51: APP_CONFIG_LABELS is missing human-friendly labels for the
DEX router entries that exist in knownContracts, so those addresses show up as
unverified; update APP_CONFIG_LABELS (the Partial<Record<keyof KnownContracts,
string>> constant) to include entries for each router key present in
KnownContracts (any keys like *RouterAddress or containing "router") and supply
descriptive labels (e.g., "Uniswap V2 Router", "SushiSwap Router", etc.)
matching the router keys so those known router addresses resolve as verified.

In `@cross-app-connect/src/app/cross-app/_lib/decoder.ts`:
- Around line 27-32: The import list in decoder.ts includes an unused symbol
KnownAction; remove KnownAction from the import statement so only
recognizeKnownAction, KnownActionCategory, and KnownActionData are imported.
Locate the import block that currently references KnownAction and delete that
identifier (and any trailing comma if necessary) to satisfy ESLint and keep the
remaining imports unchanged.

In `@cross-app-connect/src/app/cross-app/connect/ConnectClient.tsx`:
- Line 27: The import line bringing in OAUTH_PROVIDERS and OAuthProvider is
unused in ConnectClient (symbols OAUTH_PROVIDERS and OAuthProvider); remove
those unused imports (either delete the entire import statement or remove those
identifiers from the import) so the file no longer imports OAUTH_PROVIDERS and
OAuthProvider unnecessarily.

In `@cross-app-connect/src/app/cross-app/connect/PinInput.tsx`:
- Line 128: The aria-label string in the PinInput component is hardcoded ("Digit
{i+1}") — update the input's aria-label to use your i18n system (e.g.,
t('pin.digitLabel', { number: i + 1 }) or intl.formatMessage({ id:
'pin.digitLabel' }, { number: i + 1 })) instead of the literal template; add a
translation key like "pin.digitLabel" with a placeholder (e.g., "Digit
{number}") in locale files so screen-reader text is localized, and replace the
current aria-label={`Digit ${i + 1}`} in the PinInput render with the i18n call.

In `@cross-app-connect/src/app/cross-app/transact/transact.module.css`:
- Around line 275-282: Replace the deprecated CSS property word-break:
break-word with the modern equivalent overflow-wrap: break-word in the affected
selectors (e.g., .messageBody and the other classes flagged in this diff) to
preserve the same wrapping behavior; locate each occurrence of word-break:
break-word and swap it to overflow-wrap: break-word, removing the deprecated
declaration while keeping the rest of the rules (font-size, color, white-space,
margin) unchanged.

In `@cross-app-connect/src/app/i18n/I18nProvider.tsx`:
- Around line 32-36: In the useEffect that checks `detected`, await the Promise
returned by `i18n.changeLanguage(detected)` before calling `setMounted(true)` so
children only render after the language is applied; update the effect body in
I18nProvider (the useEffect that references `detected`, `i18n.changeLanguage`,
and `setMounted`) to either make the inner function async and await
`i18n.changeLanguage(detected)` or chain `.then()`/`.catch()` to handle
success/failure and only call `setMounted(true)` after the change completes
(include error handling/logging on rejection).

In `@cross-app-connect/src/app/i18n/locales/hi.json`:
- Around line 19-21: The translation for the "abstain" key is incorrect: replace
the current Hindi value "अनुपस्थित" with a governance-accurate term (e.g.,
"मतदान से विरत" or "तटस्थ") so the key "abstain" preserves vote meaning; update
the "abstain" entry in the same JSON object alongside "for" and "against" to use
the chosen glossary-approved string.

In `@cross-app-connect/src/app/i18n/locales/ja.json`:
- Around line 181-183: The translations for clauseSingular, clausePlural, and
clauseLabel incorrectly use "クローズ" (meaning "close"); update those keys to use
the correct Japanese term for "Clause" (e.g., "条項") so the UI reads correctly:
set clauseSingular to "条項", clausePlural to "条項({{count}})" (or "条項 ({{count}})"
to match project punctuation), and clauseLabel to "条項 {{index}} / {{total}} ·
宛先" so the signing review displays the proper word.

In `@cross-app-connect/src/app/i18n/locales/ko.json`:
- Around line 181-183: The keys clauseSingular, clausePlural, and clauseLabel
currently use the English-derived "클로즈" and should be updated to the proper
Korean legal term "절"; replace the values for "clauseSingular" -> "절",
"clausePlural" -> "절 ({{count}})" and "clauseLabel" -> "절 {{index}} / {{total}}
· 대상", preserving all placeholders ({{count}}, {{index}}, {{total}}) and
punctuation exactly as in the originals.

In `@docs/login-modal.md`:
- Around line 35-39: The documentation incorrectly states that Social methods
and Email work without a host-supplied privy prop; update the doc text and any
behavior matrix entries referencing "email" and "more" so they match the
implementation: mark "email" as rejected/requires privy and "more" as
allowed/does not require privy (while keeping "passkey" as requiring privy);
search for occurrences of the terms "privy", "email", "more", and "passkey" in
this doc and the other referenced locations and adjust the
defaults/table/migration note to reflect these correct defaults.

In `@examples/playground/tsconfig.json`:
- Around line 38-50: The tsconfig.json has an include pattern
"dist/dev/types/**/*.ts" but the exclude array contains "dist", which prevents
those files from being picked up; fix this by either removing "dist" from the
"exclude" array or narrowing the exclude (e.g., exclude specific folders like
"dist/build") so that "dist/dev/types/**/*.ts" remains included — update the
"exclude" entry accordingly to ensure the include pattern for
"dist/dev/types/**/*.ts" is effective.

---

Nitpick comments:
In `@cross-app-connect/scripts/fetch-app-hub.mjs`:
- Around line 23-33: The ghFetch function currently calls fetch without a
timeout; update ghFetch to create an AbortSignal via
AbortSignal.timeout(timeoutMs) (e.g., 10_000 ms) and pass signal to fetch
options so hung GitHub requests are aborted, and ensure the created timer is
used only for that request; additionally, wrap the fetch call in a simple retry
loop (2-3 attempts) handling AbortError/temporary network errors and rethrow
other errors so transient failures are retried while permanent errors bubble up.

In `@cross-app-connect/src/app/components/VechainHeader.tsx`:
- Around line 4-5: Replace the relative imports with the project's path aliases:
change the RequesterChip import to use the `@components` alias (e.g. import
RequesterChip from '`@components/RequesterChip`') and change the CSS import to use
the src-root alias (e.g. import styles from
'`@/app/components/VechainHeader.module.css`'); update the import statements in
VechainHeader.tsx so they reference RequesterChip and VechainHeader.module.css
via the configured aliases instead of relative paths.

In `@cross-app-connect/src/app/cross-app/_components/SignInPanel.tsx`:
- Around line 14-17: Replace the relative internal imports in SignInPanel (the
imports of socials, getRecentProvider/setRecentProvider, PinInput, and
connect.module.css) with the project's path aliases: use `@components` for
components (e.g. socials, PinInput), `@/`* or `@utils` for utility modules (e.g. the
recent helpers), and `@/`* or `@styles` for CSS modules as per the guidelines;
update the import specifiers referencing '../../components/socials',
'../_lib/recent', '../connect/PinInput', and '../connect/connect.module.css' to
the corresponding alias paths so the module resolution uses
`@components/`@utils/@/* aliases consistently.

In `@cross-app-connect/src/app/cross-app/_lib/app-hub.ts`:
- Line 9: Replace the relative JSON import in app-hub.ts (import data from
'./app-hub.json') with the repository path-alias form rooted at src; update the
import to use the `@/`... alias that points to src (e.g. import from
'`@/app/cross-app/_lib/app-hub.json`') so it follows the repo policy for src
imports and still resolves JSON modules via the existing TS/webpack config.

In `@cross-app-connect/src/app/cross-app/_lib/contracts.ts`:
- Line 11: The import in contracts.ts currently pulls knownContracts and
KnownContracts from a local relative module; change it to use the repository
path-alias convention instead. Replace the relative import so that
knownContracts and KnownContracts are imported via a '`@/`...' alias (e.g., import
{ knownContracts, type KnownContracts } from '`@/`...') to match src module
aliasing and coding guidelines; ensure the aliased path resolves to the same
module that exports knownContracts/KnownContracts and update any surrounding
imports if necessary.

In `@cross-app-connect/src/app/cross-app/_lib/knownActions.ts`:
- Around line 237-242: The unendorseApp action currently sets data: { endorse:
true } which is semantically wrong; update the unendorseApp entry in
knownActions.ts (the unendorseApp factory) to use a correctly named flag such as
data: { endorseAction: true } or set endorse: false, depending on intent, and
ensure any consumers reading the data key (e.g., handlers expecting endorse) are
updated accordingly so the flag name/boolean meaning is consistent across the
codebase.

In `@cross-app-connect/src/app/cross-app/_lib/useAddressInfo.ts`:
- Around line 4-10: Replace the relative import from './thor' with the project
alias-based src import (use the configured `@/` alias) so the same exports —
getAvatarForAddress, getDomainOfAddress, picassoFallback and the DomainInfo type
— are imported via the src alias (e.g. import { getAvatarForAddress,
getDomainOfAddress, picassoFallback, type DomainInfo } from '`@/`.../thor')
instead of './thor'; ensure the alias path points to the same thor module in the
src tree.

In `@cross-app-connect/src/app/cross-app/transact/TransactClient.tsx`:
- Around line 451-458: The onApprove callback (defined with useCallback)
references the localization function t() but t is missing from the dependency
array; update the dependency array for that useCallback (the onApprove callback
in TransactClient.tsx) to include t so React correctly tracks localization
changes (add t alongside client, verified, parsed, signMessage, signTypedData,
getAccessToken).

In `@cross-app-connect/src/app/i18n/config.ts`:
- Around line 6-23: Replace the relative locale imports in config.ts with the
project path-alias form (use "`@/`..." rooted at src) so all locale modules (en,
de, it, fr, es, zh, ja, ru, ro, vi, nl, ko, sv, tw, tr, hi, pt) are imported via
the alias instead of './locales/...'; update each import statement (e.g., the en
import and the other locale imports) to use "`@/app/i18n/locales/`<locale>.json"
(or the correct alias path to the i18n locales directory) so the file follows
the project's alias convention.

In `@cross-app-connect/src/app/i18n/I18nProvider.tsx`:
- Line 5: Replace the relative import in I18nProvider.tsx that imports from
'./config' with the project path alias that points to the same module (use the
'`@/`*' src-root alias), e.g. import the config module via the alias path
(targeting the same file exported by config) so the line importing i18n and
resolveLanguage uses the configured alias instead of a relative path; update the
import statement in the I18nProvider component accordingly.

In `@cross-app-connect/src/app/layout.tsx`:
- Around line 2-4: Replace the relative imports in this module with the
configured path aliases: change the import of PrivyProviderWrapper to use the
src alias (e.g., import PrivyProviderWrapper from
'`@/app/providers/PrivyProviderWrapper`' or the appropriate `@/` path to the
provider), change I18nProvider to use its alias (e.g., '`@/app/i18n/I18nProvider`'
or '`@i18n`' if configured), and update the globals.css import to use
'`@/globals.css`' so all imports (PrivyProviderWrapper, I18nProvider, and
globals.css) use the project's `@/`* alias convention.
- Around line 25-29: Remove the manual <meta name="viewport"> tag from
app/layout.tsx and instead export a Next.js App Router viewport configuration by
adding an exported constant named viewport (type Viewport) with width:
'device-width' and initialScale: 1; ensure the export is at module scope in
layout.tsx so Next.js picks it up and avoid duplicating viewport handling.

In `@cross-app-connect/src/app/page.tsx`:
- Around line 4-5: Replace the relative imports in page.tsx with path aliases:
change the VechainHeader import to use the `@components` alias (import
VechainHeader from '`@components/VechainHeader`') and change the CSS module import
(styles from './page.module.css') to use the src root alias (e.g., styles from
'`@/app/page.module.css`' or the appropriate path under `@/`* for your repo layout)
so both VechainHeader and styles use configured path aliases instead of relative
paths.

In `@packages/vechain-kit/src/providers/CrossAppErrorRecovery.tsx`:
- Line 5: Replace the relative import of useModal from './ModalProvider' with
the project path-alias import (use the `@components` alias for
component/providers): update the import that references useModal in
CrossAppErrorRecovery.tsx to import from the appropriate alias-based path (e.g.
`@components/ModalProvider` or the project's equivalent) so it follows the src
path-alias convention instead of a relative import.

In `@packages/vechain-kit/src/providers/VeChainKitProvider.tsx`:
- Line 55: In VeChainKitProvider.tsx replace the relative import "import {
CrossAppErrorRecovery } from './CrossAppErrorRecovery';" with the configured src
alias form so the module is imported via the project alias (e.g. import {
CrossAppErrorRecovery } from '`@/providers/CrossAppErrorRecovery`';), ensuring you
reference the CrossAppErrorRecovery symbol by the alias instead of a relative
path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment thread .github/workflows/cross-app-connect-check.yaml
Comment thread cross-app-connect/package.json
Comment thread cross-app-connect/src/app/components/AddressTag.tsx
Comment thread cross-app-connect/src/app/components/IdentityRow.tsx
Comment thread cross-app-connect/src/app/components/IdentityRow.tsx Outdated
Comment thread cross-app-connect/src/app/cross-app/transact/TransactClient.tsx
Comment thread cross-app-connect/src/app/page.tsx
Comment thread cross-app-connect/src/app/providers/PrivyProviderWrapper.tsx Outdated
Comment thread examples/playground/src/app/providers/VechainKitProviderWrapper.tsx
Comment thread packages/vechain-kit/src/providers/CrossAppErrorRecovery.tsx
Agilulfo1820 and others added 6 commits May 19, 2026 13:04
…sible

Per-entry opt-in for the recommended-CTA treatment:

  loginMethods={[
    { method: 'google', gridColumn: 4, isPrimary: true },
    { method: 'apple',  gridColumn: 4 },
    { method: 'more',   gridColumn: 4 },
  ]}

When no entry sets `isPrimary`, the kit still highlights the first
visible method (so default configs and minimal opt-outs get a
sensible primary CTA without thinking). `isPrimary` on `more` is
ignored — that method is a footer link, never primary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pinned `resolveLanguage()` to always return 'en' so the popup renders
in English regardless of the visitor's browser locale. The detection
logic stays in the file (unreachable after the early return) so it
can be turned back on by removing one line after the demo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The connect modal's bottom-sheet variant rendered its content flush
with the viewport bottom. On iPhones the bottom-most CTA — "More
options" especially — sat directly over the home-indicator zone,
so an attempted tap caught the system's edge-swipe gesture (Siri /
app switcher) instead.

Add `paddingBottom: max(env(safe-area-inset-bottom), 16px)` to the
scrollable container so content sits above the home indicator while
the modal surface still extends to the device edge for visual
continuity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The end-of-flow success and error screens (after domain choice,
token send, NFT transfer, profile update, swap, smart-account
upgrade, and inside the generic TransactionModal devs embed) all
rendered the same outdated treatment — a bare 100px LuCircleCheck /
LuCircleAlert centered in a VStack, no animation, four near-
identical files duplicating the layout.

Replace it with a single `StatusScreen` component in components/common:

- Soft tinted disc (~88px) holds the status icon (~44px solid). For
  success the disc bg is `vechain-kit-success-bg` (new token, 12%
  alpha of success); for error it's the existing `vechain-kit-error-bg`.
- Default icons swapped to the slightly cleaner `LuCircleCheckBig`
  (success) and `LuTriangleAlert` (error).
- Framer-motion entrance: disc fades + scales from 0.92 (220ms,
  ease-out), icon springs in with a small delay. One-shot, not the
  looping pulse that the upgrade variant used to have.
- Title centered; description rendered at 14px in
  `vechain-kit-text-secondary`, capped to ~36ch for readability.
- `actions` + `bodyExtras` + `footerExtras` slots so the existing
  callers can keep their Done buttons / Share-on-socials rows /
  explorer links without the component owning that surface.

Migrate the four duplicates to thin wrappers:

- AccountModal/SuccessfulOperationContent
- AccountModal/FailedOperationContent
- TransactionModal/TransactionModalContent (success + error branches;
  the pending and ready states keep the legacy spinner layout — they
  don't benefit from the badge treatment and the spinner has to stay
  prominent)
- UpgradeSmartAccountModal/SuccessfulOperationContent (drops the
  infinite-pulse animation in favour of the shared entrance)

Connect-modal `ErrorContent` is left alone — its 56px badge + side-
by-side Back/Try-again row is an intentional spec for that surface
and already modern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the success/error surfaces away from the crypto-y "Transaction
successful" language and add a body message that explains what
actually happened.

Defaults:
- TransactionModal success title: "Transaction successful!" →
  "Operation successful". Default body: "Your action has been
  completed and recorded on-chain." (devs can still override the
  description via `uiConfig.description`.)
- TransactionModal error default body: "An unexpected error
  occurred." → "We couldn't complete this action. Please try again."
- TransactionToast success: same title change.

Caller-specific copy (these used to all say "Transaction successful"
with no description):
- SendToken: "Tokens sent" + "{amount} {symbol} is on its way to
  {recipient}." (recipient = resolved .vet domain when available,
  else address).
- SendNft: "NFT sent" + "{nft name} is now in {recipient}'s wallet."

Swap, Profile updates, Domain set/unset already had specific
titles + descriptions — left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`extractSwapAmounts` was documented to return null when it can't pin
the swap amounts, but actually returned `{fromAmount: 0n, toAmount: 0n}`
when one side didn't match. The SwapTokenContent caller forwarded the
zeros into the description, so the user saw
"You successfully swapped 1 VET for 0 B3TR" (or "0 B3TR for X VET")
whenever strict address-matching couldn't pin one side.

Strict matching can fail when a swap is intermediated — the actual
Transfer event `from`/`to` is a router or pool contract, not the
user's address. Two fixes:

- Add a fallback heuristic: when the strict address-matching loop
  doesn't find a side, take the largest non-zero Transfer event on
  the corresponding token contract as the amount. Across the routing
  patterns we see, that movement is the user's net inflow/outflow on
  that token.

- Honor the documented contract: return null when either amount
  can't be determined (including the 0n case). The caller's existing
  `if (swapAmounts)` guard already falls back to the generic
  "You successfully swapped {fromToken} for {toToken}" message, so
  the user gets a clean fallback instead of a half-broken sentence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/vechain-kit/src/components/common/StatusScreen.tsx (1)

16-16: ⚡ Quick win

Switch local relative import to path alias.

Use the @/... alias here for consistency with repository import conventions.

Diff suggestion
-import { StickyHeaderContainer } from './StickyHeaderContainer';
+import { StickyHeaderContainer } from '`@/components/common/StickyHeaderContainer`';

As per coding guidelines, "**/src/**/*.{ts,tsx}: Use path aliases for imports: @/* for src root, @hooks for hooks, @components for components, @utils for utils`."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vechain-kit/src/components/common/StatusScreen.tsx` at line 16,
Replace the local relative import of StickyHeaderContainer with the repository
path alias; update the import for StickyHeaderContainer in StatusScreen.tsx to
use the components alias (e.g. import { StickyHeaderContainer } from
'`@components/common/StickyHeaderContainer`' or the project’s root alias
'`@/components/common/StickyHeaderContainer`') so it follows the repo convention
instead of a relative './StickyHeaderContainer' import.
packages/vechain-kit/src/components/ConnectModal/Components/LoginWithGithubButton.tsx (1)

6-7: ⚡ Quick win

Use path aliases instead of relative imports.

Please replace the new relative imports with @/... aliases to match repo conventions.

Diff suggestion
-import { RecommendedDot } from './RecommendedDot';
-import { primaryButtonStyle } from './primaryButtonStyle';
+import { RecommendedDot } from '`@/components/ConnectModal/Components/RecommendedDot`';
+import { primaryButtonStyle } from '`@/components/ConnectModal/Components/primaryButtonStyle`';

As per coding guidelines, "**/src/**/*.{ts,tsx}: Use path aliases for imports: @/* for src root, @hooks for hooks, @components for components, @utils for utils`."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/vechain-kit/src/components/ConnectModal/Components/LoginWithGithubButton.tsx`
around lines 6 - 7, Replace the relative imports for RecommendedDot and
primaryButtonStyle with the repo path aliases: change the import of
RecommendedDot (symbol RecommendedDot) and primaryButtonStyle (symbol
primaryButtonStyle) from their current "./..." relative paths to the
corresponding alias paths (e.g. use `@components/`... or `@/`... per convention) so
they import via the path-alias (for example
`@components/ConnectModal/Components/RecommendedDot` and
`@components/ConnectModal/Components/primaryButtonStyle` or the equivalent alias
that maps to those files) instead of relative paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cross-app-connect/src/app/i18n/config.ts`:
- Around line 87-92: The resolveLanguage function currently returns a hard-coded
'en', making browser locale detection unreachable; remove the unconditional
"return 'en'" so the function uses the navigator when available, keep the
server/environment guard (if typeof navigator === 'undefined') to return 'en'
for non-browser contexts, and ensure the final return falls back to 'en' by
returning normalizeBrowserTag(navigator.language) ?? 'en'; locate and edit the
resolveLanguage function to implement this change (references: resolveLanguage,
navigator, normalizeBrowserTag).

In
`@packages/vechain-kit/src/components/TransactionModal/TransactionModalContent.tsx`:
- Around line 202-209: The Confirm button is rendered when effectiveStatus ===
'ready' but onTryAgain may be undefined; update TransactionModalContent to
either render the Button only if onTryAgain is provided or render it disabled
(and/or provide a no-op) when onTryAgain is missing so the primary CTA is never
non-functional—locate the effectiveStatus === 'ready' block and change the logic
around the Button (referencing effectiveStatus, onTryAgain, and the Button
component) to guard or disable the action accordingly.

---

Nitpick comments:
In `@packages/vechain-kit/src/components/common/StatusScreen.tsx`:
- Line 16: Replace the local relative import of StickyHeaderContainer with the
repository path alias; update the import for StickyHeaderContainer in
StatusScreen.tsx to use the components alias (e.g. import {
StickyHeaderContainer } from '`@components/common/StickyHeaderContainer`' or the
project’s root alias '`@/components/common/StickyHeaderContainer`') so it follows
the repo convention instead of a relative './StickyHeaderContainer' import.

In
`@packages/vechain-kit/src/components/ConnectModal/Components/LoginWithGithubButton.tsx`:
- Around line 6-7: Replace the relative imports for RecommendedDot and
primaryButtonStyle with the repo path aliases: change the import of
RecommendedDot (symbol RecommendedDot) and primaryButtonStyle (symbol
primaryButtonStyle) from their current "./..." relative paths to the
corresponding alias paths (e.g. use `@components/`... or `@/`... per convention) so
they import via the path-alias (for example
`@components/ConnectModal/Components/RecommendedDot` and
`@components/ConnectModal/Components/primaryButtonStyle` or the equivalent alias
that maps to those files) instead of relative paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 013a446f-acd6-4bf8-94d0-e3991db7a216

📥 Commits

Reviewing files that changed from the base of the PR and between d0dc66a and 42c21a2.

📒 Files selected for processing (22)
  • cross-app-connect/src/app/i18n/config.ts
  • packages/vechain-kit/src/components/AccountModal/Contents/FailedOperation/FailedOperationContent.tsx
  • packages/vechain-kit/src/components/AccountModal/Contents/SendNft/SendNftSummaryContent.tsx
  • packages/vechain-kit/src/components/AccountModal/Contents/SendToken/SendTokenSummaryContent.tsx
  • packages/vechain-kit/src/components/AccountModal/Contents/SuccessfulOperation/SuccessfulOperationContent.tsx
  • packages/vechain-kit/src/components/ConnectModal/Components/ConnectionOptionsStack.tsx
  • packages/vechain-kit/src/components/ConnectModal/Components/LoginWithAppleButton.tsx
  • packages/vechain-kit/src/components/ConnectModal/Components/LoginWithGithubButton.tsx
  • packages/vechain-kit/src/components/ConnectModal/Components/LoginWithGoogleButton.tsx
  • packages/vechain-kit/src/components/ConnectModal/Components/VeWorldButton.tsx
  • packages/vechain-kit/src/components/ConnectModal/Components/primaryButtonStyle.ts
  • packages/vechain-kit/src/components/ConnectModal/Contents/MoreOptionsContent.tsx
  • packages/vechain-kit/src/components/TransactionModal/TransactionModalContent.tsx
  • packages/vechain-kit/src/components/TransactionToast/TransactionToastContent.tsx
  • packages/vechain-kit/src/components/UpgradeSmartAccountModal/Contents/SuccessfulOperationContent.tsx
  • packages/vechain-kit/src/components/common/BaseBottomSheet.tsx
  • packages/vechain-kit/src/components/common/StatusScreen.tsx
  • packages/vechain-kit/src/components/common/index.ts
  • packages/vechain-kit/src/languages/en.json
  • packages/vechain-kit/src/providers/VeChainKitProvider.tsx
  • packages/vechain-kit/src/theme/theme.tsx
  • packages/vechain-kit/src/utils/swap/extractSwapAmounts.ts
✅ Files skipped from review due to trivial changes (4)
  • packages/vechain-kit/src/components/common/index.ts
  • packages/vechain-kit/src/components/TransactionToast/TransactionToastContent.tsx
  • packages/vechain-kit/src/components/ConnectModal/Components/primaryButtonStyle.ts
  • packages/vechain-kit/src/languages/en.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/vechain-kit/src/components/ConnectModal/Contents/MoreOptionsContent.tsx
  • packages/vechain-kit/src/providers/VeChainKitProvider.tsx

Comment thread cross-app-connect/src/app/i18n/config.ts
Comment thread packages/vechain-kit/src/components/TransactionModal/TransactionModalContent.tsx Outdated
Agilulfo1820 and others added 5 commits May 19, 2026 14:57
…cord / …

`labelFromPrivyUser` only knew how to extract a display label from
email / phone / google.email / apple.email. A user signed in via
X (Twitter), Discord, GitHub, TikTok, LINE or Farcaster had none of
those, so the resolver fell through to the Privy DID prefix —
the transact popup's IdentityRow then rendered something like
"did:privy:cmgg8o5s400m5jy0dx2f1w1i7" under the avatar.

Extend the preference chain so every supported OAuth provider
contributes a usable label:

  email > phone
  > google.email / apple.email / github.email / discord.email /
    linkedin.email / line.email
  > @twitter.username / @farcaster.username
  > github.username / discord.username / tiktok.username
  > twitter.name / github.name / farcaster.displayName
  > truncated DID (last resort)

`IdentityRow` was also duplicating the label logic (only handling
email + google.email + id). Replace its local resolver with a call
to `labelFromPrivyUser` so the "Welcome back" greeting and the
transact identity card stay in lock-step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triage of the 18 inline review comments on PR #620 — applying the
ones with real correctness / security / privacy upside, skipping
style-only suggestions (relative-import → alias cleanups) and
intentional choices (the temporary force-English in i18n config is
for a demo).

Security:
- `CrossAppErrorRecovery`: validate `event.origin === window.location.origin`
  before honouring `vk:cross-app-no-connection`. Self-origin is enough
  because the kit's `PrivyCrossAppProvider` re-dispatches the recovery
  signal to itself via `window.postMessage` whenever signTypedData /
  signMessage reject with a stale-connection error. Cross-origin frames
  can no longer force a logout + modal reopen by impersonating the
  recovery message.

Correctness:
- `TransactClient.tsx`: wrap `JSON.parse` of the eth_signTypedData_v4
  payload in try/catch — a malformed string from the requester used
  to throw and crash the component instead of falling back to the
  "couldn't read request" screen.
- `SignInPanel.tsx`: drop the auto-submit from `PinInput.onComplete`.
  Auto-call + Verify-button click could race and fire two
  `loginWithCode` requests against Privy before `submittingCode`
  flipped. Single submission path now goes through the button, which
  respects the gating flag.
- `thor.ts` + `appConfig.ts`: replace the `as 'main' | 'test'` cast
  on `NEXT_PUBLIC_NETWORK_TYPE` with an explicit ternary. A stray env
  value (e.g. 'production') used to fall through `NETWORK[NETWORK_TYPE]`
  undefined and crash on `.nodeUrl` at runtime.
- `IdentityRow.tsx`: `walletPending` only tracks `isLoading` now. The
  old `!walletAddress || isLoading` rule made the `pendingLabel`
  placeholder branch unreachable — render fell into the skeleton even
  when resolution had settled empty.
- `TransactionModalContent.tsx`: gate the ready-state Confirm button
  on `onTryAgain` being defined, matching the error-state branch.
  Without it the primary CTA was rendered but non-functional when the
  consuming dapp didn't pass the handler.

Defensive env handling:
- `client.ts` (cross-app): explicit guard + descriptive throw when
  `NEXT_PUBLIC_PRIVY_APP_ID` is missing, mirroring the existing
  `NEXT_PUBLIC_PRIVY_DOMAIN` pattern. Same pattern in
  `PrivyProviderWrapper.tsx`. Both surfaced opaque errors before.
- `cross-app-connect/package.json`: pin `react` / `react-dom` to
  `^18.2.0` (was `^18`). Next.js 16.2.3 needs at least 18.2.0.

Privacy:
- `lastIdentity.ts`: add a 30-day TTL on the stored
  email/phone/handle. Long enough for the "Welcome back" UX to feel
  alive on a returning user, short enough that an abandoned device
  or shared browser flushes the identifier on its own. Records
  written before the TTL existed (no `savedAt`) are kept once and
  upgraded on next write.

CI:
- `cross-app-connect-check.yaml`: key concurrency by PR number, not
  by branch name, so two PRs (especially from forks) sharing a
  branch name don't cancel each other's checks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`resolveLanguage()` was pinned to always return 'en' for an upcoming
demo (ddfc7fe). Demo's done — restore the standard chain
(navigator.language → normalised tag → 'en' fallback) so the popup
picks up the device locale across all 17 shipped languages again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Agilulfo1820 Agilulfo1820 merged commit a9c9cc6 into main May 19, 2026
8 checks passed
@Agilulfo1820 Agilulfo1820 deleted the feat/cross-app-connect-whitelabel branch May 19, 2026 14:38
Agilulfo1820 added a commit that referenced this pull request May 19, 2026
useGenericDelegatorFeeEstimation and useEstimateAllTokens are restored to
their pre-PR (2.9.0) behavior: they still call the delegator's
/estimate/clauses endpoint and use the returned transactionCost as-is for
the balance check.

Reason: routing the local thor.gas.estimateGas through these hooks made
the Confirm-transfer page sit on a spinner without ever firing the
delegator network call. The send path keeps the locally-corrected
gas-token amount (via computeCorrectedGasTokenCost) so on-chain transfers
still cover the actual cost; the UI just shows the (occasionally
under-estimated) value the delegator returns.

Trade-off accepted for now: fee UI and on-chain transfer amount may
diverge slightly. Will revisit once the upstream regression in #620
(host Privy app id swap + cross-app session shape) is sorted out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Agilulfo1820 added a commit that referenced this pull request May 19, 2026
The old gate (`privy !== undefined || loginMethods includes
'vechain'/'ecosystem'`) left `feeDelegation` undefined whenever a
consumer only listed Google/Apple/Twitter/etc. without a `privy` prop.
After #620 those buttons silently fall back to VeChain's whitelabel
cross-app host, so the user ends up on a smart-account wallet that
needs fee delegation — but `useGenericDelegatorFeeEstimation` and
`useEstimateAllTokens` stay permanently disabled because they require
`feeDelegation?.genericDelegatorUrl`, and the spinner on the Confirm
page never resolves.

Drop the gate: always seed `genericDelegatorUrl` when the consumer
didn't pick a delegation strategy. dapp-kit wallets ignore it,
smart-account users get a working default.

Also drops the temporary debug console.logs from the two hooks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Agilulfo1820 added a commit that referenced this pull request May 20, 2026
…623)

* fix(generic-delegator): correct gas estimation for smart-account txs

The generic delegator's /estimate/clauses endpoint simulates the user's
raw clauses without the executeWithAuthorization wrapper and without the
embedded-wallet typed-data signature, so it under-estimates gas (and for
NFT-heavy clauses can revert outright in simulation). The returned
transactionCost was then used to build the transfer clause that pays the
delegator's deposit account, leaving it insufficient to cover the actual
on-chain gas. The failure was masked by a catch-all that surfaced a
misleading "no gas tokens have sufficient balance" error even when the
wallet had ample balance.

Mirror VeWorld mobile's approach: trust the delegator only for the
gas-token-per-gas rate (a market price independent of the gas amount),
recompute the gas number locally with thor.gas.estimateGas (caller =
smart account), add a fixed wrapper overhead per smart-account version
plus a fixed fee-payer overhead per gas token, apply a 1.1 safety
multiplier, then reapply the rate. New shared helper
computeCorrectedGasTokenCost is used by the send path, the fee-estimation
UI hook, and the all-tokens hook so they all agree.

Also revert the homepage/playground feeDelegation override introduced in
the cross-app PR. It set feeDelegation.delegatorUrl (dApp-sponsored mode)
to the generic-delegator URL, which routed transactions through the
dapp-kit fee-delegator path that POSTs to the bare base URL and 404s.
With no override the kit's default getGenericDelegatorUrl() takes over
and the request hits the correct /api/v1/sign/transaction/authorized/...
endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(generic-delegator): hoist local gas estimate out of token loop + timeout

The fee-estimation UI hooks (useGenericDelegatorFeeEstimation,
useEstimateAllTokens) were calling thor.gas.estimateGas INSIDE the
per-token loop, multiplying the local RPC round-trips by the number of
candidate gas tokens. Worse, if a single estimate hung — slow mainnet
RPC, unusual clause shape — the entire queryFn never resolved and the
"Confirm transfer" page sat on a spinner forever.

Split computeCorrectedGasTokenCost into two helpers:
- computeCorrectedTotalGasNoFeePayer: runs the local Thor estimate once,
  bounded by a 6s timeout (returns null on timeout/failure so callers
  can fall back to a delegator-derived value).
- convertGasToGasTokenAmount: pure function, applies the per-token
  fee-payer overhead and the delegator's per-gas rate.

Both fee-estimation hooks now call the gas helper once outside the
loop, then convert per-token inside. computeCorrectedGasTokenCost
remains a thin wrapper over the two, used by the send path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert: keep fee-estimation UI hooks unchanged

useGenericDelegatorFeeEstimation and useEstimateAllTokens are restored to
their pre-PR (2.9.0) behavior: they still call the delegator's
/estimate/clauses endpoint and use the returned transactionCost as-is for
the balance check.

Reason: routing the local thor.gas.estimateGas through these hooks made
the Confirm-transfer page sit on a spinner without ever firing the
delegator network call. The send path keeps the locally-corrected
gas-token amount (via computeCorrectedGasTokenCost) so on-chain transfers
still cover the actual cost; the UI just shows the (occasionally
under-estimated) value the delegator returns.

Trade-off accepted for now: fee UI and on-chain transfer amount may
diverge slightly. Will revisit once the upstream regression in #620
(host Privy app id swap + cross-app session shape) is sorted out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* debug(generic-delegator): log enabled-inputs to identify falsy gate

Temporary console.log in useEstimateAllTokens and
useGenericDelegatorFeeEstimation to confirm which input is making the
react-query disabled (status=pending, fetchStatus=idle).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(provider): always auto-inject generic delegator URL

The old gate (`privy !== undefined || loginMethods includes
'vechain'/'ecosystem'`) left `feeDelegation` undefined whenever a
consumer only listed Google/Apple/Twitter/etc. without a `privy` prop.
After #620 those buttons silently fall back to VeChain's whitelabel
cross-app host, so the user ends up on a smart-account wallet that
needs fee delegation — but `useGenericDelegatorFeeEstimation` and
`useEstimateAllTokens` stay permanently disabled because they require
`feeDelegation?.genericDelegatorUrl`, and the spinner on the Confirm
page never resolves.

Drop the gate: always seed `genericDelegatorUrl` when the consumer
didn't pick a delegation strategy. dapp-kit wallets ignore it,
smart-account users get a working default.

Also drops the temporary debug console.logs from the two hooks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(generic-delegator): use locally-corrected gas in fee-estimation UI

Reapplies the original plan after the auto-inject regression in
VeChainKitProvider was fixed:

useGenericDelegatorFeeEstimation and useEstimateAllTokens now compute
the gas-token cost via computeCorrectedTotalGasNoFeePayer (one local
thor.gas.estimateGas, bounded by a 6s timeout, gas-token-agnostic) plus
convertGasToGasTokenAmount (delegator-derived per-gas rate + per-token
fee-payer overhead). This keeps the fee shown in the Confirm-transfer
UI consistent with the value the send path actually transfers to the
generic delegator's deposit account, so the balance check and the
on-chain transfer agree.

If the local estimate times out or fails, fall back to
`delegator.transactionCost * 2` so the UI stays responsive even when
the Thor node is slow or the simulation reverts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cross-app): relabel generic-delegator fee transfers as "Pay transaction fee"

The transact page shows every clause the user is asked to sign. When the
requester routes through the generic delegator, one of those clauses is
a VET / VTHO / B3TR / VOT3 transfer to the delegator's deposit account
— previously rendered as "Send 0.21 VET to 0x86…c6fa", which a non-
crypto user reads as a separate transfer rather than a gas-payment
side effect.

Resolve the delegator's current deposit account once per page load
(cached via fetchGenericDelegatorDepositAccount) and pass it to the
decoder. Clauses whose recipient matches the deposit account are now
emitted as a known_action with the new `fee` category, summary
"Pay transaction fee" / "Paga fee per transazione", and a detail line
showing the amount + symbol routed to the gas payer.

If the delegator endpoint is unreachable, fetchGenericDelegatorDepositAccount
returns null and the decoder falls back to the original Send-style label
— no regression for offline / down delegator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(domains/profile): kit pays gas for onboarding actions

VeChain now sponsors the gas for first-time onboarding flows so users
without VTHO / B3TR / VET can still complete them. Routes claim-domain,
claim-veworld-subdomain, and update-text-record transactions through
the new kit-sponsored vechain.energy delegator:

- mainnet: https://sponsor.vechain.energy/by/1060
- testnet: https://sponsor-testnet.vechain.energy/by/221

Surfaces a new `getKitSponsoredDelegatorUrl()` helper alongside the
existing `getGenericDelegatorUrl()`. The three hooks pass the sponsored
URL as the per-transaction `delegationUrl` override, so the rest of the
kit's delegation plumbing (PrivyWalletProvider.signAndSend) routes
through the dApp-sponsored path automatically.

ChooseNameSummaryContent and CustomizationSummaryContent gate the
gas-token UI on a new `KIT_PAYS_GAS = true` flag: the
useGenericDelegatorFeeEstimation call is disabled, the GasFeeSummary
section hidden, "has enough gas balance" is forced true, and the
Confirm button is no longer waiting for an estimate. The user just
sees the action confirmation; no fee disclosure since they pay
nothing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(theme): pin Input/Textarea font-size to 16px to stop iOS auto-zoom

Mobile Safari zooms the page whenever a focused input's computed
font-size is below 16px CSS pixels. The kit's `md` font token
resolves to 14px (see tokens.ts:573 — small=12, medium=14, large=16),
so Chakra's default Input/Textarea inherits 14px through fontSize="md"
and triggers the zoom on every form in the modal (Customization,
SendToken, SendNft, Swap, FAQ search, etc).

New theme/input.ts registers Input and Textarea component themes that
pin field font-size to a hard 16px. Anchored in absolute pixels rather
than `lg` so future token tweaks (e.g. shrinking large to 15px) can't
silently regress this.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(gas-fee-selector): coherent hover surface in both color modes

GasFeeTokenSelector item _hover was effectively `'#ffffff12'` for the
backgroundColor in every mode (the `textSecondary ? '#ffffff12' :
textSecondary` ternary is always truthy since useToken never returns
falsy), and used the text-secondary color as the hover border. White-
on-white was invisible in light mode, and borrowing a text color for a
border was inconsistent with the rest of the kit's hover patterns.

GasFeeSummary's outline-style token Button set `_hover.bg =
textSecondary` while keeping `color = textSecondary`, so on hover the
chip's label disappeared (same colour as its background) — most visible
in light mode where textSecondary is a dark grey.

Both components now derive a subtle hover overlay from
useVeChainKitConfig().darkMode:
- dark mode: `rgba(255, 255, 255, 0.08)` (lighten)
- light mode: `rgba(0, 0, 0, 0.04)` (darken)

GasFeeSummary additionally bumps the button text + border to
textPrimary on hover, restoring contrast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(gas-fee-selector): tone down selected-row background

The selected item used `textTertiary` as its background. That token is
a text colour — `#718096` (mid-grey) in light mode and rgba(223,223,221,
0.5) (near-white at 50%) in dark mode — which read as a too-strong
grey-on-white in light mode and an almost-white-on-dark slab in dark
mode.

Replace with the same alpha-overlay pattern used for hover, one notch
stronger so 'selected' still reads above 'hover':
- dark: rgba(255, 255, 255, 0.12) (hover sits at 0.08)
- light: rgba(0, 0, 0, 0.06) (hover sits at 0.04)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(choose-name): only sponsor gas for claim, not for unset

ChooseNameSummaryContent set KIT_PAYS_GAS = true unconditionally,
which hid the GasFeeSummary chip, forced hasEnoughGasBalance true,
disabled the gas-estimation error banner, and bypassed the loading
gate on the Confirm button.

But the component routes to useUnsetDomain when isUnsetting=true
(see the unsetDomainHook / vetDomainHook / veWorldSubdomainHook
switch around line 103), and useUnsetDomain does NOT pass the
kit-sponsored delegator URL — the unset path still runs through the
user-pays generic delegator. Result: the user got the
"VeChain pays" UI but the tx still tried to debit their own VTHO.

Compute KIT_PAYS_GAS = !isUnsetting so the unset path falls back to
the normal fee-token UI / balance check / estimation-error display.
All five downstream gates (shouldEstimateGas,
disableConfirmButtonDuringEstimation, hasEnoughBalance, GasFeeSummary
render, showGasEstimationError) already read KIT_PAYS_GAS and need
no additional changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(send-token): auto-adjust amount when sending token == gas token

When the user tries to send (almost) their entire balance of a token
that is ALSO the only viable gas token, the fee-estimation iteration
in useGenericDelegatorFeeEstimation rejects every candidate:
- the sending token fails because balance < cost + amount
- the other gas tokens fail because the user doesn't have any
The summary used to surface the resulting error and leave the Confirm
button disabled, forcing the user to go back and manually trim the
amount.

Mirror veworld-mobile's SummaryScreen.tsx co-spend handling: pull the
per-token gas cost via useEstimateAllTokens, and when the iteration
errors out AND the sending token is one of the available gas tokens
with enough balance for the gas alone, set adjustedAmount =
balance - gas * 1.05 (5% safety buffer). The effectiveAmount drives:

- useTransferERC20 / useTransferVET (clauses rebuild with the dropped
  amount, so the on-chain transfer matches what we display)
- useGenericDelegatorFeeEstimation's sendingAmount (the iteration
  re-runs and now the sending token wins)
- the Amount row in the summary body
- the success-screen description

A blue info-style banner above the Amount row shows
"Amount adjusted from {{original}} to {{adjusted}} {{symbol}} to cover
the transaction fee." so the user understands why the displayed total
shrank between the entry step and the summary.

Swap intentionally NOT included: changing the from-amount in a swap
would invalidate the quote/slippage, so VeWorld's auto-adjust pattern
doesn't fit cleanly there. Left as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant