diff --git a/app/_layout.tsx b/app/_layout.tsx index d53eb0d1..37040109 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -11,6 +11,7 @@ import * as SplashScreen from "expo-splash-screen"; import { StatusBar } from "expo-status-bar"; import { swrConfiguration } from "lib/swr"; import * as React from "react"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; import { SafeAreaView } from "react-native-safe-area-context"; import Toast from "react-native-toast-message"; import { SWRConfig } from "swr"; @@ -120,18 +121,20 @@ export default function RootLayout() { className="w-full h-full bg-background" edges={["left", "right", "bottom"]} > - - - - - - - + + + + + + + + + diff --git a/components/GestureWrapper.tsx b/components/GestureWrapper.tsx new file mode 100644 index 00000000..4f0d816b --- /dev/null +++ b/components/GestureWrapper.tsx @@ -0,0 +1,59 @@ +import { useFocusEffect } from "expo-router"; +import React from "react"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import Animated, { + runOnJS, + useAnimatedStyle, + useSharedValue, + withDelay, + withSequence, + withTiming, +} from "react-native-reanimated"; + +interface GestureWrapperProps { + children: React.ReactNode; + onSwipe: (direction: "left" | "right") => void; + shouldWiggle?: boolean; +} + +export function GestureWrapper({ + children, + onSwipe, + shouldWiggle, +}: GestureWrapperProps) { + const wiggle = useSharedValue(0); + + useFocusEffect( + React.useCallback(() => { + if (shouldWiggle) { + wiggle.value = withDelay( + 500, + withSequence( + withTiming(-10, { duration: 150 }), + withTiming(10, { duration: 300 }), + withTiming(0, { duration: 150 }), + ), + ); + } + }, [shouldWiggle, wiggle]), + ); + + const gesture = Gesture.Pan().onEnd((evt) => { + const threshold = 50; + if (evt.translationX < -threshold) { + runOnJS(onSwipe)("left"); + } else if (evt.translationX > threshold) { + runOnJS(onSwipe)("right"); + } + }); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: wiggle.value }], + })); + + return ( + + {children} + + ); +} diff --git a/package.json b/package.json index 3f787ff5..bbfefb42 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-native": "0.76.7", + "react-native-gesture-handler": "~2.20.2", "react-native-get-random-values": "^1.9.0", "react-native-qrcode-svg": "^6.3.1", "react-native-reanimated": "~3.16.1", @@ -104,5 +105,12 @@ "transformIgnorePatterns": [ "node_modules/(?!(?:.pnpm/)?((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg))" ] + }, + "expo": { + "doctor": { + "reactNativeDirectoryCheck": { + "listUnknownPackages": false + } + } } } diff --git a/pages/Home.tsx b/pages/Home.tsx index 839ea935..c2dafe7f 100644 --- a/pages/Home.tsx +++ b/pages/Home.tsx @@ -18,6 +18,7 @@ import { Text } from "~/components/ui/text"; import { LinearGradient } from "expo-linear-gradient"; import { SvgProps } from "react-native-svg"; import AlbyBanner from "~/components/AlbyBanner"; +import { GestureWrapper } from "~/components/GestureWrapper"; import LargeArrowDown from "~/components/icons/LargeArrowDown"; import LargeArrowUp from "~/components/icons/LargeArrowUp"; import Screen from "~/components/Screen"; @@ -60,6 +61,20 @@ export function Home() { } } + function handleSwipe(direction: "left" | "right") { + if (!wallets.length) { + return; + } + let newId = selectedWalletId; + if (direction === "left") { + newId = (selectedWalletId + 1) % wallets.length; + } else { + newId = (selectedWalletId - 1 + wallets.length) % wallets.length; + } + // SWR detects the key change and updates the balance + useAppStore.getState().setSelectedWalletId(newId); + } + return ( <> - 1} > - {wallets.length > 1 && ( - { - router.push("/settings/wallets"); - }} - > - + {wallets.length > 1 && ( + { + router.push("/settings/wallets"); + }} > - {wallets[selectedWalletId].name || DEFAULT_WALLET_NAME} - - - )} - - {balance && !refreshingBalance ? ( - <> - + + {wallets[selectedWalletId].name || DEFAULT_WALLET_NAME} + + + )} + + {balance && !refreshingBalance ? ( + <> + + {balanceDisplayMode === "sats" && + new Intl.NumberFormat().format( + Math.floor(balance.balance / 1000), + )} + {balanceDisplayMode === "fiat" && + getFiatAmount && + getFiatAmount(Math.floor(balance.balance / 1000))} + {balanceDisplayMode === "hidden" && "****"} + + {balanceDisplayMode === "sats" && ( + + sats + + )} + + ) : ( + + )} + + + {balance && !refreshingBalance ? ( + {balanceDisplayMode === "sats" && - new Intl.NumberFormat().format( - Math.floor(balance.balance / 1000), - )} - {balanceDisplayMode === "fiat" && getFiatAmount && getFiatAmount(Math.floor(balance.balance / 1000))} - {balanceDisplayMode === "hidden" && "****"} + {balanceDisplayMode === "fiat" && + new Intl.NumberFormat().format( + Math.floor(balance.balance / 1000), + ) + " sats"} - {balanceDisplayMode === "sats" && ( - - sats - - )} - - ) : ( - - )} - - - {balance && !refreshingBalance ? ( - - {balanceDisplayMode === "sats" && - getFiatAmount && - getFiatAmount(Math.floor(balance.balance / 1000))} - {balanceDisplayMode === "fiat" && - new Intl.NumberFormat().format( - Math.floor(balance.balance / 1000), - ) + " sats"} - - ) : ( - - )} - - + ) : ( + + )} + + + {new Date().getDate() === 21 && } diff --git a/yarn.lock b/yarn.lock index 0cb47c61..81f7828e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -842,6 +842,13 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@egjs/hammerjs@^2.0.17": + version "2.0.17" + resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124" + integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A== + dependencies: + "@types/hammerjs" "^2.0.36" + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" @@ -2266,6 +2273,11 @@ dependencies: "@types/node" "*" +"@types/hammerjs@^2.0.36": + version "2.0.46" + resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.46.tgz#381daaca1360ff8a7c8dff63f32e69745b9fb1e1" + integrity sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" @@ -5231,6 +5243,13 @@ hermes-parser@0.25.1: dependencies: hermes-estree "0.25.1" +hoist-non-react-statics@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hosted-git-info@^7.0.0: version "7.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz#9b751acac097757667f30114607ef7b661ff4f17" @@ -7937,7 +7956,7 @@ react-helmet-async@^1.3.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-is@^16.13.1: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -7954,6 +7973,16 @@ react-native-css-interop@0.1.22: lightningcss "^1.27.0" semver "^7.6.3" +react-native-gesture-handler@~2.20.2: + version "2.20.2" + resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz#73844c8e9c417459c2f2981bc4d8f66ba8a5ee66" + integrity sha512-HqzFpFczV4qCnwKlvSAvpzEXisL+Z9fsR08YV5LfJDkzuArMhBu2sOoSPUF/K62PCoAb+ObGlTC83TKHfUd0vg== + dependencies: + "@egjs/hammerjs" "^2.0.17" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + prop-types "^15.7.2" + react-native-get-random-values@^1.9.0: version "1.11.0" resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz#1ca70d1271f4b08af92958803b89dccbda78728d"