feat(frontend): share-link button on /trader/:address (mobile Web Share + copy) #665
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#665
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Add a Share control on the public trader profile (
/trader/:address) so a user can send the canonical profile URL from a phone without opening the browser chrome, selecting a longterra1…path, or copying the wallet address by mistake.The report named
/trader/{pair}. There is no/trader/:pairroute. The shareable public lookup is/trader/:address(wallet bech32). Pair deep links are/trade/:pairAddrand/charts/:pairAddr. This issue ships trader-profile share as the required surface. A reusable helper may also be mounted on those pair pages in the same change if it stays one control — do not invent a second share widget./portfoliois wallet-home and is not a public URL. A Share action there, if added, must emit/trader/{connectedAddr}, never/portfolio.Current codebase
Routes and chrome
/trader/trader/:addressgetTrader+TraderSummaryStats+ positions + trade history. No share-link control./portfolio/trader/{wallet}. No share./trade/:pairAddr/charts/:pairAddrWired in
App.tsx:TraderRouteShellfor/traderand/trader/:addresswithresetKeyson the address segment (#126). Nav label Trader is under More (navItems.ts).TraderPage.tsxheader is title + one-line lead + search / My Profile / My Portfolio. After a successful profile load,TraderSummaryStats.tsxshowsAddressRow(copyAriaLabel="Copy trader address") plus explorer. That copies the bech32, nothttps://…/trader/{addr}.There is no
navigator.shareusage anywhere infrontend-dapp/src. Clipboard is centralized:copyToClipboard+CopyButton(#183)copyButtonCopy.tsAddressRowmust not be overloaded to copy a URL (explorer + address copy stay as they are).Why mobile is the gap
On a phone, the address bar is often hidden (in-app browsers, PWA, Keplr webview — #632). Selecting a 44+ character
terra1…path is error-prone. Recipients who receive a raw address cannot open the profile without knowing to paste it into/trader. The OS share sheet (navigator.share) is the expected path; clipboard is the fallback when Web Share is missing or the user cancels.Open Graph (do not “fix” by adding per-route meta)
Crawlers do not run React. Every SPA path, including
/trader/:address, returns the same staticindex.htmltags (#578, OG-5 / OG-6). Share must not addreact-helmet, prerender, orog:urlbuilt fromwindow.location/ wallet / pair. Recipients see the product card. That is accepted. This issue is an in-app control, not a crawler change.Tests today
TraderPage.test.tsxcovers outage vs 404 only.e2e/trader-page.spec.tsasserts heading + positions. No share assertions.Why this is needed
Traders and community members want to send a profile link (leaderboard row, Discord, Telegram, SMS). Today the only one-tap action on the profile header copies the wallet, not the dApp URL. On mobile that is worse: no visible URL, no share sheet, easy to send the wrong string.
A small control next to the profile header (or next to
AddressRow) that shares or copies the canonical same-origin/trader/{validated-address}URL closes that gap without a new indexer route or per-wallet OG.Constraints / guardrails
/trader/:addresswhenisValidTerraAddress(param)is true. Hide Share on/traderwith no address, and when the path segment failsisValidTerraAddress. Do not create/trader/:pair.origin + '/trader/' + address(orURLAPI). Strip?/#/ wallet-connect / theme junk. Do not sharewindow.location.hrefif it still has search/hash. Address in the path must passisValidTerraAddressafter trim; prefer the route param, not a rawlocation.pathnamesplice.navigator.share({ url, title, text })(user gesture only).AbortError/ user dismiss is not a failure toast. Ifshareis missing, throws a non-abort error, orcanShareis false, fall back tocopyToClipboard(do not add a secondnavigator.clipboard.writeTextpath).aria-labele.g. Share trader profile link). Do not replace address copy.copyButtonCopy.ts. No “how to share”, no Telegram/Discord lecture, noVITE_*/ host:port in errors.TraderSummaryStatsor the page H1 row). No extrashell-panel*/card-glasswrap. No metric-tile nest.python3 scripts/check_chrome_nesting.pystays green.<button type="button">, explicitaria-label,:focus-visiblering (#144). Clipboard fallback uses the existingsr-only+aria-live="polite"pattern fromCopyButton. 44px-class hit target on phone (375).sounds.playButtonPresson activate.og:*from request host / wallet / pair. Sharetext/titleare static product copy (e.g. CL8Y DEX trader) plus optional shortened address viashortenAddress— not P&L, volume, or indexer fields (stale + spoof).window.location.originso LocalTerra / QA / prod share the deployment the user is on. Do not hard-codehttps://dex.cl8y.comin the helper (that origin is for baked OG only —viteOg.ts). Do not readVITE_PUBLIC_ORIGINin the React bundle for this./trade/:pairAddrand/charts/:pairAddr(canonical/trade/{pair}//charts/{pair}after existing pair-param validation) and on/portfolioas Share public profile →/trader/{wallet}when connected. Do not share/portfolio. Do not add Share to Swap/Pool/Limits in this issue.shareis absent, clipboard fallback must still work; if clipboard is denied, useCOPY_BUTTON_FAILURE_MESSAGE(permission-safe, noDOMExceptiontext).Relevant files
frontend-dapp/src/pages/TraderPage.tsx/traderhas nonefrontend-dapp/src/components/trader/TraderSummaryStats.tsxfrontend-dapp/src/components/ui/AddressRow.tsxfrontend-dapp/src/components/ui/CopyButton.tsxfrontend-dapp/src/utils/copyToClipboard.tsfrontend-dapp/src/utils/copyButtonCopy.tsfrontend-dapp/src/utils/constants.tsisValidTerraAddressfrontend-dapp/src/App.tsxfrontend-dapp/src/pages/TraderPage.test.tsxfrontend-dapp/e2e/trader-page.spec.tsdocs/frontend.md§ Trader profileskills/AGENTS_FRONTEND_COPY_BUTTON.mdfrontend-dapp/src/pages/PortfolioPage.tsx/trader/{addr}frontend-dapp/src/pages/TradePage.tsx/ChartsPage.tsxLikely new:
frontend-dapp/src/utils/sharePageLink.ts(canonical URL +sharevs copy) andfrontend-dapp/src/components/ui/ShareLinkButton.tsx(+ Vitest). Do not fork a second clipboard helper.Recommended direction
{ origin, path }(orkind: 'trader' \| 'trade' \| 'charts'+ validated id), return an absolute URL with no search/hash. Reject invalid terra addresses (returnnull).ShareLinkButton: on click, ifnavigator.shareexists,await navigator.share({ title, text, url }); on abort, stop; elsecopyToClipboard(url)and announce via the CopyButton live-region pattern. Injectshare/ clipboard in tests.TraderSummaryStatsheader (flex wrap next to AddressRow) or on the Trader page H1 row so it remains visible while the profile is loading/404. Prefer the header row that is on screen at 375px without horizontal scroll.data-testid="trader-share-link"(andshare-link-buttonon the primitive).docs/frontend.mdthat Share sends the profile URL; AddressRow still copies the wallet.Acceptance criteria
/trader/{valid terra1…}, a Share control is visible at 375px and 1280px, light and dark.navigator.shareis called with the canonicalorigin/trader/{address}URL (no query/hash). User cancel does not show an error./trader(no address) and/trader/not-a-walletdo not show Share (or Share is disabled and does not write garbage).shell-panel/ nestedcard-glass; chrome-nesting check stays green.docs/frontend.md§ trader profile mentions Share vs address copy./trade/:pairand/charts/:pair(validated pair) and/portfolio→/trader/{wallet}only.Test plan — all paths
/trader/{valid}, mocknavigator.shareresolvessharerejectsAbortErrorsharerejectsTypeError/NotAllowedErrornavigator.shareshare{ ok: false }COPY_BUTTON_FAILURE_MESSAGE; no raw exception?utm=or#foo/trader/evil/ empty/trader/trader/{addr}; You badge unchangedgetTrader404/trader/{addr}getTrader502/trader/{wallet}, not/portfolio/trade/{pair}//charts/{pair}/trader/:addr--ink/btn-mutedor icon; focus-visible ring/trader/{dev}trader-share-linkvisible; no new console errors beyond allowlistVitest: helper unit tests (URL build, reject invalid, strip search/hash) +
ShareLinkButtonwith injectedshare/copyToClipboard+TraderPage/TraderSummaryStatspresence tests. Playwright: extende2e/trader-page.spec.ts(visibility; optional clipboard grant). Smoke workers stay 5.Test plan — attack, hack, and abuse
Share is client-only and must not widen write surface or leak secrets.
/trader/https://evilor//evil.comisValidTerraAddressfails; no Share, or Share never buildshttps://evil….share.url/ clipboard is same-origin/trader/…only.<script>/javascript:in param or indexeraddressisValidTerraAddress. NodangerouslySetInnerHTML. Sharetext/titleare static +shortenAddress, not raw HTML.origin+ allowed path prefix + validated id. Nevernew URL(userString)as the sole href.?mnemonic=/ WC URI /#tokenleft onlocation.hreflocation.hrefthrough.VITE_PUBLIC_ORIGIN/ host header /X-Forwarded-Hostwindow.location.originof the loaded SPA. Do not bake request Host into the URL. Do not use unallowlisted OG origin logic here.textcontains “approve tx” / wallet CTA/trader/{addr}sharewithout a click.shareundefined, clipboard blocked/trade/{gem-pair}(if pair mount)target=_blank<a href>tomailto:/ third-partynavigator.shareor clipboard, not a random outbound share host.og:urlfrom walletVerification criteria
Done when all of the following are true:
/trader/{valid}shows Share; activate copies or opens the OS share sheet with…/trader/{addr}only./traderempty — no Share. Invalid segment — no Share.make test-frontend(or scoped Vitest) covers helper + button + trader presence, including abort vs fallback.e2e/trader-page.spec.ts(or added spec) assertstrader-share-linkon the connected profile path.python3 scripts/check_chrome_nesting.pyandmake verify-issue-653stay green.docs/frontend.mdstates Share vs address copy. No indexer diff required.Out of scope
react-helmet(#578)/trader/:pairroute or pair-filtered trader API/portfolioas a public URLDependencies
CopyButton/copyToClipboardalready shipped: #183)Labels / owner / priority
frontendUXenhancementmissing-implementationdocstestinge2eproductpriority::mediumOwner: frontend
Priority: P2
marked as related to #183
marked as related to #188
marked as related to #578
marked as related to #656
marked as related to #657
mentioned in commit
8069dfc31fmentioned in merge request !1171
mentioned in merge request !1172
mentioned in commit
9a82f40580mentioned in commit
676df7e520Merged to
mainvia !1172. Share lives on/trader/:addressnext to the #657 leaderboard. Optional/trade//chartsShare mounts were not in this MR (playbook: pair mounts optional; do not add Share to Swap/Pool/Limits here).Leftover: live
/trader/:addressShare (Web Share + clipboard).make verify-issue-665. Follow-up only if we want Share on/tradeor/charts.mentioned in issue #673
mentioned in issue #680
mentioned in issue #713
mentioned in issue #715
marked as related to #715
mentioned in issue #716