├── .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 |
--------------------------------------------------------------------------------