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"