Skip to content

Feat/cross app connect whitelabel#620

Draft
Agilulfo1820 wants to merge 53 commits into
mainfrom
feat/cross-app-connect-whitelabel
Draft

Feat/cross app connect whitelabel#620
Agilulfo1820 wants to merge 53 commits into
mainfrom
feat/cross-app-connect-whitelabel

Conversation

@Agilulfo1820
Copy link
Copy Markdown
Member

No description provided.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Restructure as a phase machine computed up-front:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

coderabbitai Bot commented May 15, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc4edf7e-29a0-4ebd-a906-d52e416ca5a4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cross-app-connect-whitelabel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Agilulfo1820 and others added 3 commits May 15, 2026 16:52
The previous decoder fell back to "Send X tokens" for any ERC-20
contract not in the static address book -- which meant common cases
like USDT, WoV, Tessera token, custom dApp tokens all rendered
without their actual ticker. The user's example
0xa9059cbb... transfer hit this path.

Mirror VeWorld mobile's approach: when the address book misses,
read symbol() and decimals() live from Thor and cache the result.
Uses the kit's executeCallClause helper (re-exported via
@vechain/vechain-kit/utils) over a viem-typed ERC-20 metadata ABI,
two parallel reads per unknown token. Results memoized in a
module-level Map so the same token across multiple clauses (or
re-renders during the OAuth round-trip) hits cache.

Lookup order is now:
  1. Static address book (VET / VTHO / B3TR / VOT3 -- instant)
  2. In-memory cache from a previous live lookup
  3. Live Thor read of symbol() + decimals()
  4. Generic "tokens" / 18 decimals fallback if Thor errors

The transact page passes useThor() into decodeClause() and adds it
to the decode effect's deps so the first render with a Thor client
triggers the lookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modern wallets (Rabby, MetaMask) show a balance-change summary before
the user signs -- "USDC 1,234.50 -> 1,224.50 (-10)". Add the same to
the transact screen so non-crypto users can see what's actually
leaving their wallet before they tap Continue.

cross-app-connect/src/app/cross-app/_lib/simulate.ts aggregates
predictable deltas from the decoded clauses:

  - native_transfer -> -amount VET
  - token_transfer  -> -amount on that token
  - token_approve   -> no balance prediction (allowance only)
  - unknown         -> marks the whole simulation as partial

For each token with a delta, it reads the user's current balance via
Thor (thor.accounts.getAccount for VET, balanceOf via the kit's
executeCallClause for ERC-20). Renders as:

    VET   100.0  ->  95.0    -5.0
    USDC  1,234  ->  1,224   -10

