Commit a9c9cc6
feat: VeChain whitelabel cross-app connect host (#620)
* feat(cross-app-connect): add whitelabel Privy cross-app provider host
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>
* chore(deps): bump @privy-io/cross-app-connect from 0.2.2 to 0.5.8
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>
* feat(cross-app-connect): custom sign-in UI without Privy modal
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>
* feat(cross-app): pass login intent through to whitelabel connect page
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>
* feat(login): allow google/apple/email login methods without privy prop
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>
* fix(login): show google/apple/email buttons when no privy prop
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>
* fix(cross-app-connect): break OAuth redirect loop on intent flow
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>
* feat(cross-app-connect): auto-accept connection after sign-in
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>
* refactor(login): route useLoginWithOAuth fallback inside the hook
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>
* fix(cross-app-connect): only auto-accept when the user just authenticated
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>
* feat(login): all 10 privy oauth providers fall back to cross-app
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>
* feat(cross-app-connect): support all 10 privy oauth providers in sign-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>
* chore(examples): list all 10 oauth providers + update notes
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>
* chore(login): trim oauth list to providers actually enabled in vechain 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>
* docs(login): reflect cross-app fallback in login-modal page
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>
* fix(cross-app-connect): honor intent when an old session already exists
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>
* fix(cross-app-connect): phase machine + honor matching session
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>
* feat(cross-app-connect): vechain brand theme + system color mode
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>
* feat(cross-app-connect): privy-style sign-in panel + recent badge
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>
* feat(cross-app-connect): apply vechain branding to transact page
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>
* fix(cross-app-connect): suppress body hydration warning + add debug toggle
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>
* fix(cross-app-connect): make color-mode toggle visible everywhere
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>
* chore(cross-app-connect): move color-mode toggle to top-left
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>
* feat(cross-app-connect): rework sign-in list per vechain config
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>
* fix(login): email requires privy again; add phone/farcaster intents
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>
* fix(cross-app-connect): logo flicker + cancel button invisibility
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>
* refactor(cross-app-connect): mirror vechain-kit's design tokens
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>
* refactor(cross-app-connect): polish sign-in UX (chip, link, dot, labels)
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>
* chore(cross-app-connect): chromatic glyphs for discord/tiktok/line/phone
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>
* feat(cross-app-connect): translate transactions into plain language
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>
* feat: simulate on homepage
* feat(cross-app-connect): resolve unknown ERC-20 metadata from thor
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>
* feat: yarn
* feat(cross-app-connect): balance change preview on the transact screen
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>
* feat(cross-app-connect): label known contracts, hide dev noise
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>
* fix: remove more
* fix: build
* feat(cross-app-connect): account chip, shield title, friendly labels
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>
* feat(cross-app-connect): resolve .vet domain + avatar via kit hooks
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>
* revert(cross-app-connect): drop balance change simulation
Per user request, remove the "Your wallet will" before/after balance
preview that was added in 4471de12. 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>
* fix(provider): fall back to VECHAIN_PRIVY_APP_ID when no privy prop
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>
* feat(cross-app-connect): risk-adaptive shield, specific titles, more
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>
* chore(cross-app-connect): hide color-mode toggle by default
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>
* chore(cross-app-connect): swap wordmark for logomark in the header
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>
* style: content
* feat(cross-app-connect): identity row + force light mode
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>
* feat(cross-app-connect): app-hub verification + identity polish
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>
* chore(cross-app-connect): tighten unverified-app warning copy
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>
* feat(cross-app-connect): swap in-place spinners for skeletons
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>
* style(cross-app-connect): align transact card with the connect pattern
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>
* chore(cross-app-connect): retitle picker to "Log in to your wallet"
"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>
* chore(cross-app-connect): apply default title to VechainHeader
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>
* feat(cross-app-connect): support personal_sign and generic eth_signTypedData_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>
* feat: get rid of chakra, vechain kit and tanstack
* feat: use same account component
* feat: nextjs 16
* feat(cross-app-connect): polish identity + requester chip
- Fold balance display into IdentityRow as a separator-divided second line;
transaction-relevant tokens first, VET last, 2-4 decimals.
- Drop BalanceLine in favour of the consolidated IdentityRow.
- Lowercase address truncation via a shared truncateAddress helper so
0x2e25…2D1B no longer has mismatched checksum casing.
- Rebuild RequesterChip around a single signal: yellow ⚠ for local /
HTTP origins, green ✓ for App-Hub verified apps, neutral 🔒 for plain
HTTPS. Favicon + dual-icon clutter removed; chip tightened.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cross-app-connect): recognize ecosystem actions + polish transact UX
Known-action recognizer (`_lib/knownActions.ts`):
- Registry mapping (contract address, selector) → human-readable summaries.
- VeChain domain ops (setName / setAddr / setText / claim) so the kit's own
flows stop tripping the "couldn't double-check this" warning.
- VeBetterDAO governance (castVote, allocation vote, endorseApp), rewards
(voter + allocation pool claims), B3TR↔VOT3 conversion.
- NFT transfers (safeTransferFrom 721/1155) + setApprovalForAll.
- DEX swaps recognized by router address (BetterSwap, VeTrade Uniswap-
compatible, VeTrade custom). Custom router has non-standard selectors so
we fall back to address-only recognition.
- Decoder calls the recognizer between the ERC-20 fast path and the b32
fallback; recognised calls produce a new `known_action` DecodedClause
kind that bypasses the unverified-action warning.
Title + subtitle overhaul:
- All titles use a verb-led "Confirm X" pattern (Confirm token transfer,
Confirm token swap, Confirm domain update, Confirm VeBetterDAO vote,
Confirm rewards claim, etc.). Approve→swap batches coalesce into a single
"Confirm token swap" rather than "Confirm 2 actions".
- Subtitle carries the per-transaction specifics that the title omits:
"1 B3TR to 0x3f90…dcee", "Unlimited B3TR for 0xabcd…1234",
"1 B3TR via VeTrade", "Switching to me.veworld.vet",
"FOR proposal", "From cycle 4", "1 B3TR → VOT3".
- Short readable values inline in the setText summary; opaque values
(IPFS hashes, data URLs, long hex) reduced to "Update avatar" etc.
Action list visual polish:
- "ACTIONS TO APPROVE" section header above the per-clause list.
- Numbered step chips on multi-clause batches.
- "To" / "Spender" / "Operator" inline labels styled as uppercase tags so
they read as keys, not body text.
- 24px gap between major sections of the card.
CTA copy:
- "Continue" / "Continue anyway" / "I understand, continue" →
"Confirm" / "Confirm anyway" / "I understand, confirm".
- Loading state: "Working…" → "Signing…" (transact) / "Connecting…" (connect).
Other:
- Drop the balance display from the account card; per-clause rows show
the amounts that matter.
- Wait for smart-account resolution before revealing the full UI so the
IdentityRow doesn't pop in mid-render.
- Avatars now `object-fit: cover` instead of stretching.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cross-app-connect): render typed data as labelled fields, not JSON
EIP-712 messages now render using their own `types` schema rather than a
raw JSON.stringify dump:
Mail
From: My Dapp v1
NAME
Alice
WALLET
[avatar] 0x0000…0000
Per-type rendering:
- `address` → AddressTag (avatar + domain + truncated hex)
- `bool` → "Yes" / "No"
- `string` → text
- bytes / numbers → monospace
- nested struct types → recurse with a left-bar indent
- arrays → bullet list
If a message references a primaryType missing from the schema we fall back
to the original JSON dump so nothing renders blank. The full raw JSON is
still available in the Inspect panel below.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cross-app-connect): wire up i18next foundation
- Add `i18next` + `react-i18next` deps.
- `src/app/i18n/config.ts` initialises the i18next instance and runs the
language detection chain at module load:
URL `?lng=` → localStorage `vk-cross-app-connect:lng`
→ navigator.language → 'en'
Resolved language is persisted so the transact popup (which Privy's SDK
doesn't let us URL-tag) reuses what the connect popup learned.
- Chinese variant normalisation: `zh-TW` / `zh-HK` / `zh-MO` / anything
marked Hant → `tw`; everything else → `zh`.
- `I18nProvider.tsx` wraps the tree in `<I18nextProvider>` so every
component can call `useTranslation()`.
- 17 empty locale stubs created at `src/app/i18n/locales/*.json` (one per
language the kit ships). Strings will be extracted next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cross-app-connect): extract ~235 strings to en.json + thread t() everywhere
All visible UI copy now goes through i18next. English source-of-truth at
`src/app/i18n/locales/en.json` with a tree of keys grouped by feature
area: common.*, vote.*, header.*, identity.*, addressTag.*, requester.*,
domains.label.*, action.{transfer|approve|unknown|domain|governance|
token|rewards|swap|nft}.*, transact.*, connect.*, landing.*. Placeholder
syntax is i18next's `{{name}}`; `count` is reserved by the library and
typed as a number.
Files touched (literals → t() calls):
- `src/app/cross-app/_lib/decoder.ts` — transfer / approve / unknown.
- `src/app/cross-app/_lib/knownActions.ts` — every recognised action,
the `friendlyTextRecordKey` lookup, vote labels. Now also returns a
structured `data` field per clause (KnownActionData) so labels.ts can
build batch subtitles from facts instead of parsing localised text.
- `src/app/cross-app/_lib/labels.ts` — titles, subtitles, CTA labels,
humanPrimaryType. Batch subtitles read `data.setPrimaryName`,
`data.voteSupport`, `data.allocationAppCount`, `data.rewardCycle`,
`data.rewardRound`, `data.convertAmount/From/To`, `data.dex` instead
of regex-matching English summaries.
- `src/app/cross-app/transact/TransactClient.tsx` — page UI, action
rows, inspect panel, typed-data renderer, network labels. The
`renderTypedValue` helper became a `<TypedValue>` component so it can
use `useTranslation()` for the Yes/No / (empty) strings.
- `src/app/cross-app/connect/ConnectClient.tsx` — sign-in panel, phone
form, OAuth row labels, recent-used badge, confirm screen.
- `src/app/components/{VechainHeader,IdentityRow,AddressTag,RequesterChip}.tsx`
— placeholder text, tooltips, security signals.
- `src/app/page.tsx` — landing copy. Promoted to client component
(same one-off pattern as VechainHeader) so it can call useTranslation.
The Privy popup is now multilingual-ready end-to-end, but ships
English-only until Phase 4 fills out the 16 non-English locale files
(`it.json`, `de.json`, etc.) — those still load as `{}` so i18next falls
back to en for every key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cross-app): pass kit language to the cross-app popup URL
When the kit triggers a cross-app login (`useCrossAppClient().login()`),
read `i18n.language` and append it as `&lng=` to the connect URL so the
whitelabel host renders the popup in the same locale as the kit-using
app. The host stashes the value in localStorage on its own origin, so the
transact popup -- whose URL is built by `@privy-io/cross-app-connect`
and can't take extra query params -- picks up the same language.
Generalised the existing `appendIntent` helper into `appendCrossAppParams`
that takes both `intent` and `lng`. The override path now runs whenever
either is set (intent stays optional; lng is always present after kit
boot since the kit's i18n defaults to 'en'). For users in non-English
locales this is the only way to avoid the popup defaulting to
navigator.language and diverging from the kit's current setting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cross-app-connect): translate UI to Italian and French
Two of the kit's 17 supported languages. Both translations mirror the
key tree in en.json verbatim — same placeholders, same nested shape so
i18next interpolation Just Works. Technical terms stay as proper nouns
(B3TR, VOT3, VeChain, VeBetterDAO, BetterSwap, VeTrade, IPFS).
FOR/AGAINST/ABSTAIN → A FAVORE/CONTRO/ASTENUTO (it),
POUR/CONTRE/ABSTENTION (fr).
The remaining 15 locales (de, es, pt, nl, ja, zh, tw, ko, ru, ro, sv,
vi, tr, hi) still load as `{}` and fall back to en.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cross-app-connect): translate UI to the remaining 14 locales
Closes the i18n loop: every supported language now has a real
translation, not an empty stub falling back to English.
Languages landed:
- Latin/Germanic: es, pt, de, nl
- East Asian: ja, zh (Simplified), tw (Traditional), ko
- Slavic/Romance/Nordic: ru, ro, sv
- Other: tr, vi, hi (Devanagari)
Each file mirrors the 171-key tree of en.json verbatim — same nested
shape, same `{{placeholder}}` tokens, no extras, no missing keys
(verified by JSON parse + key-set diff). i18next can switch between any
of these without missing-key warnings.
Conventions consistent across all 17 locales:
- Proper nouns stay in source form: VeChain, VeBetterDAO, VOT3, B3TR,
BetterSwap, VeTrade, IPFS, Farcaster, Privy, Warpcast, SIWF, App Hub,
Twitter, GitHub, Telegram, NFT, VET, Thor Solo, calldata, hex,
selector.
- Vote labels use natural local form (often ALL CAPS where culturally
appropriate; CJK uses normal forms): A FAVOR / EN CONTRA / ABSTENCIÓN
(es), 賛成 / 反対 / 棄権 (ja), ЗА / ПРОТИВ / ВОЗДЕРЖАЛСЯ (ru), etc.
- Phone placeholder uses a regionally appropriate example number with
a matching codeHint country code (+34, +49, +81, +86, +91, +84, etc.).
- Imperative for buttons, neutral-declarative for everything else.
- "wallet" / "token" / "swap" translated naturally where there's an
established local term; left in source where not.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* i18n(cross-app-connect): translate block + error + Privy UI strings
A second sweep through the source caught literals that slipped past
the initial extraction:
Transact:
- Block messages (chain-id mismatch, smart-account mismatch, invalid
chain-id, unsupported method) — these surface in the red alert above
the action list whenever the smart-account safety gates trip.
- Error fallbacks: "Failed to read request", "Failed to sign request".
- Privy uiOptions strings passed to the signing UI: title, buttonText
for both `signMessage` and `signTypedData` (the smart-account vs.
generic typed-data variants).
Connect:
- Error fallbacks: "Failed to accept connection", "Failed to send code",
"Failed to verify code", and the "Missing access token" thrown when
Privy hasn't returned one yet.
14 new keys added under `transact.{block,error,privyUi}.*` and
`connect.error.*`. All 17 locales now sit at 185 keys with perfect
parity vs. en.json (verified by JSON parse + key-set diff). The
TransactClient block effect's dep array picks up `t` so language
switching at runtime updates the message text correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add yarn lock
* fix(cross-app-connect): silence script warning + i18n hydration mismatch
Two issues showing up in dev console:
1. **Inline <script> in JSX** triggered React 19's "Encountered a script
tag while rendering React component" warning. Swapped to Next's
`<Script id="vk-color-mode" strategy="beforeInteractive">` so the
color-mode detector runs before React hydrates without React thinking
it owns the tag. Logic unchanged: still reads `prefers-color-scheme`
and sets `data-color-mode` on <html> before first paint.
2. **i18n hydration mismatch**: server rendered the title in English
("Reviewing transaction") because `navigator` doesn't exist on the
server, while the client immediately rendered Italian ("Revisione
transazione") because resolveLanguage() found the URL/browser locale.
React flagged the mismatch and regenerated the tree, producing the
en → it flash the user reported.
Fix: initialise i18n with `lng: 'en'` always (so SSR and the client's
first React render produce identical strings — hydration is clean),
then apply the detected language in a `useLayoutEffect` inside
`<I18nProvider>`. Layout effect runs synchronously after the React
commit and before the browser paints, so the language switch happens
between the no-op initial render and the first frame the user
actually sees. No console warning, no visible flash.
Falls back to `useEffect` on the server, which never runs there
anyway (the isomorphic layout-effect dance).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cross-app-connect): kill the en → it text flash by gating SSR
The browser paints the server-rendered HTML *before* JS loads, so any
useLayoutEffect-based language swap arrives too late — the user already
saw the English text. Switched I18nProvider to render `null` during SSR
and the first client render (server and client agree → no hydration
warning), then mount the real tree only after detecting the language.
The mounted check sits at the provider level so it covers every
translated component in one shot. The brief blank moment between paint
and mount is hidden behind each page's own spinner shell.
Trade-off: translated routes no longer get SSR rendering of body
content. For a one-shot popup this is acceptable — JS loads fast and
the popup has no SEO surface to lose.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* revert(i18n): always use device language, drop URL/localStorage/kit sync
Per product decision: the cross-app popup should reflect the user's OS /
browser locale every time it opens, with English as fallback. No URL
override, no localStorage stash, no sync with the kit-using app's
currently selected language.
Reverts the three mechanisms put in place earlier:
1. **`?lng=` URL priority** removed from `resolveLanguage()` (host side).
2. **`vk-cross-app-connect:lng` localStorage stash** removed — the
resolver is now a pure read of `navigator.language` → fallback 'en'.
3. **Kit-side `appendCrossAppParams` extension** reverted to the
original `appendIntent` (intent-only). Dropped the `i18n` import.
The Chinese-variant normalisation (`zh-TW` / `zh-HK` / `zh-MO` / `*Hant`
→ `tw`) stays — that's pure browser-tag mapping, not user preference.
Trade-off acknowledged: a user whose OS is English but who has set the
kit-using app to Italian will see the popup in English. That's the
explicitly chosen behaviour now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: tsconfig
* feat(cross-app-connect): own the session-expired login screen + remember user
Three issues with the "session expired" branch on the transact popup:
1. Tapping "Continue" opened Privy's modal with their own picker — we
wanted to drive sign-in entirely through the kit's matching UI.
2. The screen was a single anonymous "Continue" button — no indication
of who the user was previously logged in as.
3. Nothing pre-highlighted the provider the user actually used last, so
every re-login was a cold pick.
Fixes:
- **Extracted `SignInPanel`** (plus `ProviderRow` / `PhoneRow` /
`FarcasterRow` / `RecentDot`) out of `connect/ConnectClient.tsx` into
`cross-app/_components/SignInPanel.tsx`. ConnectClient now imports
it; TransactClient renders it in the `!authenticated` branch with
`onCancel={onReject}` so closing the picker rejects the cross-app
request cleanly.
- **No more Privy modal**: dropped `useLogin()` from TransactClient.
The headless `useLoginWithOAuth` / `useLoginWithSms` inside SignInPanel
drive everything through our own UI.
- **Last-identity stash** at `cross-app/_lib/lastIdentity.ts`. Whenever
`usePrivy().user` is non-null (on both connect and transact), we
persist `{ label, provider }` to localStorage. `labelFromPrivyUser`
derives a display string from `user.email.address`, then phone, then
linked-social email, then the DID prefix.
- **"Welcome back, {identity}"** title on the transact session-expired
screen when a stashed identity is available. The previously-used
provider is forwarded to `SignInPanel` via the new `presetRecent`
prop so its dot lights up the right row.
- **`transact.title.welcomeBack`** key added in all 17 locales.
Note re Privy session duration: it's a dashboard setting on the Privy
app (Authentication → Session), not a code option. Default is ~30 days
sliding. To raise it, edit the dashboard for the VECHAIN_PRIVY_APP_ID
app — no code change possible from the kit side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cross-app-connect): auto-create connection record when transact lands cold
When a user authenticates fresh inside the transact popup (Privy session
expired or first-time visitor on this requester), Privy's backend has no
recorded "connection" between their userId and the requester's app —
`getVerifiedTransactionRequest()` then fails with "No connection found
for requester". Privy's modal used to paper over this by mixing login +
connection acceptance in one flow; once we drove sign-in through our own
SignInPanel, that step went missing.
The SDK exposes `acceptConnection({ accessToken, address, userId,
connectionRequest })` and a `CrossAppConnectionRequest` only needs
`{ requesterPublicKey, callbackUrl }`. The transact URL already carries
both, under `requester_public_key` and `requester_origin`. So when the
"No connection" error fires:
1. Reconstruct the connection request from those URL params via the new
`connectionRequestFromTransactUrl()` helper.
2. Pull the embedded wallet address (which we already have for the smart
account lookup) and the Privy access token.
3. Call `client.acceptConnection(...)` — creates the record + sends a
CONNECT_RESPONSE postMessage to the opener (the kit-using app, which
ignores it since nothing's awaiting it).
4. Retry `getVerifiedTransactionRequest()`. Now the connection exists,
so the request decrypts and we proceed as normal.
Any failure in the retry path surfaces as the original parse error so
the user still gets a meaningful message instead of an infinite spinner.
`embedded?.address` and `getAccessToken` added to the effect's dep array
since both are now part of the recovery path. The "wait for embedded"
gate (which adds maybe 100 ms for first-render) is the price for not
racing on the connection-creation case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(cross-app-connect): force-flag to exercise the no-connection retry path
The auto-`acceptConnection` recovery branch is impo…1 parent af7f4dd commit a9c9cc6
108 files changed
Lines changed: 14125 additions & 920 deletions
File tree
- .github/workflows
- cross-app-connect
- public/brand
- scripts
- src/app
- components
- cross-app
- _components
- _lib
- connect
- transact
- i18n
- locales
- providers
- docs
- examples
- homepage/src/app
- components/features/LoginUIControl
- providers
- playground
- src/app
- components/features/LoginUIControl
- providers
- packages/vechain-kit
- src
- components
- AccountModal/Contents
- FailedOperation
- SendNft
- SendToken
- SuccessfulOperation
- ConnectModal
- Components
- Contents
- TransactionModal
- TransactionToast
- UpgradeSmartAccountModal/Contents
- common
- hooks
- login
- modals
- languages
- providers
- theme
- utils/swap
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
34 | 43 | | |
35 | 44 | | |
36 | 45 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
0 commit comments