├── .gitignore ├── App.tsx ├── app.json ├── assets ├── adaptive-icon.png ├── cards │ ├── Card 1.png │ ├── Card 2.png │ ├── Card 3.png │ ├── Card 4.png │ ├── Card 5.png │ ├── Card 6.png │ ├── Card 7.png │ ├── Card 8.png │ └── Card 9.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── package-lock.json ├── package.json ├── src └── components │ ├── Card.tsx │ └── CardsList.tsx └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from 'expo-status-bar'; 2 | import { StyleSheet, SafeAreaView } from 'react-native'; 3 | import { GestureHandlerRootView } from 'react-native-gesture-handler'; 4 | 5 | import CardsList from './src/components/CardsList'; 6 | 7 | export default function App() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | const styles = StyleSheet.create({ 20 | container: { 21 | flex: 1, 22 | backgroundColor: 'black', 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "WalletApp", 4 | "slug": "WalletApp", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./assets/adaptive-icon.png", 23 | "backgroundColor": "#ffffff" 24 | } 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/cards/Card 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/cards/Card 1.png -------------------------------------------------------------------------------- /assets/cards/Card 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/cards/Card 2.png -------------------------------------------------------------------------------- /assets/cards/Card 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/cards/Card 3.png -------------------------------------------------------------------------------- /assets/cards/Card 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/cards/Card 4.png -------------------------------------------------------------------------------- /assets/cards/Card 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/cards/Card 5.png -------------------------------------------------------------------------------- /assets/cards/Card 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/cards/Card 6.png -------------------------------------------------------------------------------- /assets/cards/Card 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/cards/Card 7.png -------------------------------------------------------------------------------- /assets/cards/Card 8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/cards/Card 8.png -------------------------------------------------------------------------------- /assets/cards/Card 9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/cards/Card 9.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notJust-dev/AppleWallet/2a572be5a9dda6bf3e2a9c94bd9d232235e31fc1/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['react-native-reanimated/plugin'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "walletapp", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "expo": "~50.0.14", 13 | "expo-status-bar": "~1.11.1", 14 | "react": "18.2.0", 15 | "react-native": "0.73.6", 16 | "typescript": "^5.3.0", 17 | "@types/react": "~18.2.45", 18 | "react-native-gesture-handler": "~2.14.0", 19 | "react-native-reanimated": "~3.6.2" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.20.0" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import Animated, { 3 | Easing, 4 | clamp, 5 | useAnimatedReaction, 6 | useSharedValue, 7 | withTiming, 8 | } from 'react-native-reanimated'; 9 | import { Gesture, GestureDetector } from 'react-native-gesture-handler'; 10 | import { View, useWindowDimensions, StyleSheet } from 'react-native'; 11 | 12 | const Card = ({ card, index, scrollY, activeCardIndex }) => { 13 | const [cardHeight, setCardHeight] = useState(0); 14 | const translateY = useSharedValue(0); 15 | 16 | const { height: screenHeight } = useWindowDimensions(); 17 | 18 | useAnimatedReaction( 19 | () => scrollY.value, 20 | (current) => { 21 | translateY.value = clamp(-current, -index * cardHeight, 0); 22 | } 23 | ); 24 | 25 | useAnimatedReaction( 26 | () => activeCardIndex.value, 27 | (current, pervious) => { 28 | if (current === pervious) { 29 | return; 30 | } 31 | if (activeCardIndex.value === null) { 32 | // No card selected, move to list view 33 | translateY.value = withTiming( 34 | clamp(-scrollY.value, -index * cardHeight, 0) 35 | ); 36 | } else if (activeCardIndex.value === index) { 37 | // This card becomes active 38 | translateY.value = withTiming(-index * cardHeight, { 39 | easing: Easing.out(Easing.quad), 40 | duration: 500, 41 | }); 42 | } else { 43 | // Another card is active, move to the bottom 44 | translateY.value = withTiming( 45 | -index * cardHeight * 0.9 + screenHeight * 0.7, 46 | { 47 | easing: Easing.out(Easing.quad), 48 | duration: 500, 49 | } 50 | ); 51 | } 52 | } 53 | ); 54 | 55 | const tap = Gesture.Tap().onEnd(() => { 56 | if (activeCardIndex.value === null) { 57 | activeCardIndex.value = index; 58 | } else { 59 | activeCardIndex.value = null; 60 | } 61 | }); 62 | 63 | return ( 64 | 65 | 66 | 69 | setCardHeight(event.nativeEvent.layout.height + 10) 70 | } 71 | style={[styles.image, { transform: [{ translateY }] }]} 72 | /> 73 | 74 | 75 | ); 76 | }; 77 | 78 | const styles = StyleSheet.create({ 79 | container: { 80 | shadowColor: '#B387DF', 81 | shadowOffset: { 82 | width: 0, 83 | height: 2, 84 | }, 85 | shadowOpacity: 0.25, 86 | shadowRadius: 3.84, 87 | 88 | elevation: 5, 89 | }, 90 | image: { 91 | width: '100%', 92 | height: undefined, 93 | aspectRatio: 7 / 4, 94 | marginVertical: 5, 95 | }, 96 | }); 97 | 98 | export default Card; 99 | -------------------------------------------------------------------------------- /src/components/CardsList.tsx: -------------------------------------------------------------------------------- 1 | import { View, useWindowDimensions } from 'react-native'; 2 | import { Gesture, GestureDetector } from 'react-native-gesture-handler'; 3 | import { 4 | cancelAnimation, 5 | useSharedValue, 6 | withDecay, 7 | clamp, 8 | withClamp, 9 | } from 'react-native-reanimated'; 10 | import Card from './Card'; 11 | import { useState } from 'react'; 12 | 13 | const cards = [ 14 | require('../../assets/cards/Card 1.png'), 15 | require('../../assets/cards/Card 2.png'), 16 | require('../../assets/cards/Card 3.png'), 17 | require('../../assets/cards/Card 4.png'), 18 | require('../../assets/cards/Card 5.png'), 19 | require('../../assets/cards/Card 6.png'), 20 | require('../../assets/cards/Card 7.png'), 21 | require('../../assets/cards/Card 8.png'), 22 | require('../../assets/cards/Card 9.png'), 23 | ]; 24 | 25 | const CardsList = () => { 26 | const [listHeight, setListHeight] = useState(0); 27 | const { height: screenHeight } = useWindowDimensions(); 28 | 29 | const activeCardIndex = useSharedValue(null); 30 | 31 | const scrollY = useSharedValue(0); 32 | const maxScrollY = listHeight - screenHeight + 70; 33 | 34 | const pan = Gesture.Pan() 35 | .onBegin(() => { 36 | cancelAnimation(scrollY); 37 | }) 38 | .onChange((event) => { 39 | scrollY.value = clamp(scrollY.value - event.changeY, 0, maxScrollY); 40 | }) 41 | .onEnd((event) => { 42 | scrollY.value = withClamp( 43 | { min: 0, max: maxScrollY }, 44 | withDecay({ velocity: -event.velocityY }) 45 | ); 46 | }); 47 | 48 | return ( 49 | 50 | setListHeight(event.nativeEvent.layout.height)} 53 | > 54 | {cards.map((card, index) => ( 55 | 62 | ))} 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default CardsList; 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "extends": "expo/tsconfig.base" 4 | } 5 | --------------------------------------------------------------------------------