Skip to content

feat: Share receipt to social + native sharing#874

Open
C0mberry wants to merge 7 commits intosolana-foundation:masterfrom
hoodieshq:development-share-to-social
Open

feat: Share receipt to social + native sharing#874
C0mberry wants to merge 7 commits intosolana-foundation:masterfrom
hoodieshq:development-share-to-social

Conversation

@C0mberry
Copy link
Contributor

@C0mberry C0mberry commented Mar 10, 2026

Description

  • adding share to x btn
  • adding native sharing
  • fixing share btn height

Type of change

  • Bug fix
  • New feature

Screenshots

Desktop Mobile
Screenshot 2026-03-05 at 14 46 01 IMG_8760

Testing

  1. Open http://localhost:3000/tx/4izwTCUeRGAMGReXeXDumiBzAgXPGz6KzccacCf1WU5YXCCpSDjAQT7J6D6dY45bL1NW9AiqwCuWEnz3hbGtZS2y?view=receipt on desktop

  2. See share popup

  3. Open the same link on mobile

  4. See native sharing menu

Related Issues

HOO-320
HOO-319

Checklist

  • My code follows the project's style guidelines
  • All tests pass locally and in CI
  • I have run build:info script to update build information
  • CI/CD checks pass
  • I have included screenshots for protocol screens (if applicable)

* share to X init

* init share native

* removed native sharing for desctop and fixed btn height

* move back native sharing desktop

* add sentry error handling

* fix tests

* add native share for mobile only

* codestyle update
@vercel
Copy link

vercel bot commented Mar 10, 2026

@C0mberry is attempting to deploy a commit to the Solana Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR adds social and native sharing capabilities to the transaction receipt view. On mobile devices (detected via pointer: coarse + hover: none media queries plus Web Share API presence), a native OS share button is shown. On desktop, a popover expands to offer "Share on X" and "Copy Link" options. It also extracts a server.ts barrel from the receipt feature for use by the OG image route, and updates the route's test suite accordingly.

Key changes:

  • useCanNativeShare: New hook that correctly gates on both navigator.share and navigator.canShare availability alongside mobile media-query heuristics, preventing broken UX on older iOS versions that lacked canShare.
  • handleNativeShare: Async share handler with AbortError suppression and re-throw for unexpected errors; includes title metadata for richer share-sheet previews.
  • ShareOnXShareItem: New component that opens the X intent URL via globalThis.open with noreferrer; the if (win) onShare?.() analytics guard is still at risk of never firing in modern browsers — this was discussed at length in prior review threads and has not yet been fully resolved.
  • server.ts barrel: Clean separation of server-only exports; fixes test isolation by removing client-bundle dependencies from the OG route mock.
  • Analytics: Two new tracking events (rcpt_share_native, rcpt_share_on_x) added consistently with the existing pattern and GA4 length enforcement.

Confidence Score: 3/5

  • Core sharing functionality works correctly, but X analytics tracking reliability remains unresolved from prior review threads.
  • The feature itself is solid — useCanNativeShare is well-guarded, the server barrel refactor is clean, and tests pass. However, the rcpt_share_on_x analytics event is likely silently dropped in modern Chrome and Firefox because noreferrer in window.open features implies noopener, causing the return value to be null and the if (win) guard to always be false. This was flagged in depth in prior threads but the fix (removing noopener from the original noopener,noreferrer features string) may not fully resolve it in all browsers. The PR is mergeable but the analytics gap should be confirmed before shipping.
  • app/features/receipt/ui/ShareOnXShareItem.tsx — verify window.open return value behaviour with noreferrer across target browsers to confirm analytics fires correctly.

Important Files Changed

