From be8e41fece1e16f08e1681b5b39ba806e8668b0b Mon Sep 17 00:00:00 2001 From: Chr1st1anG <73988455+Chr1st1anG@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:46:16 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(figgos):=20share=20card=20via?= =?UTF-8?q?=20native=20share=20sheet=20with=20styled=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "Share It" button (neo-brutalist style) to carousel and collection card overlays. Downloads card image, shares with Unicode-styled message including bold rarity, stat bars, drop rate and serial number. Co-Authored-By: Claude Opus 4.6 --- .../apps/mobile/app/(tabs)/carousel.tsx | 38 ++++++++ .../apps/mobile/app/(tabs)/collection.tsx | 37 ++++++++ apps/figgos/apps/mobile/package.json | 2 + apps/figgos/apps/mobile/utils/share-figure.ts | 94 +++++++++++++++++++ pnpm-lock.yaml | 36 +++++-- 5 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 apps/figgos/apps/mobile/utils/share-figure.ts diff --git a/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx b/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx index daa92fe5e..fbbc0d4b1 100644 --- a/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx +++ b/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx @@ -11,9 +11,11 @@ import { } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useFocusEffect } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; import type { FigureResponse, FigureRarity } from '@figgos/shared'; import { api } from '../../services/api'; import FlippableCard from '../../components/FlippableCard'; +import { shareFigure } from '../../utils/share-figure'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const CARD_WIDTH = SCREEN_WIDTH * 0.8; @@ -185,6 +187,42 @@ export default function CarouselScreen() { Drag to rotate ยท Double-tap to flip + + shareFigure(selected)} + className="active:opacity-90" + style={{ marginTop: 24 }} + > + + + + + + Share It + + + + )} diff --git a/apps/figgos/apps/mobile/app/(tabs)/collection.tsx b/apps/figgos/apps/mobile/app/(tabs)/collection.tsx index d35d98d59..0138a9f86 100644 --- a/apps/figgos/apps/mobile/app/(tabs)/collection.tsx +++ b/apps/figgos/apps/mobile/app/(tabs)/collection.tsx @@ -11,9 +11,11 @@ import { } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useFocusEffect } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; import type { FigureResponse } from '@figgos/shared'; import { api } from '../../services/api'; import FlippableCard from '../../components/FlippableCard'; +import { shareFigure } from '../../utils/share-figure'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const GAP = 10; @@ -157,6 +159,41 @@ export default function CollectionScreen() { Drag to rotate ยท Double-tap to flip + shareFigure(selected)} + className="active:opacity-90" + style={{ marginTop: 24 }} + > + + + + + + Share It + + + + )} diff --git a/apps/figgos/apps/mobile/package.json b/apps/figgos/apps/mobile/package.json index ca8391758..f70cd9fe3 100644 --- a/apps/figgos/apps/mobile/package.json +++ b/apps/figgos/apps/mobile/package.json @@ -22,12 +22,14 @@ "@react-navigation/native": "^7.1.8", "expo": "~54.0.33", "expo-constants": "~18.0.13", + "expo-file-system": "^19.0.21", "expo-font": "~14.0.11", "expo-image-manipulator": "^14.0.8", "expo-image-picker": "^17.0.10", "expo-linking": "~8.0.11", "expo-router": "~6.0.23", "expo-secure-store": "~15.0.8", + "expo-sharing": "~14.0.7", "expo-splash-screen": "~31.0.13", "expo-status-bar": "~3.0.9", "expo-web-browser": "~15.0.10", diff --git a/apps/figgos/apps/mobile/utils/share-figure.ts b/apps/figgos/apps/mobile/utils/share-figure.ts new file mode 100644 index 000000000..517d43fb8 --- /dev/null +++ b/apps/figgos/apps/mobile/utils/share-figure.ts @@ -0,0 +1,94 @@ +import { Share, Platform, Alert } from 'react-native'; +import * as Sharing from 'expo-sharing'; +import { File, Paths } from 'expo-file-system/next'; +import type { FigureResponse, FigureRarity } from '@figgos/shared'; + +const RARITY_EMOJI: Record = { + common: 'โšช', + rare: '๐Ÿ”ต', + epic: '๐ŸŸฃ', + legendary: '๐Ÿ”ฅ', +}; + +const RARITY_DROP_RATE: Record = { + common: '60%', + rare: '25%', + epic: '12%', + legendary: '3%', +}; + +let sharing = false; + +export async function shareFigure(figure: FigureResponse): Promise { + if (!figure.imageUrl || sharing) return; + sharing = true; + + try { + const ext = figure.imageUrl.endsWith('.webp') ? 'webp' : 'png'; + const destination = new File(Paths.cache, `figgos-share-${figure.id}.${ext}`); + + if (!destination.exists) { + await File.downloadFileAsync(figure.imageUrl, destination); + } + + if (Platform.OS === 'ios') { + await Share.share({ + message: buildShareMessage(figure), + url: destination.uri, + }); + } else { + await Sharing.shareAsync(destination.uri, { + mimeType: ext === 'webp' ? 'image/webp' : 'image/png', + dialogTitle: `Share ${figure.name}`, + }); + } + } catch { + Alert.alert('Share failed', 'Could not share this figure. Please try again.'); + } finally { + sharing = false; + } +} + +// Unicode Mathematical Bold map for A-Z, 0-9 +const BOLD_UPPER: Record = { + A: '๐—”', B: '๐—•', C: '๐—–', D: '๐——', E: '๐—˜', F: '๐—™', G: '๐—š', H: '๐—›', I: '๐—œ', + J: '๐—', K: '๐—ž', L: '๐—Ÿ', M: '๐— ', N: '๐—ก', O: '๐—ข', P: '๐—ฃ', Q: '๐—ค', R: '๐—ฅ', + S: '๐—ฆ', T: '๐—ง', U: '๐—จ', V: '๐—ฉ', W: '๐—ช', X: '๐—ซ', Y: '๐—ฌ', Z: '๐—ญ', + '0': '๐Ÿฌ', '1': '๐Ÿญ', '2': '๐Ÿฎ', '3': '๐Ÿฏ', '4': '๐Ÿฐ', + '5': '๐Ÿฑ', '6': '๐Ÿฒ', '7': '๐Ÿณ', '8': '๐Ÿด', '9': '๐Ÿต', +}; + +function toBold(text: string): string { + return [...text].map((c) => BOLD_UPPER[c] ?? c).join(''); +} + +function statBar(value: number): string { + const filled = Math.round(value / 10); + return 'โ–ฐ'.repeat(filled) + 'โ–ฑ'.repeat(10 - filled); +} + +function buildShareMessage(figure: FigureResponse): string { + const emoji = RARITY_EMOJI[figure.rarity]; + const rate = RARITY_DROP_RATE[figure.rarity]; + const stats = figure.generatedProfile?.stats; + const serial = figure.id.split('-').pop()?.toUpperCase(); + const line = 'โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'; + + let msg = line; + msg += `\n${emoji} ${toBold(figure.rarity.toUpperCase() + ' PULL')}`; + msg += `\n${line}`; + msg += `\n"${figure.name}"`; + + if (stats) { + msg += `\nATK ${statBar(stats.attack)} ${stats.attack}`; + msg += `\nDEF ${statBar(stats.defense)} ${stats.defense}`; + msg += `\nSPL ${statBar(stats.special)} ${stats.special}`; + } + + msg += `\n${serial ? `#${serial} ยท ` : ''}${rate} drop rate`; + msg += `\n${line}`; + msg += `\nThink you can pull better? โœจ`; + msg += `\nMade with Figgos`; + + return msg; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 249a54469..874b50e42 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1603,6 +1603,9 @@ importers: expo-constants: specifier: ~18.0.13 version: 18.0.13(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + expo-file-system: + specifier: ^19.0.21 + version: 19.0.21(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) expo-font: specifier: ~14.0.11 version: 14.0.11(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -1621,6 +1624,9 @@ importers: expo-secure-store: specifier: ~15.0.8 version: 15.0.8(expo@54.0.33) + expo-sharing: + specifier: ~14.0.7 + version: 14.0.7(expo@54.0.33) expo-splash-screen: specifier: ~31.0.13 version: 31.0.13(expo@54.0.33) @@ -40212,17 +40218,27 @@ snapshots: expo: 54.0.13(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - expo-file-system@19.0.19(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): + expo-file-system@19.0.21(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): + dependencies: + expo: 54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + + expo-file-system@19.0.21(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): + dependencies: + expo: 54.0.13(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + + expo-file-system@19.0.21(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): dependencies: expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - expo-file-system@19.0.19(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)): + expo-file-system@19.0.21(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)): dependencies: expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) - expo-file-system@19.0.19(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): + expo-file-system@19.0.21(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): dependencies: expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) @@ -41101,6 +41117,10 @@ snapshots: dependencies: expo: 54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-sharing@14.0.7(expo@54.0.33): + dependencies: + expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-splash-screen@0.29.24(expo@52.0.47): dependencies: '@expo/prebuild-config': 8.2.0 @@ -41433,7 +41453,7 @@ snapshots: babel-preset-expo: 54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.12)(react-refresh@0.14.2) expo-asset: 12.0.10(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-constants: 18.0.13(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-file-system: 19.0.19(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-file-system: 19.0.21(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) expo-font: 14.0.11(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-keep-awake: 15.0.7(expo@54.0.12)(react@19.1.0) expo-modules-autolinking: 3.0.14 @@ -41470,7 +41490,7 @@ snapshots: babel-preset-expo: 54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.13)(react-refresh@0.14.2) expo-asset: 12.0.10(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-constants: 18.0.13(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-file-system: 19.0.19(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-file-system: 19.0.21(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) expo-font: 14.0.11(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-keep-awake: 15.0.7(expo@54.0.13)(react@19.1.0) expo-modules-autolinking: 3.0.15 @@ -41507,7 +41527,7 @@ snapshots: babel-preset-expo: 54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25)(react-refresh@0.14.2) expo-asset: 12.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-constants: 18.0.13(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-file-system: 19.0.19(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-file-system: 19.0.21(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) expo-font: 14.0.11(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-keep-awake: 15.0.7(expo@54.0.25)(react@19.1.0) expo-modules-autolinking: 3.0.22 @@ -41544,7 +41564,7 @@ snapshots: babel-preset-expo: 54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25)(react-refresh@0.14.2) expo-asset: 12.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-constants: 18.0.13(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-file-system: 19.0.19(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-file-system: 19.0.21(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) expo-font: 14.0.11(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-keep-awake: 15.0.7(expo@54.0.25)(react@19.1.0) expo-modules-autolinking: 3.0.22 @@ -41581,7 +41601,7 @@ snapshots: babel-preset-expo: 54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25)(react-refresh@0.14.2) expo-asset: 12.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) expo-constants: 18.0.13(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)) - expo-file-system: 19.0.19(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)) + expo-file-system: 19.0.21(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)) expo-font: 14.0.11(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) expo-keep-awake: 15.0.7(expo@54.0.25)(react@18.3.1) expo-modules-autolinking: 3.0.22