Celebration Sharing
Objectuve lets you share a real moment — a badge, a completed goal, a milestone, or a streak — as an image, through your device's own share sheet. It's one small kit reused across every celebration surface, not a bespoke share button per feature.
Related guides: Achievements (badge-sharing entry points) · Goals (goal/milestone completion) · Streak Motifs (streak sharing)
What can be shared
Four kinds, each resolved and authorized server-side by Social::ShareableSubject (rails_api/app/services/social/shareable_subject.rb):
| Kind | What it shares | Resolves against | Card copy |
|---|---|---|---|
badge | A single earned badge | The caller's own UserAction — rejects a badge the caller hasn't earned | "BADGE EARNED" |
goal_completion | A completed goal | The caller's own Goal | "GOAL COMPLETE" |
milestone | A completed milestone | The caller's own Milestone | "MILESTONE REACHED" |
streak | The caller's current streak | The caller's own User (not a per-goal streak field — see ShareableSubject's SHARE-15 comment) | "STREAK" |
A fifth variant, criticalPath, predates this kit (Critical Path's own share card, play/shareCardRender.ts) and is carried through the shared type union (ionic_frontend/src/share/types.ts) for parity checking only — it does not go through useCelebrationShare. See Critical Path for that path.
A user can only ever share their own celebration — ShareableSubject rejects an unearned badge or another user's goal/milestone/streak before a card is ever built.
Entry points
| Surface | Kind | Which one |
|---|---|---|
| Achievements Hall header | badge | The most recently earned badge |
Badge detail modal (BadgeModal.vue) | badge | Whichever badge the user opened — added by OBJ-3404 so sharing isn't limited to the most recent one |
Goal page (GoalActionBar.vue → Goal.vue) | goal_completion or milestone | The goal itself if completed, else its most recently completed milestone |
Streak details (StreakDetailsModal.vue) | streak | The user's current streak |
All four are gated behind the celebration_sharing_enabled PostHog flag (ionic_frontend/src/lib/featureFlags.ts) and, for the badge detail modal specifically, an explicit canShare prop — see Achievements § Sharing a badge for why that prop exists and isn't just achieved.
How a share happens
One composable, useCelebrationShare (ionic_frontend/src/composables/useCelebrationShare.ts), orchestrates every entry point above so they can't drift on behavior:
open(kind, subjectPublicId)— runsshareableMoment(network-only; a share should never show stale data) and, on success, opens theShareSheetwith the server-authored facts. Fires theshare_offeredPostHog event. Returnsfalse(never throws) if the subject can't be previewed — an unearned badge, someone else's goal, and so on.share()— renders the card to a PNG via canvas (share/shareCardRender.ts— see the "one card, two renderers" note below for how it's kept in sync with the on-screenShareCard.vue), then hands the image + share text + link to the raw Web Share API (share/webShare.ts). Records the completed share via therecordSharemutation and firesshare_completed. Dismissing the sheet without sharing firesshare_cancelledinstead.saveImage()— same render, downloaded locally instead of shared, for platforms/users who'd rather save the card than open a share sheet.
The share URL embeds a shareCode reserved server-side by the shareableMoment query and threaded through to recordShare, so the outgoing link and the recorded event always agree on the same code (v4.56 Phase 5, REF-01/REF-07).
Why the Web Share API, not @capacitor/share (CARD-03)
This kit shares files through the raw navigator.share() / navigator.canShare() Web Share API on web, iOS, and Android alike — it deliberately does not use @capacitor/share. The rationale lives in a source comment (ionic_frontend/src/share/webShare.ts:1-16) and is recorded here so it's discoverable without reading that file cold — OBJ-3404 originally asked for @capacitor/share precisely because this decision wasn't visible anywhere else.
@capacitor/share's web implementation is a thin wrapper around navigator.share() that forwards only title/text/url — it does not pass through a files array on web. Sharing a file through the Capacitor plugin on native would additionally require writing the in-memory PNG Blob to disk via @capacitor/filesystem first, since native Share.share() takes a file path/URI, not a Blob. But Capacitor apps already run inside a WKWebView (iOS 15+) / Chromium WebView (Android) that both implement navigator.share() with files support directly — so calling the raw Web Share API covers web, iOS, and Android with one code path and no filesystem hop.
InviteShareSheet.vue — the ally-invite share sheet — was migrated off @capacitor/share onto this same helper for the same reason. If you're reaching for @capacitor/share for a new share surface in this app, read webShare.ts's comment first; the answer is almost certainly "use share/webShare.ts instead."
canShareFiles() probes navigator.canShare({ files: [...] }) up front; when file sharing isn't supported, shareFile() falls back to copying the share text + link to the clipboard rather than failing silently.
One card, two renderers
share/ShareCard.vue (the on-screen preview) and share/shareCardRender.ts (the canvas PNG export) are two independent renderers — one DOM, one CanvasRenderingContext2D — but must draw the same card, a standing UI-SPEC requirement (OBJ-3680) so what a user previews in the sheet is exactly what they end up sharing. They're kept from drifting by sharing the same content source (share/copy.ts's cardCopy(), share/layout.ts's band geometry) rather than by sharing markup; a design-token or copy change still needs both files updated together.
Testing
ionic_frontend/tests/unit/composables/useCelebrationShare.spec.ts— open/share/cancel lifecycle, analytics events, shareCode threadingionic_frontend/tests/unit/components/BadgeModal.spec.ts,ionic_frontend/tests/unit/views/Achievements.render.spec.ts— badge-modal share control (visibility, selected-key correctness, re-entrancy, error/Retry path)rails_api/spec/services/social/shareable_subject_spec.rb— authorization per kind (unearned badge, another user's goal, etc.)
Last updated: 2026-09-13 (v4.8.4) — badge detail modal sharing (OBJ-3404)