Filename Overview
app/features/receipt/ui/ReceiptView.tsx Core share logic added; handleNativeShare re-throws non-AbortErrors as unhandled promise rejections with no user feedback, and the if (!navigator.canShare?.(shareData)) return silently fails with no user indication — both flagged in previous threads and not yet fully addressed.
app/features/receipt/ui/ShareOnXShareItem.tsx Opens X intent URL via window.open with noreferrer; in modern browsers (Chrome 88+, Firefox 79+) noreferrer implies noopener and causes window.open to return null, so the if (win) onShare?.() analytics guard will never be satisfied — discussed in prior threads.
app/shared/lib/use-can-native-share.ts Correctly checks both navigator.share and navigator.canShare (addressing prior iOS 12–15.3 thread), plus media-query heuristics for mobile detection; initialized to false for SSR safety.
app/features/receipt/server.ts New server-only barrel file cleanly re-exports symbols needed by the OG image route; breaks the dependency on the client-safe main barrel and aligns with Next.js edge-runtime constraints.
app/og/receipt/[signature]/route.tsx Trivial import path change from @features/receipt to @features/receipt/server; no behavioural changes, correctly aligns with the new server barrel.
app/og/receipt/[signature]/tests/route.spec.ts Mock target updated to @features/receipt/server; Sentry mock added to handle transitive imports from createReceipt; removed unused mock entries for deleted exports; tests remain comprehensive.
app/shared/lib/analytics/receipt.ts Two new analytics events added (rcpt_share_native, rcpt_share_on_x) with correct GA4 length enforcement; clean and consistent with existing patterns.
app/features/receipt/ui/PopoverButton.tsx Added optional className prop correctly typed and forwarded to the inner Button; non-breaking change.
app/features/receipt/ui/icons/XIcon.tsx Correctly sized inline SVG of the X logo with aria-hidden for accessibility; no issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ReceiptView mounts] --> B{useCanNativeShare}
    B -->|navigator.share + canShare + isMobileDevice| C[canNativeShare = true]
    B -->|otherwise| D[canNativeShare = false]

    C --> E[Render native Share button]
    D --> F[Render PopoverButton]

    E -->|onClick| G[handleNativeShare]
    G --> H{navigator.canShare shareData ?}
    H -->|false / undefined| I[silent return]
    H -->|true| J[navigator.share url + title]
    J -->|resolved| K[trackShareNative]
    J -->|AbortError| L[silent return]
    J -->|other error| M[throw → unhandled rejection]

    F --> N[Share Popover opens]
    N --> O[ShareOnXShareItem]
    N --> P[CopyLinkShareItem]

    O -->|handleClick| Q[globalThis.open x.com intent noreferrer]
    Q -->|win != null| R[trackShareOnX]
    Q -->|win == null popup blocked or noopener| S[analytics skipped]

    P -->|onCopy| T[trackShareCopyLink]
Loading

Last reviewed commit: 284783b

@rogaldh
Copy link
Contributor

rogaldh commented Mar 10, 2026

@greptile-apps issue was addressed

@C0mberry
Copy link
Contributor Author

@greptile-apps issue was addressed


useEffect(() => {
const hasCoarsePointer = window.matchMedia('(pointer: coarse)').matches;
const hasNoHover = window.matchMedia('(hover: none)').matches;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about moving this code into a separate hook? That should allow using it at other palces

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I agree that this should at least be extracted to a local hook, like useCanNativeShare, because having raw implementation details in a UI component is inappropriate. Additionally, the heuristic itself is undocumented. It would be better to include either comments or meaningful helper function names, such as isMobileDevice

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


useEffect(() => {
const hasCoarsePointer = window.matchMedia('(pointer: coarse)').matches;
const hasNoHover = window.matchMedia('(hover: none)').matches;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I agree that this should at least be extracted to a local hook, like useCanNativeShare, because having raw implementation details in a UI component is inappropriate. Additionally, the heuristic itself is undocumented. It would be better to include either comments or meaningful helper function names, such as isMobileDevice

receiptAnalytics.trackShareNative(signature);
} catch (e) {
if (e instanceof Error && e.name === 'AbortError') return;
Sentry.captureException(e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: I don't think this kind of error deserves sentry logging, but the decision is yours. Wdyt @rogaldh?

Copy link
Contributor

@rogaldh rogaldh Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Too harsh for the inability to share. To inform the user would be enough. We have a toast for that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

import { NextRequest } from 'next/server';
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('@sentry/nextjs', () => ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thougth: This isn't really an issue, but it might not be clear for everyone. According to FSD, everything is correct; however, barrel imports can leak unwanted elements in the development environment. Perhaps, in addition to the index, we could have something like server.ts to support separate feature API surfaces. Then, the import for server routes could look like this: import { X } from "@features/receipt/server"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdyt @rogaldh ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also agree. Having everything exported to a single place mixes client and server code. That's why we have to mock something that is not even used.
Let's split the backend endpoint code into a separate file, server.ts.

I do not like barrel files very much, but according to the FSD, it seems reasonable and also allows us to expose a "public" API for the feature.

TY @askov for noticing this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we can remove this sentry mock, it does nothing

@@ -0,0 +1,10 @@
export function XIcon() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Typically, such icons should have aria-hidden attributes since they're purely decorative. However, I'm not sure if the other icons in our project follow this recommendation.

Copy link
Contributor

@rogaldh rogaldh Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is fair, but AFAIK, event react-feathers icons do not have this attribute. Pure SVGs, same as this. So this should be applied to each icon specifically.
I do like the suggestions. Let's maybe implement a wrapper or similar solution to apply this attribute to make icons a11y-friendly. @C0mberry @askov thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@C0mberry
Copy link
Contributor Author

@greptile-apps issue was addressed

@C0mberry
Copy link
Contributor Author

@greptile-apps issue was addressed

@C0mberry C0mberry requested review from askov and rogaldh March 12, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants