feat: VeChain whitelabel cross-app connect host#620
Conversation
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>
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>
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>
There was a problem hiding this comment.
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 winNormalize
NEXT_PUBLIC_NETWORK_TYPEbefore 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 causesNETWORK_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 winReplace deprecated
word-break: break-wordwith modern alternative.Stylelint correctly flags
word-break: break-wordas deprecated at lines 279, 302, and 332. Useoverflow-wrap: break-wordfor 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 winRemove unused
KnownActionimport.ESLint correctly flags that
KnownActionis imported but never used. OnlyKnownActionCategoryandKnownActionDataare 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 winKnown router contracts are currently unlabelled.
knownContractsincludes DEX router addresses, but they’re missing fromAPP_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 winLocalize the digit
aria-labeltext.
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 winRemove unused imports.
OAUTH_PROVIDERSandOAuthProviderare 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
OAuthProvideris 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 winFix doc behavior matrix for no-
privymode (more).These lines conflict with the current implementation:
privy, whilemoreis allowed withoutprivy. 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
includeentries underdist/**are neutralized byexclude: ["dist"].Line 44 adds
dist/dev/types/**/*.ts, but Line 49 excludes the wholedistdirectory, 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 winAdd 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.mdaround 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., changetodotenv) 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-nextmust 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().useris 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
fulllabel, and ensure reads check the TTL and reject expired entries; update
the functions/logic that read/write the last-identity (the code paths
referencinglabel,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 thetypedData,primaryType,
isSmartAccountAuth, andparseClauseslogic).- 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 ``` toIn `@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
…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>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/vechain-kit/src/components/common/StatusScreen.tsx (1)
16-16: ⚡ Quick winSwitch 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,@hooksfor hooks,@componentsfor components,@utilsfor 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 winUse 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,@hooksfor hooks,@componentsfor components,@utilsfor 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
📒 Files selected for processing (22)
cross-app-connect/src/app/i18n/config.tspackages/vechain-kit/src/components/AccountModal/Contents/FailedOperation/FailedOperationContent.tsxpackages/vechain-kit/src/components/AccountModal/Contents/SendNft/SendNftSummaryContent.tsxpackages/vechain-kit/src/components/AccountModal/Contents/SendToken/SendTokenSummaryContent.tsxpackages/vechain-kit/src/components/AccountModal/Contents/SuccessfulOperation/SuccessfulOperationContent.tsxpackages/vechain-kit/src/components/ConnectModal/Components/ConnectionOptionsStack.tsxpackages/vechain-kit/src/components/ConnectModal/Components/LoginWithAppleButton.tsxpackages/vechain-kit/src/components/ConnectModal/Components/LoginWithGithubButton.tsxpackages/vechain-kit/src/components/ConnectModal/Components/LoginWithGoogleButton.tsxpackages/vechain-kit/src/components/ConnectModal/Components/VeWorldButton.tsxpackages/vechain-kit/src/components/ConnectModal/Components/primaryButtonStyle.tspackages/vechain-kit/src/components/ConnectModal/Contents/MoreOptionsContent.tsxpackages/vechain-kit/src/components/TransactionModal/TransactionModalContent.tsxpackages/vechain-kit/src/components/TransactionToast/TransactionToastContent.tsxpackages/vechain-kit/src/components/UpgradeSmartAccountModal/Contents/SuccessfulOperationContent.tsxpackages/vechain-kit/src/components/common/BaseBottomSheet.tsxpackages/vechain-kit/src/components/common/StatusScreen.tsxpackages/vechain-kit/src/components/common/index.tspackages/vechain-kit/src/languages/en.jsonpackages/vechain-kit/src/providers/VeChainKitProvider.tsxpackages/vechain-kit/src/theme/theme.tsxpackages/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
…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>
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>
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>
…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>
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' }(orapple,twitter, …) inloginMethodswith noprivyprop, 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 packageA 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 postsPRIVY_CROSS_APP_CONNECT_RESPONSEback to the opener./cross-app/transact— signing surface; decrypts the request, decodes calldata into a plain-language summary, signs, postsPRIVY_CROSS_APP_ACTION_RESPONSE.Key features:
vechain/app-hubmanifest cached at build time so we renderConfirm token swap on Nubilainstead of a raw URL.navigator.language.PRIVY_CROSS_APP_ACTION_ERRORwith avk:cross-app-no-connectionmarker on close. The kit catches it and auto-runs logout + reopen-connect-modal.useLoginWithOAuthanduseLoginWithSmshooks behind our own picker. Users never see Privy's modal, even on session expiry inside/transact.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 changesuseLoginWithOAuth— when the consumer doesn't pass aprivyprop, routes Google/Apple/Twitter/Discord/GitHub/TikTok/LINE through the whitelabel popup via the cross-app intent param (?intent=googleetc.) instead of throwing.PrivyCrossAppProvider—appendIntenthelper 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 onsignTypedData/signMessageand re-dispatches them as the recovery event.CrossAppErrorRecovery— render-less component mounted insideModalProviderthat listens forvk:cross-app-no-connectionmessages and runsuseWallet().disconnect()+openConnectModal()automatically.VECHAIN_PRIVY_APP_IDfallback — when noprivyprop is supplied, the kit'sPrivyProvidernow boots with VeChain's app ID so the embedded Privy SDK can authenticate against the same account the popup uses.{ method: 'google' },'apple','twitter','discord','github','tiktok','line'render even whenprivyisn't set (previously hidden / threw).CI & deploy
.github/workflows/cross-app-connect-check.yaml—pull_request_targetwith the existingsafe-to-buildlabel gate, scoped tocross-app-connect/**,packages/vechain-kit/**,yarn.lock. Runsyarn workspace cross-app-connect typecheck+build..github/workflows/cross-app-connect-deploy.yaml—pushtomainandworkflow_dispatch. Static-builds the popup, adds.nojekyll, uploads viaactions/upload-pages-artifact, deploys viaactions/deploy-pages.concurrency: pagesqueues 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 (plusworkflow_dispatch). Runsyarn generate-app-hubagainstvechain/app-hub; if the resultingapp-hub.jsondiffers from the committed copy, opens (or updates) a single chore PR onchore/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
README.mdupdated to list the no-Privy social providers and link to the popup README.cross-app-connect/README.mdadded — 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):
connection-types.md,quickstart/setup-privy-optional.md,quickstart/provider-configuration.md(both GitBook root +docs/copies). Reframes Privy as opt-in for email/passkey/SMS/custom OAuth.kit-social-login.mdskill reference so AI assistants stop telling users they need Privy for Google login. Adds a head-to-head comparison table between the whitelabel cross-app path and self-hosted Privy.Test plan
yarn dev:cross-app-connectboots both kit watch and popup on:3001{ method: 'google' }direct button → popup opens straight to Google OAuth (intent param honored)privy-caw:*:connectionfrom popup localStorage between two sign-ins → popup shows "Reconnection needed" → click Close → kit auto-logs-out and reopens the connect modalcross-app-connect-check.yamlruns on a test PR; type-check + build passcross-app-connect-deploy.yaml; artifact uploads; GH Pages serves itcross-app-connect-refresh-app-hub.yamlviaworkflow_dispatchon a copy of main with the cache deleted; verify it opens a PR with the regenerated fileOne-time GitHub setup after merge
NEXT_PUBLIC_PRIVY_APP_ID,NEXT_PUBLIC_PRIVY_CLIENT_ID,NEXT_PUBLIC_PRIVY_DOMAINNEXT_PUBLIC_BASE_PATH=/vechain-kitso the popup serves atvechain.github.io/vechain-kit/. Remove once a custom domain is wired.VECHAIN_PRIVY_APP_ID.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Localization
Chores
Documentation