When any clause is `unknown` (couldn't be decoded), the panel says
"Other changes are possible -- this app called something we couldn't
simulate" so the preview never pretends to be exhaustive. When the
simulation finds nothing predictable, it says so explicitly.

Cross-app smart-account flows are fee-delegated, so the panel adds a
quiet "Network fees are covered for you" footnote instead of a VTHO
gas line. If decoding flagged unknowns, the footnote is suppressed --
no point promising "free" when we don't fully know what's happening.

A small Spinner-with-text placeholder ("Checking how this affects
your balance...") shows while the Thor reads are in flight, so the
UI doesn't jump when results arrive.

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

github-actions Bot commented May 15, 2026

🚀 Preview environment deployed!

Preview URL: https://preview.vechainkit.vechain.org/featcross-app-connect-whitelabel

Three transact-screen UX fixes:

1. Recipient address resolution. cross-app-connect/_lib/contracts.ts
   walks the kit's appConfig and labels every VeChain-maintained
   contract by name -- B3TR Token, VeBetter Treasury, X2Earn Rewards
   Pool, Stargate, Smart Account Factory, VeChain Domains, ...
   The new AddressTag component (components/AddressTag.tsx) renders:
     - Verified contract -> friendly label + green check tooltip
     - User's own address -> "Your account" + green check
     - Anything else -> truncated hex + orange warning ("Unverified
       contract -- make sure you trust it before continuing")
   Wired into every place the page used to dump truncated hex: the
   "To" / "Spender" line under each ActionRow, and the per-clause
   "to" row inside the Inspect details panel.

2. value 0x0 / data 0x dev noise. In the Inspect details:
     - value rows render as "0.5 VET" (formatUnits) instead of raw
       hex, and disappear entirely when BigInt(value) === 0.
     - data is hidden by default behind a per-clause "Show raw
       calldata" link button. Click to expand a Collapse with the
       full hex (no more arbitrary 80-char truncation).

3. Clause count visibility. The Inspect heading is now
   "Clause" / "Clauses (3)" depending on count, and each row labels
   itself "Clause 2 of 3 · to" so the user can tell whether more
   are hidden.

Removed the leftover describeDetail() helper; replaced by the new
ActionRowDetail subcomponent that emits an AddressTag for the
recipient/spender slot.

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

github-actions Bot commented May 15, 2026

Size Change: +15.5 kB (+0.18%)

Total Size: 8.71 MB

Filename Size Change
packages/vechain-kit/dist/index-B5lPKVc-.d.mts 0 B -177 kB (removed) 🏆
packages/vechain-kit/dist/index-B5lPKVc-.d.mts.map 0 B -48.6 kB (removed) 🏆
packages/vechain-kit/dist/index-B7W1mM8I.d.mts 0 B -5.63 kB (removed) 🏆
packages/vechain-kit/dist/index-B7W1mM8I.d.mts.map 0 B -2.99 kB (removed) 🏆
packages/vechain-kit/dist/index-C8Uj8ple.d.cts 0 B -5.63 kB (removed) 🏆
packages/vechain-kit/dist/index-C8Uj8ple.d.cts.map 0 B -2.99 kB (removed) 🏆
packages/vechain-kit/dist/index-DHPT-508.d.cts 0 B -177 kB (removed) 🏆
packages/vechain-kit/dist/index-DHPT-508.d.cts.map 0 B -48.6 kB (removed) 🏆
packages/vechain-kit/dist/index.cjs.map 2.79 MB +6.31 kB (+0.23%)
packages/vechain-kit/dist/index.mjs.map 2.72 MB +6.25 kB (+0.23%)
packages/vechain-kit/dist/index-Bekk3wer.d.cts 5.63 kB +5.63 kB (new file) 🆕
packages/vechain-kit/dist/index-Bekk3wer.d.cts.map 2.99 kB +2.99 kB (new file) 🆕
packages/vechain-kit/dist/index-BYfc-9J5.d.mts 5.63 kB +5.63 kB (new file) 🆕
packages/vechain-kit/dist/index-BYfc-9J5.d.mts.map 2.99 kB +2.99 kB (new file) 🆕
packages/vechain-kit/dist/index-D4ZuZvbE.d.cts 178 kB +178 kB (new file) 🆕
packages/vechain-kit/dist/index-D4ZuZvbE.d.cts.map 48.7 kB +48.7 kB (new file) 🆕
packages/vechain-kit/dist/index-tsgg6Bt8.d.mts 179 kB +179 kB (new file) 🆕
packages/vechain-kit/dist/index-tsgg6Bt8.d.mts.map 47.3 kB +47.3 kB (new file) 🆕
ℹ️ View Unchanged
Filename Size Change
packages/vechain-kit/dist/assets 4.1 kB 0 B
packages/vechain-kit/dist/assets-BL24r-Yp.mjs 51.3 kB 0 B
packages/vechain-kit/dist/assets-BL24r-Yp.mjs.map 74.1 kB 0 B
packages/vechain-kit/dist/assets-DNJsQD7_.cjs 58.5 kB 0 B
packages/vechain-kit/dist/assets-DNJsQD7_.cjs.map 75.5 kB 0 B
packages/vechain-kit/dist/assets/index.cjs 716 B 0 B
packages/vechain-kit/dist/assets/index.d.cts 973 B 0 B
packages/vechain-kit/dist/assets/index.d.mts 973 B 0 B
packages/vechain-kit/dist/assets/index.mjs 718 B 0 B
packages/vechain-kit/dist/index.cjs 1.13 MB +569 B (+0.05%)
packages/vechain-kit/dist/index.d.cts 23.9 kB +110 B (+0.46%)
packages/vechain-kit/dist/index.d.mts 23.9 kB +110 B (+0.46%)
packages/vechain-kit/dist/index.mjs 1.09 MB +557 B (+0.05%)
packages/vechain-kit/dist/utils 4.1 kB 0 B
packages/vechain-kit/dist/utils-B1rpHKZq.mjs 22.1 kB 0 B
packages/vechain-kit/dist/utils-B1rpHKZq.mjs.map 66.7 kB 0 B
packages/vechain-kit/dist/utils-D0w5dcVX.cjs 27.3 kB 0 B
packages/vechain-kit/dist/utils-D0w5dcVX.cjs.map 67.4 kB 0 B
packages/vechain-kit/dist/utils/index.cjs 1.98 kB 0 B
packages/vechain-kit/dist/utils/index.d.cts 3.04 kB 0 B
packages/vechain-kit/dist/utils/index.d.mts 3.04 kB 0 B
packages/vechain-kit/dist/utils/index.mjs 2 kB 0 B

compressed-size-action

Agilulfo1820 and others added 18 commits May 15, 2026 17:11
Five polish items on the transact screen:

- Friendly primaryType label. _lib/labels.ts.humanPrimaryType maps
  ExecuteWithAuthorization / ExecuteBatchWithAuthorization to
  "Authorized call" / "Authorized batch call". The Inspect "Type"
  badge now reads that instead of SHOUTING_CAPS.

- Plain-English summary sentence. _lib/labels.ts.summarizeActions
  reads the decoded clauses and writes one sentence like:
    "You're about to give this app unlimited access to your tokens."
    "You're sending tokens out of your wallet in 3 steps."
    "You're approving 4 actions, some of which we couldn't fully
     verify."
  Renders as the VechainHeader subtitle in place of the bland
  "This app wants to:" label. Goal: dramatically reduce the
  "what's this going to do?" support load.

- Title shield icon. VechainHeader accepts an optional `titleIcon`
  prop. The transact screen uses LuShieldCheck colored with the
  brand accent so the title visually anchors the security framing.

- Smart-account chip with copy + balance. New AccountChip component
  sits at the top of the transact card body: wallet icon, truncated
  address (with a tooltip-d Copy button via useClipboard), and the
  user's live VET balance fetched from Thor on mount. Mirrors the
  "0x129...60ef . 1,240 VET" pattern modern wallets use.

- AddressTag now distinguishes 'contract' from 'recipient'. For
  token transfers, the "to" argument is a destination wallet, not a
  contract -- showing "Unverified contract" there was a bug
  (flagged by user). 'recipient' mode renders the truncated address
  without the warning badge; if the address happens to resolve via
  appConfig it still gets a label + check. 'contract' mode
  (token_approve spender, raw clause `to` in Inspect, the actual
  contract the user is calling) keeps the verified/unverified
  phishing defence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the kit's address display pattern from VeWorld:

    [dan.vet avatar]   dan.vet
                       0x3f90...Dcee

instead of bare truncated hex. Powered by the kit's existing
useVechainDomain + useGetAvatarOfAddress hooks -- which already
handle the VNS lookup (.vet domain resolution), the avatar custom
field on the resolver, and a Picasso-generated identicon fallback
when neither is set.

Updated components:

- AddressTag: when the address isn't a verified VeChain contract,
  render `[avatar] [domain or truncated]`. Verified contracts keep
  the label-plus-check treatment (the brand is implicit in the name,
  no avatar needed). `kind='contract'` still surfaces the orange
  "Unverified contract" warning; `kind='recipient'` does not.

- AccountChip: identicon / domain avatar to the left of the address
  block, domain as the primary label when set (truncated hex demoted
  to a small caption underneath), VET balance right-aligned. Mirrors
  the wallet card layout from the VeWorld screenshot the user shared.

React Query caches results across instances, so rendering N
AddressTags pointing at the same address triggers one fetch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per user request, remove the "Your wallet will" before/after balance
preview that was added in 4471de1. Specifically:

- Delete cross-app/_lib/simulate.ts (the aggregate-deltas-then-fetch
  flow).
- Remove the simulation state, the decoded-clauses -> simulate effect,
  the BalanceChangeSection + BalanceChangeRow components, and the
  Simulation type import from the transact page.

The AccountChip still shows the user's live VET balance, the
balanceOf-driven token decoding in decoder.ts still resolves symbol +
decimals for unknown tokens, and the action list still reads in plain
language -- so the user knows what's being sent, just without the
"100 -> 90" preview row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the consumer dApp doesn't pass a privy prop the kit was mounting
PrivyProvider with a dummy app id (clzdb5k0b02b9qvzjm6jpknsc) plus a
dummy client id. The PrivyProvider hits POST /api/v1/sessions on
mount and any deploy whose origin wasn't on the dummy app's allow
list got back 403 -- stalling the connect button forever (most
recently spotted on preview.vechainkit.vechain.org).

Use VECHAIN_PRIVY_APP_ID instead. That app id:
  - is the same one the whitelabel cross-app-connect host serves, so
    the requester and host are consistent;
  - has the full kit-ecosystem allowed origins list maintained by the
    VeChain team, which covers preview / staging / production
    domains and localhost;
  - keeps the cross-app OAuth fallback intact, because that fallback
    is gated on the `privy` prop in useVeChainKitConfig() -- not on
    which app id is actually mounted in PrivyProvider.

clientId drops to '' (the previous dummy client id was scoped to the
dummy app and wouldn't authenticate against VECHAIN_PRIVY_APP_ID
anyway; the PrivyProvider treats it as "web flow, no multi-env
client").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five UX upgrades on the transact screen, plus consistency fixes:

1. Risk-adaptive shield. computeRisk() classifies the batch into
     safe -- everything decoded cleanly
     caution -- one of (unknown clause | unlimited approve)
     danger -- both, or the page is in a blocked state
   The title icon swaps to match: LuShieldCheck/accent (safe),
   LuShieldAlert/orange.400 (caution), LuShieldX/red.400 (danger).
   VechainHeader now takes a titleIconColor prop driven by this map.

2. Specific titles instead of generic "Confirm action". titleForActions
   returns "Send tokens", "Approve spending", "Interact with contract",
   "Confirm N actions" (mixed batches), or "Action blocked" -- so the
   user lands on the page already knowing what kind of thing they're
   about to do. The plain-English summary now demotes to subtitle.

3. Risk-aware Continue copy. continueLabel: "Continue" / "Continue
   anyway" / "I understand, continue". The strongest verb only fires
   when both warnings stack -- otherwise it'd cry wolf.

4. Token-aware AccountChip. uniqueTokensFromDecoded() collects every
   ERC-20 the batch touches; AccountChip now batches one balanceOf
   per token alongside the native VET read, so the chip reads
   "1,240 B3TR · 12 VET" instead of just "12 VET" when the batch is
   moving B3TR.

5. Action row redesign. Drop the round LuArrowUpRight / LuShieldCheck
   icon container -- the strong title carries the action already.
   Each row now has a subtle left-border (2px, accent-colored on
   warning-bearing rows) so risk scans at a glance without
   decorative chrome.

Also: AddressTag now renders the avatar (Picasso identicon by default)
next to verified-contract labels too, not just unresolved addresses.
Verified spenders / contracts no longer feel like a different family
from "dan.vet" two rows away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously rendered in dev automatically; now opt-in via the
NEXT_PUBLIC_SHOW_COLOR_MODE_TOGGLE=true env var so it's out of the
way during normal flow testing. Toggle behaviour itself unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use the square VeChain logomark (V monogram) instead of the
horizontal wordmark. The mark is more compact on the popup's narrow
card width and matches what Privy's hosted UI does (logomark above
title). Both light + dark variants already lived under public/brand
from the original logos.zip extract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returning-user view on the connect page used to show a flat
"Signed in as <email> / Wallet 0xabc..." block reading the embedded
EOA. Three upgrades:

1. Identity row -- avatar + .vet domain + smart-account address.
   useSmartAccount(embedded.address) resolves the actual VeChain
   smart-account address (what the user identifies as on-chain).
   useVechainDomain + useGetAvatarOfAddress reuse the kit hooks to
   surface the .vet name and avatar / Picasso identicon. Display
   only -- acceptConnection still passes the embedded EOA address,
   which is what the cross-app session protocol expects.

2. Linked-social badges. linkedSocials() walks user.{google,
   apple, twitter, discord, github, tiktok, line, phone, farcaster}
   and renders a small brand-coloured icon for each linked account.
   Tooltip on each badge so the brand colour alone doesn't have to
   carry the meaning.

3. Non-invasive "Use another account" link. Plain text link in the
   identity row footer, calls Privy useLogout(). On flip to
   !authenticated the page re-renders into the show_picker phase
   where the user picks again. No modal, no confirm dialog -- the
   only way to lose data here is the connect attempt itself, which
   they can also just Cancel.

Also: force light mode for now. theme.tsx sets initialColorMode
'light' + useSystemColorMode false (was 'system' / true). Toggle
component is already env-gated and stays hidden.

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

1. App-hub verification. scripts/fetch-app-hub.mjs bakes a snapshot
   of vechain/app-hub@master into _lib/app-hub.json keyed by origin
   (122 entries). _lib/app-hub.ts exposes lookupAppByUrl() so the
   runtime check is O(1) string lookup.

   RequesterChip uses it: listed dApps render their canonical
   `name` + a green check + green chip border, "Listed in the
   VeChain App Hub" tooltip. Unlisted dApps render the hostname
   + orange warning, "Not listed in the VeChain App Hub..."
   tooltip.

   Connect-page wiring:
   - Title becomes "Connect to <App Name>" when verified, falls back
     to a plain "Confirm connection" when not.
   - Unverified state renders a left-accent warning Alert and the
     Continue button reads "Continue anyway".
   - The kit ships a `yarn workspace cross-app-connect
     generate-app-hub` script to refresh the snapshot when new
     dApps register.

2. IdentityRow rebalance. The previous layout put the wallet domain
   / address in the primary slot and the email in a secondary
   "Signed in as" line. Flipped: email is the primary, bold
   text -- it's how the user thinks about themselves -- with the
   linked-social badges immediately to the right of the email.
   Wallet (domain + truncated address, mono) becomes the secondary
   caption.

   "Use another account" is no longer inside the row; it now lives
   as a small "Not you? Use another account" link _below_ the
   Continue / Cancel buttons. Doesn't compete with the main CTA.

3. Force light mode actually works now. theme.config already had
   initialColorMode 'light' + useSystemColorMode false, but
   Chakra's ColorModeScript reads localStorage before that takes
   effect -- so a user who toggled dark earlier kept seeing dark.
   New ForceColorMode component mounts inside ChakraProvider and
   re-applies setColorMode('light') once on hydration, overriding
   the cached value. Also fixed `darkMode` prop on
   VechainKitProviderWrapper which was hardcoded to true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the AlertIcon, shrink to xs / 1.3 line-height, and trim "Only
continue if you trust the site in the chip above." -> "...so only
continue if you trust the site." Reads less like a stack-trace next
to the friendlier identity row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three spots where waiting on async data was rendering a generic
Spinner that flashes-then-jumps. Replace with shaped skeletons
that match the eventual content layout:

- AccountChip: SkeletonCircle for the avatar, Skeleton bar for the
  address / domain block, Skeleton bar for the right-side balance
  list. Driven by useVechainDomain.isPending / .isPending +
  balances === null, so the chip morphs into final state inline
  instead of jumping.

- IdentityRow (connect screen): same pattern -- SkeletonCircle on
  the avatar, Skeleton bar replacing the wallet line until the
  domain query resolves. Email + linked-social badges keep
  rendering instantly (they come from the Privy user object).

- Transact ActionRow list: while decoded === null, render
  parsed.clauses.length placeholder rows shaped like the real
  ActionRow (two stacked skeleton bars behind the same left-border
  treatment). Replaces the centered Spinner so the card layout
  doesn't shift when decode resolves.

Skeleton colors track the theme tokens (login-btn-hover-bg /
card-border) so the pulse blends with the card surface in both
light and dark.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull the transact card in line with the cleaner shape we converged on
for the connect card so the two pages feel like one design language:

- Stack spacing: 5 -> 4 to match the connect card's rhythm.
- Warning / error alerts: drop AlertIcon, use the left-accent variant
  with fontSize=xs / lineHeight=1.3 (same pattern that landed on the
  unverified-app warning on the connect page). The Alert no longer
  shouts with a leading icon when the friendly title + risk shield in
  the header already carry the framing.
- Copy tweaks: "We couldn't double-check every step, so only continue
  if you trust this app." / "This app is asking for unlimited access
  to one of your tokens -- make sure you trust it." Tighter, no
  trailing "here." filler.
- Continue button gets h=48px (matched to connect's Continue).
- Inspect details affordance moved to the same "label + link" pattern
  the connect card uses for "Not you? Use another account":
    "Want the technical details?  Inspect ▾"
  The Collapse expands inline below the link so the inspect tree
  doesn't push the Continue button down the card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Log in to VeChain" was ambiguous -- vague about whether the user is
logging in to VeChain.org, the kit, or some service. The actual
action is opening their VeChain wallet, so swap to "Log in to your
wallet": clearer, still short (5 words / 22 chars), and the
subtitle ("Sign in to grant access to [chip]") already names the
requester. The VeChain wordmark stays implicit in the logomark
above it.

VechainHeader's default title also updated for any place that
doesn't pass one explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixup -- the previous commit only landed the explicit title prop in
the loading state; the VechainHeader default still read "Log in to
VeChain", so the show_picker phase (which uses the default) was
still rendering the old copy. Sync the default now.

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

Splits the transact request into a discriminated union (smart_account /
typed_data / message) so the host can decode + show plain message text and
arbitrary EIP-712 payloads in addition to the existing VeChain
ExecuteWithAuthorization flow. Smart-account safety gates only fire for
that kind; other requests just need the user to read what they're signing.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant