feat(figgos): share card via native share sheet with styled message

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 <noreply@anthropic.com>
This commit is contained in:
Chr1st1anG 2026-02-12 23:46:16 +01:00
parent 3caf731afd
commit be8e41fece
5 changed files with 199 additions and 8 deletions

View file

@ -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
</Text>
<FlippableCard figure={selected} maxWidth={SCREEN_WIDTH - 48} />
<Pressable
onPress={() => shareFigure(selected)}
className="active:opacity-90"
style={{ marginTop: 24 }}
>
<View style={{ position: 'relative' }}>
<View
className="bg-primary-dark rounded-lg"
style={{ position: 'absolute', top: 5, left: 4, right: -4, bottom: -5 }}
/>
<View
className="bg-primary rounded-lg flex-row items-center justify-center"
style={{
paddingHorizontal: 20,
paddingVertical: 12,
borderWidth: 3,
borderColor: 'rgb(255, 224, 102)',
gap: 8,
}}
>
<Ionicons name="paper-plane" size={18} color="rgb(15, 15, 30)" />
<Text
style={{
fontSize: 15,
fontWeight: '900',
letterSpacing: 2,
textTransform: 'uppercase',
color: 'rgb(15, 15, 30)',
}}
>
Share It
</Text>
</View>
</View>
</Pressable>
</View>
)}
</Pressable>

View file

@ -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
</Text>
<FlippableCard figure={selected} maxWidth={SCREEN_WIDTH - 48} />
<Pressable
onPress={() => shareFigure(selected)}
className="active:opacity-90"
style={{ marginTop: 24 }}
>
<View style={{ position: 'relative' }}>
<View
className="bg-primary-dark rounded-lg"
style={{ position: 'absolute', top: 5, left: 4, right: -4, bottom: -5 }}
/>
<View
className="bg-primary rounded-lg flex-row items-center justify-center"
style={{
paddingHorizontal: 20,
paddingVertical: 12,
borderWidth: 3,
borderColor: 'rgb(255, 224, 102)',
gap: 8,
}}
>
<Ionicons name="paper-plane" size={18} color="rgb(15, 15, 30)" />
<Text
style={{
fontSize: 15,
fontWeight: '900',
letterSpacing: 2,
textTransform: 'uppercase',
color: 'rgb(15, 15, 30)',
}}
>
Share It
</Text>
</View>
</View>
</Pressable>
</View>
)}
</Pressable>

View file

@ -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",

View file

@ -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<FigureRarity, string> = {
common: '⚪',
rare: '🔵',
epic: '🟣',
legendary: '🔥',
};
const RARITY_DROP_RATE: Record<FigureRarity, string> = {
common: '60%',
rare: '25%',
epic: '12%',
legendary: '3%',
};
let sharing = false;
export async function shareFigure(figure: FigureResponse): Promise<void> {
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<string, string> = {
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;
}

36
pnpm-lock.yaml generated
View file

@ -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