Skip to content

Commit a9c9cc6

Browse files
Agilulfo1820claude
andauthored
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

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Cross-App Connect — Check
2+
3+
on:
4+
# zizmor: ignore[dangerous-triggers] - Mitigated with label-based approval
5+
pull_request_target:
6+
types: [labeled, opened, synchronize, reopened]
7+
branches:
8+
- main
9+
paths:
10+
- 'cross-app-connect/**'
11+
- 'packages/vechain-kit/**'
12+
- 'yarn.lock'
13+
- '.github/workflows/cross-app-connect-check.yaml'
14+
15+
concurrency:
16+
# Key by PR number so two PRs that happen to share a branch name
17+
# (common from forks) don't cancel each other's checks.
18+
group: ${{ github.workflow }}-pr-${{ github.event.pull_request.number || github.ref_name }}
19+
cancel-in-progress: true
20+
21+
permissions:
22+
contents: read
23+
24+
jobs:
25+
# External PRs: post a one-time comment explaining the safe-to-build gate.
26+
comment-external-pr:
27+
runs-on: ubuntu-latest
28+
permissions:
29+
pull-requests: write
30+
if: |
31+
github.event.pull_request.head.repo.full_name != github.repository &&
32+
github.event.action == 'opened'
33+
steps:
34+
- name: Comment on external PR
35+
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
36+
with:
37+
issue-number: ${{ github.event.pull_request.number }}
38+
body: |
39+
## 👋 Thanks for your contribution!
40+
41+
Since this PR comes from a fork, the cross-app-connect
42+
check will only run after a maintainer adds the
43+
`safe-to-build` label (and re-adds it after each new
44+
commit, for security).
45+
46+
typecheck-and-build:
47+
name: Type-check & Build
48+
runs-on: ubuntu-latest
49+
if: |
50+
(github.event.label.name == 'safe-to-build') ||
51+
(github.event.pull_request.head.repo.full_name == github.repository) && github.event.pull_request.head.ref != 'main'
52+
steps:
53+
- name: Checkout
54+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
55+
with:
56+
ref: ${{ github.event.pull_request.head.sha }}
57+
58+
- name: Setup Node
59+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
60+
with:
61+
node-version-file: .nvmrc
62+
cache: yarn
63+
64+
- name: Install
65+
run: yarn && yarn install:all
66+
67+
- name: Type-check (cross-app-connect)
68+
run: yarn workspace cross-app-connect typecheck
69+
70+
- name: Build (cross-app-connect)
71+
run: yarn workspace cross-app-connect build
72+
env:
73+
# Build-time public env. Provided as repo Variables; the
74+
# popup needs them to construct the Privy client. The build
75+
# tolerates missing values but won't run end-to-end without
76+
# them — sufficient to verify the bundle compiles cleanly.
77+
NEXT_PUBLIC_PRIVY_APP_ID: ${{ vars.NEXT_PUBLIC_PRIVY_APP_ID }}
78+
NEXT_PUBLIC_PRIVY_CLIENT_ID: ${{ vars.NEXT_PUBLIC_PRIVY_CLIENT_ID }}
79+
NEXT_PUBLIC_PRIVY_DOMAIN: ${{ vars.NEXT_PUBLIC_PRIVY_DOMAIN }}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Cross-App Connect — Deploy to Pages
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'cross-app-connect/**'
9+
- 'packages/vechain-kit/**'
10+
- 'yarn.lock'
11+
- '.github/workflows/cross-app-connect-deploy.yaml'
12+
workflow_dispatch:
13+
14+
concurrency:
15+
# Pages permits one in-flight deploy. Queue rather than cancel so a hot
16+
# fix doesn't get wiped by a follow-up merge mid-deploy.
17+
group: pages
18+
cancel-in-progress: false
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
build:
25+
name: Build static site
26+
runs-on: ubuntu-latest
27+
permissions:
28+
contents: read
29+
steps:
30+
- name: Checkout
31+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
32+
33+
- name: Setup Node
34+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
35+
with:
36+
node-version-file: .nvmrc
37+
cache: yarn
38+
39+
- name: Install
40+
run: yarn && yarn install:all
41+
42+
- name: Build (cross-app-connect)
43+
run: yarn workspace cross-app-connect build
44+
env:
45+
NEXT_PUBLIC_PRIVY_APP_ID: ${{ vars.NEXT_PUBLIC_PRIVY_APP_ID }}
46+
NEXT_PUBLIC_PRIVY_CLIENT_ID: ${{ vars.NEXT_PUBLIC_PRIVY_CLIENT_ID }}
47+
NEXT_PUBLIC_PRIVY_DOMAIN: ${{ vars.NEXT_PUBLIC_PRIVY_DOMAIN }}
48+
# Empty when a custom domain is set up in repo Settings →
49+
# Pages. Override to `/vechain-kit` (or whatever the repo
50+
# name is) if you want to test at the default
51+
# `<org>.github.io/<repo>/` URL before the CNAME points here.
52+
NEXT_PUBLIC_BASE_PATH: ${{ vars.NEXT_PUBLIC_BASE_PATH }}
53+
54+
- name: Disable Jekyll on Pages
55+
# GitHub Pages otherwise strips paths starting with `_` (e.g.
56+
# `_next/`), which kills every JS/CSS chunk Next.js emits.
57+
run: touch cross-app-connect/dist/.nojekyll
58+
59+
- name: Configure Pages
60+
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
61+
62+
- name: Upload artifact
63+
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
64+
with:
65+
path: cross-app-connect/dist
66+
67+
deploy:
68+
name: Deploy to GitHub Pages
69+
needs: build
70+
runs-on: ubuntu-latest
71+
# Pages-specific permissions scoped to this job only — zizmor flags
72+
# them as too broad at the workflow level since the build job
73+
# doesn't need to write anything.
74+
permissions:
75+
pages: write
76+
id-token: write
77+
environment:
78+
name: github-pages
79+
url: ${{ steps.deployment.outputs.page_url }}
80+
steps:
81+
- name: Deploy
82+
id: deployment
83+
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Cross-App Connect — Refresh App Hub cache
2+
3+
on:
4+
schedule:
5+
# Daily at 04:13 UTC — odd minute to avoid the top-of-hour rush on
6+
# GitHub-hosted runners, and well before EU/US working hours so a
7+
# follow-up PR is waiting for review when the team comes online.
8+
- cron: '13 4 * * *'
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
14+
concurrency:
15+
# Only one refresh attempt at a time; if a previous PR is still pending
16+
# merge, queueing another would create a duplicate branch tip.
17+
group: ${{ github.workflow }}
18+
cancel-in-progress: false
19+
20+
jobs:
21+
refresh:
22+
name: Regenerate app-hub.json
23+
runs-on: ubuntu-latest
24+
# PR-opening permissions are scoped to this job (zizmor flags them
25+
# as too broad at the workflow level).
26+
permissions:
27+
contents: write
28+
pull-requests: write
29+
steps:
30+
- name: Checkout
31+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
32+
33+
- name: Setup Node
34+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
35+
with:
36+
node-version-file: .nvmrc
37+
cache: yarn
38+
39+
- name: Install
40+
run: yarn && yarn install:all
41+
42+
- name: Regenerate cache
43+
run: yarn workspace cross-app-connect generate-app-hub
44+
env:
45+
# Raises the GitHub API rate limit from 60/hr (anonymous)
46+
# to 5000/hr so the fetch can cover the full registry
47+
# without throttling.
48+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49+
50+
- name: Open PR if anything changed
51+
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
52+
with:
53+
commit-message: |
54+
chore(cross-app-connect): refresh app-hub.json from vechain/app-hub
55+
56+
Auto-generated by the cross-app-connect-refresh-app-hub workflow.
57+
title: 'chore(cross-app-connect): refresh app-hub cache'
58+
body: |
59+
Scheduled refresh of `cross-app-connect/src/app/cross-app/_lib/app-hub.json` from [`vechain/app-hub`](https://github.com/vechain/app-hub).
60+
61+
The cache keys the registry by origin so the transact popup can show
62+
`Confirm token swap on Nubila` instead of a raw URL. This PR appears
63+
whenever a new app is added, an existing one is updated, or one is
64+
removed from the registry.
65+
66+
Review the diff to make sure the changes look like additions you
67+
expected (no surprise deletions, plausible names). Merge when happy.
68+
69+
🤖 Opened by `.github/workflows/cross-app-connect-refresh-app-hub.yaml`.
70+
branch: chore/refresh-app-hub-cache
71+
base: main
72+
delete-branch: true
73+
labels: |
74+
chore
75+
automated
76+
add-paths: |
77+
cross-app-connect/src/app/cross-app/_lib/app-hub.json

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ VeChain Kit is a comprehensive library designed to make building VeChain applica
1212

1313
It offers:
1414

15-
- <b>Seamless Wallet Integration:</b> Support for VeWorld, Sync2, WalletConnect, VeChain Embedded Wallet, and social logins (Google, Apple, GitHub, email, passkey — powered by Privy).
15+
- <b>Seamless Wallet Integration:</b> Support for VeWorld, Sync2, WalletConnect, VeChain Embedded Wallet, and social logins (Google, Apple, GitHub, X/Twitter, Discord, TikTok, LINE, email, passkey — powered by Privy).
1616
- <b>Custom Connection UI:</b> Vechain-kit's own connect modal handles the VeWorld and Sync2 flows directly, with a built-in “Waiting for signature…” view and a fully themeable layout. WalletConnect's QR modal is preserved.
17+
- <b>Social Logins Without Your Own Privy Account:</b> Drop a "Continue with Google / Apple / X / Discord / GitHub / TikTok / LINE" button in your app and it just works — the kit routes through VeChain's whitelabel cross-app host (`cross-app-connect/`) so users get one VeChain identity that follows them across every kit-using dApp. No Privy bill, no dashboard setup.
1718
- <b>Unified Ecosystem Accounts:</b> Leverage Privy’s Ecosystem feature to give users a single wallet across multiple dApps, providing a consistent identity within the VeChain network.
1819
- <b>Developer-Friendly Hooks:</b> Easy-to-use React Hooks that let you read and write data on the VeChainThor blockchain.
1920
- <b>Pre-Built UI Components:</b> Ready-to-use components (e.g., TransactionModal) to simplify wallet operations and enhance your users’ experience.
@@ -31,6 +32,14 @@ It offers:
3132
- [Smart Account Factory](https://vechain.github.io/smart-accounts/)
3233
- [Docs](https://docs.vechainkit.vechain.org/)
3334

35+
## Cross-app Whitelabel Host
36+
37+
When a user picks "Continue with Google / Apple / VeChain / …" from a kit-using dApp, the kit opens a small popup that handles the OAuth/SMS handshake and posts the resulting signature back. That popup runs on VeChain's whitelabel host, which lives in this repo at [`cross-app-connect/`](./cross-app-connect/) — a Next.js static export with VeChain branding, a calldata-aware transaction summary, recovery from stale connection records, and 17 languages.
38+
39+
The whitelabel popup is why your app can offer social login without owning a Privy account: users see VeChain chrome and get one identity across every kit-integrated dApp.
40+
41+
See [`cross-app-connect/README.md`](./cross-app-connect/README.md) for the popup's architecture, the rationale behind dropping Chakra / TanStack Query / vechain-kit from that surface, deploy instructions (GitHub Pages workflow included), and how to add translation keys.
42+
3443
## Table of Contents
3544

3645
- [Setting up for local development](#setting-up-for-local-development)

cross-app-connect/.env.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Privy app this host represents
2+
# Default = VeChain's mainnet Privy app (mirrors VECHAIN_PRIVY_APP_ID in vechain-kit)
3+
NEXT_PUBLIC_PRIVY_APP_ID=cm4wxxujb022fyujl7g0thb21
4+
NEXT_PUBLIC_PRIVY_CLIENT_ID=
5+
# REQUIRED. Whitelabel Privy auth subdomain provisioned for your app in the
6+
# Privy dashboard. Must include scheme, e.g. https://privy.your-app.privy.dev
7+
# This is NOT the public auth.privy.io host.
8+
NEXT_PUBLIC_PRIVY_DOMAIN=
9+
10+
# VeChain network -- main | test | solo
11+
NEXT_PUBLIC_NETWORK_TYPE=main
12+
NEXT_PUBLIC_DELEGATOR_URL=
13+
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=

cross-app-connect/.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
extends: ['next'],
3+
rules: {
4+
'@typescript-eslint/unbound-method': 'off',
5+
},
6+
};

cross-app-connect/.gitignore

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
.yarn/install-state.gz
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
/dist/
16+
17+
# production
18+
/build
19+
20+
# misc
21+
.DS_Store
22+
*.pem
23+
24+
# debug
25+
npm-debug.log*
26+
yarn-debug.log*
27+
yarn-error.log*
28+
29+
# local env files
30+
.env
31+
.env*.local
32+
33+
# vercel
34+
.vercel
35+
36+
# typescript
37+
*.tsbuildinfo
38+
next-env.d.ts

0 commit comments

Comments
 (0)