Mobile: standalone photo demo + full-bleed full-screen cards
CI / policy (push) Successful in 4s
CI / profile (push) Successful in 12s
CI / mobile (push) Successful in 31s
CI / backend (push) Failing after 1m9s

- New /demo flow (no login/backend): take/pick a photo → card front;
  swipe → back reveals QR + name/title. Built in-memory on device.
- CardFace renders image backgrounds (local URI or asset key) + borderRadius prop
- CardViewer `fullScreen` mode fills the device window (useWindowDimensions),
  radius 0, status bar hidden; applied to the demo and the real card viewer
- Landing screen offers the demo without signing in
- Added expo-image-picker (camera/photo perms); pinned expo-constants/expo-linking
  to SDK 51 (npm had resolved newer-SDK majors that broke the Gradle build)
- Native android/ is gitignored (regenerated by `expo prebuild`)

Built and deployed to a physical Android device (BUILD SUCCESSFUL).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-04 17:49:52 -05:00
co-authored by Claude Opus 4.8
parent 826ef69030
commit bc6f158f43
9 changed files with 387 additions and 75 deletions
+18
View File
@@ -0,0 +1,18 @@
# Expo / Metro
.expo/
dist/
node_modules/
# Native projects are generated by `expo prebuild` (CNG) — not committed.
/android
/ios
# Misc
*.log
.DS_Store
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
# The following patterns were generated by expo-cli
expo-env.d.ts
# @end expo-cli
+10 -1
View File
@@ -15,7 +15,16 @@ const config: ExpoConfig = {
android: { android: {
package: "com.cardclaws.app", package: "com.cardclaws.app",
}, },
plugins: ["expo-router"], plugins: [
"expo-router",
[
"expo-image-picker",
{
photosPermission: "CardClaws uses your photos to set your card image.",
cameraPermission: "CardClaws uses the camera to capture your card image.",
},
],
],
experiments: { experiments: {
typedRoutes: true, typedRoutes: true,
}, },
+1 -1
View File
@@ -41,7 +41,7 @@ export default function CardViewScreen() {
</View> </View>
) : ( ) : (
<> <>
<CardViewer card={data.definition} /> <CardViewer card={data.definition} fullScreen />
<ShareSheet <ShareSheet
cardId={cardId} cardId={cardId}
handle={data.handle} handle={data.handle}
+235
View File
@@ -0,0 +1,235 @@
// Standalone, no-login demo (no backend needed): take/pick a photo → it's your
// card front; swipe → the back reveals a QR code + your info. Everything is
// built in-memory on the device.
import * as ImagePicker from "expo-image-picker";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useState } from "react";
import {
Image,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import { CardViewer } from "../src/components/card/CardViewer";
import { newLayerId } from "../src/stores/cardStore";
import { CardDefinition, DEFAULT_SETTINGS, TextLayer } from "../src/types/card";
function textLayer(text: string, y: number, size: number, color: string): TextLayer {
return {
id: newLayerId(),
type: "text",
x: 0.08,
y,
width: 0.84,
height: 0.12,
opacity: 1,
zIndex: 10,
text,
fontFamily: "System",
fontWeight: 700,
fontSize: size,
lineHeight: size + 4,
letterSpacing: 0,
color,
align: "left",
};
}
/// Build the demo card in memory: photo as the full-bleed face, QR + info on the
/// back.
function buildDemoCard(imageUri: string, name: string, title: string): CardDefinition {
return {
id: "demo",
ownerId: "demo",
handle: "demo",
version: 1,
face: {
layers: [
textLayer(name, 0.78, 30, "#ffffff"),
title ? textLayer(title, 0.87, 18, "#f0f0f2") : textLayer("", 0.87, 1, "#ffffff"),
],
background: { type: "image", value: imageUri },
entryAnimation: "fade",
},
back: {
layers: [
{
id: newLayerId(),
type: "qr",
x: 0.27,
y: 0.14,
width: 0.46,
height: 0.31,
opacity: 1,
zIndex: 5,
},
textLayer(name, 0.58, 26, "#f5f5f7"),
title ? textLayer(title, 0.67, 17, "#9a9aa0") : textLayer("", 0.67, 1, "#fff"),
],
background: { type: "solid", value: "#101014" },
entryAnimation: "fade",
},
palette: { colors: [] },
settings: DEFAULT_SETTINGS,
};
}
export default function DemoScreen() {
const [imageUri, setImageUri] = useState<string | null>(null);
const [name, setName] = useState("Omar Sobh");
const [title, setTitle] = useState("Founder & CEO");
const [url, setUrl] = useState("https://cardclaws.com/omar");
const [showing, setShowing] = useState(false);
const pickFromLibrary = async () => {
const res = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [2, 3],
quality: 0.9,
});
if (!res.canceled) setImageUri(res.assets[0].uri);
};
const takePhoto = async () => {
const perm = await ImagePicker.requestCameraPermissionsAsync();
if (!perm.granted) return;
const res = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [2, 3],
quality: 0.9,
});
if (!res.canceled) setImageUri(res.assets[0].uri);
};
if (showing && imageUri) {
const card = buildDemoCard(imageUri, name, title);
return (
<View style={styles.viewerRoot}>
{/* Hide the header + status bar so the card is truly full-bleed. */}
<Stack.Screen options={{ headerShown: false }} />
<StatusBar hidden />
<CardViewer card={card} profileUrl={url} fullScreen />
<Text style={styles.swipeHint}>Swipe or tap the card to flip →</Text>
<Pressable style={styles.editBtn} onPress={() => setShowing(false)}>
<Text style={styles.editText}>Edit</Text>
</Pressable>
</View>
);
}
return (
<ScrollView style={styles.root} contentContainerStyle={styles.content}>
<Stack.Screen options={{ title: "Card demo" }} />
<Text style={styles.h1}>Make your card</Text>
{imageUri ? (
<Image source={{ uri: imageUri }} style={styles.preview} resizeMode="cover" />
) : (
<View style={[styles.preview, styles.previewEmpty]}>
<Text style={styles.previewHint}>Your photo becomes the card front</Text>
</View>
)}
<View style={styles.photoRow}>
<Pressable style={styles.photoBtn} onPress={takePhoto}>
<Text style={styles.photoText}>Take photo</Text>
</Pressable>
<Pressable style={styles.photoBtn} onPress={pickFromLibrary}>
<Text style={styles.photoText}>Choose photo</Text>
</Pressable>
</View>
<Field label="Name" value={name} onChangeText={setName} />
<Field label="Title" value={title} onChangeText={setTitle} />
<Field label="QR link" value={url} onChangeText={setUrl} autoCapitalize="none" />
<Pressable
style={[styles.cta, !imageUri && styles.ctaDisabled]}
disabled={!imageUri}
onPress={() => setShowing(true)}
>
<Text style={styles.ctaText}>Show my card</Text>
</Pressable>
</ScrollView>
);
}
function Field({
label,
...props
}: { label: string } & React.ComponentProps<typeof TextInput>) {
return (
<View style={styles.field}>
<Text style={styles.label}>{label}</Text>
<TextInput {...props} style={styles.input} placeholderTextColor="#6b6b70" />
</View>
);
}
const styles = StyleSheet.create({
root: { flex: 1, backgroundColor: "#0a0a0c" },
content: { padding: 20, gap: 14 },
h1: { color: "#f5f5f7", fontSize: 28, fontWeight: "800" },
preview: {
width: "100%",
aspectRatio: 2 / 3,
borderRadius: 20,
backgroundColor: "#15151a",
maxHeight: 360,
alignSelf: "center",
},
previewEmpty: { alignItems: "center", justifyContent: "center" },
previewHint: { color: "#6b6b70" },
photoRow: { flexDirection: "row", gap: 12 },
photoBtn: {
flex: 1,
backgroundColor: "#222228",
borderRadius: 14,
paddingVertical: 14,
alignItems: "center",
},
photoText: { color: "#f5f5f7", fontWeight: "600", fontSize: 16 },
field: { gap: 6 },
label: { color: "#9a9aa0", fontSize: 14 },
input: {
backgroundColor: "#15151a",
color: "#f5f5f7",
borderRadius: 12,
paddingHorizontal: 14,
paddingVertical: 12,
fontSize: 16,
},
cta: {
backgroundColor: "#ff3b30",
borderRadius: 16,
paddingVertical: 16,
alignItems: "center",
marginTop: 8,
},
ctaDisabled: { opacity: 0.4 },
ctaText: { color: "#fff", fontWeight: "700", fontSize: 17 },
viewerRoot: { flex: 1, backgroundColor: "#0a0a0c" },
swipeHint: {
position: "absolute",
bottom: 90,
alignSelf: "center",
color: "#9a9aa0",
fontSize: 14,
},
editBtn: {
position: "absolute",
bottom: 36,
alignSelf: "center",
backgroundColor: "#222228",
borderRadius: 20,
paddingHorizontal: 24,
paddingVertical: 12,
},
editText: { color: "#f5f5f7", fontWeight: "600" },
});
+26 -5
View File
@@ -1,8 +1,29 @@
// Entry redirect: authenticated users land on their cards, others on login. // Landing: jump straight into the no-login demo, or sign in to the full app.
import { Redirect } from "expo-router"; import { useRouter } from "expo-router";
import { useAuthStore } from "../src/stores/authStore"; import { Pressable, StyleSheet, Text, View } from "react-native";
export default function Index() { export default function Index() {
const authed = useAuthStore((s) => s.accessToken !== null); const router = useRouter();
return <Redirect href={authed ? "/(tabs)/cards" : "/(auth)/login"} />; return (
<View style={styles.root}>
<Text style={styles.title}>CardClaws</Text>
<Text style={styles.tagline}>The other side of you.</Text>
<Pressable style={styles.primary} onPress={() => router.push("/demo")}>
<Text style={styles.primaryText}>Try the card demo</Text>
</Pressable>
<Pressable onPress={() => router.push("/(auth)/login")}>
<Text style={styles.link}>Sign in to the full app</Text>
</Pressable>
</View>
);
} }
const styles = StyleSheet.create({
root: { flex: 1, backgroundColor: "#0a0a0c", alignItems: "center", justifyContent: "center", padding: 24, gap: 14 },
title: { color: "#f5f5f7", fontSize: 44, fontWeight: "800", letterSpacing: -1 },
tagline: { color: "#9a9aa0", marginBottom: 24 },
primary: { backgroundColor: "#ff3b30", borderRadius: 16, paddingVertical: 16, paddingHorizontal: 40 },
primaryText: { color: "#fff", fontWeight: "700", fontSize: 17 },
link: { color: "#9a9aa0", marginTop: 12 },
});
+30 -50
View File
@@ -12,8 +12,11 @@
"@tanstack/react-query": "^5.51.0", "@tanstack/react-query": "^5.51.0",
"axios": "^1.7.2", "axios": "^1.7.2",
"expo": "~51.0.28", "expo": "~51.0.28",
"expo-constants": "~16.0.2",
"expo-haptics": "~13.0.1", "expo-haptics": "~13.0.1",
"expo-router": "~3.5.23", "expo-image-picker": "~15.1.0",
"expo-linking": "~6.3.1",
"expo-router": "~3.5.24",
"expo-status-bar": "~1.12.1", "expo-status-bar": "~1.12.1",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "18.2.0", "react": "18.2.0",
@@ -8547,7 +8550,7 @@
"expo": "*" "expo": "*"
} }
}, },
"node_modules/expo-asset/node_modules/expo-constants": { "node_modules/expo-constants": {
"version": "16.0.2", "version": "16.0.2",
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-16.0.2.tgz", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-16.0.2.tgz",
"integrity": "sha512-9tNY3OVO0jfiMzl7ngb6IOyR5VFzNoN5OOazUWoeGfmMqVB5kltTemRvKraK9JRbBKIw+SOYLEmF0sEqgFZ6OQ==", "integrity": "sha512-9tNY3OVO0jfiMzl7ngb6IOyR5VFzNoN5OOazUWoeGfmMqVB5kltTemRvKraK9JRbBKIw+SOYLEmF0sEqgFZ6OQ==",
@@ -8560,45 +8563,6 @@
"expo": "*" "expo": "*"
} }
}, },
"node_modules/expo-constants": {
"version": "56.0.16",
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.16.tgz",
"integrity": "sha512-6tsiN+gmTUPp/atyA+uY9Tg8VOdXdmb4s/3TVGolfn6A/oCAraw1pcPZX5XllyD+xUguxB6eBSFAT8494hZVMA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@expo/env": "~2.3.0"
},
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo-constants/node_modules/@expo/env": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@expo/env/-/env-2.3.0.tgz",
"integrity": "sha512-9HnnIbzwTTdbwSjNLXTk0fPm9ZwMJ7c1/31tsni8HZ8Q62KzYCyspahH+V365vg5J6lr001DzNwBxVWSaYCQLg==",
"license": "MIT",
"peer": true,
"dependencies": {
"chalk": "^4.0.0",
"debug": "^4.3.4",
"getenv": "^2.0.0"
},
"engines": {
"node": ">=20.12.0"
}
},
"node_modules/expo-constants/node_modules/getenv": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
"integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/expo-file-system": { "node_modules/expo-file-system": {
"version": "17.0.1", "version": "17.0.1",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-17.0.1.tgz", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-17.0.1.tgz",
@@ -8629,6 +8593,27 @@
"expo": "*" "expo": "*"
} }
}, },
"node_modules/expo-image-loader": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-4.7.0.tgz",
"integrity": "sha512-cx+MxxsAMGl9AiWnQUzrkJMJH4eNOGlu7XkLGnAXSJrRoIiciGaKqzeaD326IyCTV+Z1fXvIliSgNW+DscvD8g==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-image-picker": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-15.1.0.tgz",
"integrity": "sha512-6cE10S7d0qG7+pcHtsYukRKuFL44ssOM0/v+5JIBzRwxpIgGA6qokr8PxasFEpxOA9NpN0gwry33jmq1qLXJTg==",
"license": "MIT",
"dependencies": {
"expo-image-loader": "~4.7.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-keep-awake": { "node_modules/expo-keep-awake": {
"version": "13.0.2", "version": "13.0.2",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-13.0.2.tgz", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-13.0.2.tgz",
@@ -8639,18 +8624,13 @@
} }
}, },
"node_modules/expo-linking": { "node_modules/expo-linking": {
"version": "56.0.13", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-56.0.13.tgz", "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-6.3.1.tgz",
"integrity": "sha512-38YrpTh6xdiDxmYSDIUffDqev1hIcEggw2fZ3IZhNp2DVLF1xvqsbO6hJD1fuBKN8P34B3Ggc9Yy26fkqdfCOA==", "integrity": "sha512-xuZCntSBGWCD/95iZ+mTUGTwHdy8Sx+immCqbUBxdvZ2TN61P02kKg7SaLS8A4a/hLrSCwrg5tMMwu5wfKr35g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"expo-constants": "~56.0.16", "expo-constants": "~16.0.0",
"invariant": "^2.2.4" "invariant": "^2.2.4"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
} }
}, },
"node_modules/expo-modules-autolinking": { "node_modules/expo-modules-autolinking": {
+5 -2
View File
@@ -15,9 +15,13 @@
"@tanstack/react-query": "^5.51.0", "@tanstack/react-query": "^5.51.0",
"axios": "^1.7.2", "axios": "^1.7.2",
"expo": "~51.0.28", "expo": "~51.0.28",
"expo-constants": "~16.0.2",
"expo-haptics": "~13.0.1", "expo-haptics": "~13.0.1",
"expo-router": "~3.5.23", "expo-image-picker": "~15.1.0",
"expo-linking": "~6.3.1",
"expo-router": "~3.5.24",
"expo-status-bar": "~1.12.1", "expo-status-bar": "~1.12.1",
"qrcode": "^1.5.4",
"react": "18.2.0", "react": "18.2.0",
"react-native": "0.74.5", "react-native": "0.74.5",
"react-native-gesture-handler": "~2.16.1", "react-native-gesture-handler": "~2.16.1",
@@ -25,7 +29,6 @@
"react-native-reanimated": "~3.10.1", "react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.5", "react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1", "react-native-screens": "3.31.1",
"qrcode": "^1.5.4",
"zustand": "^4.5.4" "zustand": "^4.5.4"
}, },
"devDependencies": { "devDependencies": {
@@ -19,6 +19,8 @@ interface Props {
side: CardSide; side: CardSide;
width: number; width: number;
height: number; height: number;
/** Corner radius; 0 for full-bleed full-screen cards. */
borderRadius?: number;
/** URL encoded by any `qr` layer on this side (the card's profile URL). */ /** URL encoded by any `qr` layer on this side (the card's profile URL). */
profileUrl?: string; profileUrl?: string;
} }
@@ -37,6 +39,16 @@ function assetUri(r2Key: string): string {
return `${API_BASE}/assets/${r2Key}`; return `${API_BASE}/assets/${r2Key}`;
} }
/// Resolve an image background to a displayable URI. `value` carries a direct
/// URI (local file:// or content:// for the demo, or a full URL); otherwise the
/// r2Key resolves to the asset endpoint.
function backgroundImageUri(bg: BackgroundConfig): string | null {
if (bg.type !== "image") return null;
if (bg.value) return bg.value;
if (bg.r2Key) return assetUri(bg.r2Key);
return null;
}
function LayerView({ function LayerView({
layer, layer,
width, width,
@@ -137,10 +149,14 @@ function LayerView({
} }
} }
export function CardFace({ side, width, height, profileUrl }: Props) { export function CardFace({ side, width, height, borderRadius = 24, profileUrl }: Props) {
const ordered = [...side.layers].sort((a, b) => a.zIndex - b.zIndex); const ordered = [...side.layers].sort((a, b) => a.zIndex - b.zIndex);
const bgImage = backgroundImageUri(side.background);
return ( return (
<View style={[styles.face, { width, height }, backgroundStyle(side.background)]}> <View style={[styles.face, { width, height, borderRadius }, backgroundStyle(side.background)]}>
{bgImage && (
<Image source={{ uri: bgImage }} style={StyleSheet.absoluteFill} resizeMode="cover" />
)}
{ordered.map((layer) => ( {ordered.map((layer) => (
<LayerView <LayerView
key={layer.id} key={layer.id}
@@ -156,7 +172,6 @@ export function CardFace({ side, width, height, profileUrl }: Props) {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
face: { face: {
borderRadius: 24,
overflow: "hidden", overflow: "hidden",
}, },
contactLine: { contactLine: {
@@ -1,6 +1,9 @@
// Full-screen card viewer: entry animation, flip, and ambient idle drift // Card viewer: entry animation, flip, and ambient idle drift (PRD §6.2).
// (PRD §6.2). Ambient brightness boost requires expo-brightness (added with the // Ambient brightness boost requires expo-brightness (added with the hardware
// hardware integration milestone); the idle motion is implemented here. // integration milestone); the idle motion is implemented here.
//
// Sizing: by default the card is a centered, rounded artifact. In `fullScreen`
// mode it fills the entire device window (PRD §5.2 "Full-bleed always").
import { useEffect } from "react"; import { useEffect } from "react";
import { StyleSheet, useWindowDimensions, View } from "react-native"; import { StyleSheet, useWindowDimensions, View } from "react-native";
@@ -18,11 +21,24 @@ import { CardDefinition } from "../../types/card";
import { CardFace } from "./CardFace"; import { CardFace } from "./CardFace";
import { CardFlip } from "./CardFlip"; import { CardFlip } from "./CardFlip";
export function CardViewer({ card }: { card: CardDefinition }) { export function CardViewer({
card,
profileUrl: profileUrlOverride,
fullScreen = false,
}: {
card: CardDefinition;
/** Override the QR target; defaults to the card's public profile URL. */
profileUrl?: string;
/** Fill the entire device window instead of a centered, rounded card. */
fullScreen?: boolean;
}) {
// useWindowDimensions is the live size of the app's drawable area; it updates
// on rotation/resize. Full-screen cards use it verbatim.
const { width, height } = useWindowDimensions(); const { width, height } = useWindowDimensions();
const cardWidth = Math.min(width * 0.92, 380); const cardWidth = fullScreen ? width : Math.min(width * 0.92, 380);
const cardHeight = Math.min(height * 0.72, cardWidth * 1.5); const cardHeight = fullScreen ? height : Math.min(height * 0.72, cardWidth * 1.5);
const profileUrl = `${PROFILE_BASE}/${card.handle}`; const radius = fullScreen ? 0 : 24;
const profileUrl = profileUrlOverride ?? `${PROFILE_BASE}/${card.handle}`;
const entry = useSharedValue(0); const entry = useSharedValue(0);
const float = useSharedValue(0); const float = useSharedValue(0);
@@ -46,14 +62,16 @@ export function CardViewer({ card }: { card: CardDefinition }) {
const containerStyle = useAnimatedStyle(() => ({ const containerStyle = useAnimatedStyle(() => ({
opacity: entry.value, opacity: entry.value,
transform: [ transform: [
{ scale: 0.92 + 0.08 * entry.value }, // Full-bleed cards fade in without the scale-from-center (which would
{ translateY: float.value * 6 }, // reveal the background at the edges); a tiny ambient drift remains.
{ scale: fullScreen ? 1 : 0.92 + 0.08 * entry.value },
{ translateY: float.value * (fullScreen ? 3 : 6) },
], ],
})); }));
return ( return (
<View style={styles.root}> <View style={[styles.root, fullScreen && styles.rootFull]}>
<Animated.View style={containerStyle}> <Animated.View style={[containerStyle, fullScreen && StyleSheet.absoluteFill]}>
<CardFlip <CardFlip
width={cardWidth} width={cardWidth}
height={cardHeight} height={cardHeight}
@@ -61,10 +79,22 @@ export function CardViewer({ card }: { card: CardDefinition }) {
hapticEnabled={card.settings.hapticEnabled} hapticEnabled={card.settings.hapticEnabled}
gesture={card.settings.flipGesture} gesture={card.settings.flipGesture}
front={ front={
<CardFace side={card.face} width={cardWidth} height={cardHeight} profileUrl={profileUrl} /> <CardFace
side={card.face}
width={cardWidth}
height={cardHeight}
borderRadius={radius}
profileUrl={profileUrl}
/>
} }
back={ back={
<CardFace side={card.back} width={cardWidth} height={cardHeight} profileUrl={profileUrl} /> <CardFace
side={card.back}
width={cardWidth}
height={cardHeight}
borderRadius={radius}
profileUrl={profileUrl}
/>
} }
/> />
</Animated.View> </Animated.View>
@@ -79,4 +109,5 @@ const styles = StyleSheet.create({
justifyContent: "center", justifyContent: "center",
backgroundColor: "#0a0a0c", backgroundColor: "#0a0a0c",
}, },
rootFull: { alignItems: "stretch", justifyContent: "flex-start" },
}); });