diff --git a/packages/mobile/app.json b/packages/mobile/app.json
index fc7d73f3d2..7b7cd51c09 100644
--- a/packages/mobile/app.json
+++ b/packages/mobile/app.json
@@ -57,6 +57,12 @@
"configureAndroidBackup": true,
"faceIDPermission": "Allow $(PRODUCT_NAME) to access your Face ID biometric data."
}
+ ],
+ [
+ "expo-camera",
+ {
+ "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera"
+ }
]
],
"experiments": {
diff --git a/packages/mobile/app/_layout.tsx b/packages/mobile/app/_layout.tsx
index 3097fad341..cb6a3907ca 100644
--- a/packages/mobile/app/_layout.tsx
+++ b/packages/mobile/app/_layout.tsx
@@ -155,6 +155,10 @@ export default function RootLayout() {
}}
>
+
diff --git a/packages/mobile/app/onboarding/camera-scan.tsx b/packages/mobile/app/onboarding/camera-scan.tsx
new file mode 100644
index 0000000000..e8d728a117
--- /dev/null
+++ b/packages/mobile/app/onboarding/camera-scan.tsx
@@ -0,0 +1,195 @@
+import MaskedView from "@react-native-masked-view/masked-view";
+import { CameraView, FocusMode } from "expo-camera";
+import { useState } from "react";
+import { Dimensions, StyleSheet, TouchableOpacity, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+import { RouteHeader } from "~/components/route-header";
+import { Text } from "~/components/ui/text";
+import { Colors } from "~/constants/theme-colors";
+
+const SCAN_ICON_RADIUS_RATIO = 0.05;
+const SCAN_ICON_MASK_OFFSET_RATIO = 0.02; // used for mask to match spacing in CameraScan SVG
+const CAMERA_ASPECT_RATIO = 4 / 3;
+const SCAN_ICON_WIDTH_RATIO = 0.7;
+
+export default function Welcome() {
+ const { top } = useSafeAreaInsets();
+ const [autoFocus, setAutoFocus] = useState("off");
+
+ const resetCameraAutoFocus = () => {
+ const abortController = new AbortController();
+ setAutoFocus("off");
+ setTimeout(() => {
+ if (!abortController.signal.aborted) {
+ setAutoFocus("on");
+ }
+ }, 100);
+ return () => abortController.abort();
+ };
+
+ const overlayWidth = Dimensions.get("window").height / CAMERA_ASPECT_RATIO;
+ const cameraWidth = Dimensions.get("window").width;
+ const cameraHeight = CAMERA_ASPECT_RATIO * cameraWidth;
+ const scannerSize =
+ Math.min(overlayWidth, cameraWidth) * SCAN_ICON_WIDTH_RATIO;
+
+ return (
+
+
+
+
+ Scan to Connect
+
+
+
+
+
+
+ }
+ >
+ {
+ console.log(data);
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ Scan your QR Code
+
+ With your wallet connected, navigate{"\n"}
+ to 'Profile' > 'Link mobile device' on{"\n"}
+ Osmosis web app.
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ camera: {
+ flex: 1,
+ },
+ overlay: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ scanArea: {
+ backgroundColor: "transparent",
+ borderRadius: 24,
+ position: "relative",
+ },
+ cornerTopLeft: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ width: 60,
+ height: 60,
+ borderTopWidth: 2,
+ borderLeftWidth: 2,
+ borderColor: "white",
+ borderTopLeftRadius: 32,
+ },
+ cornerTopRight: {
+ position: "absolute",
+ top: 0,
+ right: 0,
+ width: 60,
+ height: 60,
+ borderTopWidth: 2,
+ borderRightWidth: 2,
+ borderColor: "white",
+ borderTopRightRadius: 32,
+ },
+ cornerBottomLeft: {
+ position: "absolute",
+ bottom: 0,
+ left: 0,
+ width: 60,
+ height: 60,
+ borderBottomWidth: 2,
+ borderLeftWidth: 2,
+ borderColor: "white",
+ borderBottomLeftRadius: 32,
+ },
+ cornerBottomRight: {
+ position: "absolute",
+ bottom: 0,
+ right: 0,
+ width: 60,
+ height: 60,
+ borderBottomWidth: 2,
+ borderRightWidth: 2,
+ borderColor: "white",
+ borderBottomRightRadius: 32,
+ },
+ scanText: {
+ color: "white",
+ fontSize: 24,
+ fontWeight: "600",
+ marginBottom: 16,
+ },
+ instructionText: {
+ color: Colors.osmoverse[300],
+ fontSize: 16,
+ textAlign: "center",
+ lineHeight: 24,
+ },
+});
diff --git a/packages/mobile/app/onboarding/enable-qr-code.tsx b/packages/mobile/app/onboarding/enable-qr-code.tsx
new file mode 100644
index 0000000000..a0f92ffe76
--- /dev/null
+++ b/packages/mobile/app/onboarding/enable-qr-code.tsx
@@ -0,0 +1,121 @@
+import * as Linking from "expo-linking";
+import React from "react";
+import { Image, StyleSheet, View } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+import { RouteHeader } from "~/components/route-header";
+import { Text } from "~/components/ui/text";
+
+import { Button } from "../../components/ui/button";
+import { Colors } from "../../constants/theme-colors";
+
+export default function LinkViaDesktop() {
+ const handleOpenSettings = () => {
+ Linking.openSettings();
+ };
+
+ const handleConnectManually = () => {
+ // TODO: Implement manual connection flow
+ console.log("Connect manually pressed");
+ };
+
+ return (
+
+
+
+
+ Link via Desktop
+
+
+
+
+
+
+
+
+ To Connect via QR Code, you need to allow access to the camera through
+ the iPhone settings and connect using QR code.
+
+
+
+
+
+
+
+
+ Or you can connect manually with your desktop account using your
+ pairing code.
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.osmoverse[1000],
+ },
+ content: {
+ flex: 1,
+ alignItems: "center",
+ paddingHorizontal: 24,
+ paddingTop: 40,
+ },
+ illustration: {
+ width: "100%",
+ height: 258,
+ marginBottom: 40,
+ },
+ description: {
+ fontSize: 16,
+ textAlign: "center",
+ marginBottom: 32,
+ lineHeight: 24,
+ },
+ buttonContainer: {
+ width: "100%",
+ gap: 12,
+ marginBottom: 24,
+ },
+ button: {
+ width: "100%",
+ },
+ buttonLabel: {
+ fontSize: 16,
+ fontWeight: "600",
+ },
+ manualButton: {
+ width: "100%",
+ },
+ manualButtonLabel: {
+ fontSize: 16,
+ color: Colors.osmoverse[100],
+ },
+ alternateText: {
+ fontSize: 14,
+ color: Colors.osmoverse[300],
+ textAlign: "center",
+ paddingHorizontal: 32,
+ },
+});
diff --git a/packages/mobile/app/onboarding/security.tsx b/packages/mobile/app/onboarding/security.tsx
deleted file mode 100644
index 1c5265fc34..0000000000
--- a/packages/mobile/app/onboarding/security.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useRouter } from "expo-router";
-
-import { DeviceSecurityScreen } from "../../components/onboarding/device-security-screen";
-
-export default function Security() {
- const router = useRouter();
-
- const handleUseFaceID = async () => {
- // Implement FaceID setup logic here
- router.push("/onboarding/connect");
- };
-
- const handleUseDevicePin = () => {
- // Implement PIN setup logic here
- router.push("/onboarding/connect");
- };
-
- return (
-
- );
-}
diff --git a/packages/mobile/app/onboarding/set-up-biometrics.tsx b/packages/mobile/app/onboarding/set-up-biometrics.tsx
new file mode 100644
index 0000000000..2c13505653
--- /dev/null
+++ b/packages/mobile/app/onboarding/set-up-biometrics.tsx
@@ -0,0 +1,189 @@
+import { useRouter } from "expo-router";
+import { useEffect, useState } from "react";
+import { Image, StyleSheet, View } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useShallow } from "zustand/react/shallow";
+
+import { RouteHeader } from "~/components/route-header";
+import { Text } from "~/components/ui/text";
+import { Colors } from "~/constants/theme-colors";
+import {
+ useBiometricPrompt,
+ useDeviceSupportsBiometricAuth,
+ useOsBiometricAuthEnabled,
+} from "~/hooks/biometrics";
+import { useSettingsStore } from "~/stores/settings";
+
+import { Button } from "../../components/ui/button";
+
+export default function Security() {
+ const router = useRouter();
+ const { faceId } = useDeviceSupportsBiometricAuth();
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+ const { isCheckingBiometricAvailability, isBiometricEnabled } =
+ useOsBiometricAuthEnabled();
+
+ const { setBiometricForAppAccess, setBiometricForTransactions } =
+ useSettingsStore(
+ useShallow((state) => ({
+ biometricForAppAccess: state.biometricForAppAccess,
+ biometricForTransactions: state.biometricForTransactions,
+ setBiometricForAppAccess: state.setBiometricForAppAccess,
+ setBiometricForTransactions: state.setBiometricForTransactions,
+ }))
+ );
+
+ const { authenticate: authenticateBiometrics } = useBiometricPrompt({
+ onSuccess: () => {
+ setBiometricForAppAccess(true);
+ setBiometricForTransactions(true);
+ router.push("/(tabs)");
+ },
+ });
+
+ const handleUseDevicePin = () => {
+ router.push("/(tabs)");
+ };
+
+ useEffect(() => {
+ if (!isCheckingBiometricAvailability && !isBiometricEnabled) {
+ router.push("/(tabs)");
+ }
+ }, [isCheckingBiometricAvailability, isBiometricEnabled, router]);
+
+ return (
+
+
+
+
+ Device Security
+
+
+
+
+
+
+
+
+ First, let's set up your device security.
+
+
+ To ensure your funds are only accessible by you, secure this device
+ with {faceId ? "FaceID" : "TouchID"}.
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.osmoverse[900],
+ },
+ content: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ paddingHorizontal: 24,
+ },
+ illustration: {
+ height: 231,
+ position: "relative",
+ marginBottom: 32,
+ },
+ phone: {
+ position: "absolute",
+ width: 120,
+ height: 200,
+ backgroundColor: Colors.wosmongton[500],
+ borderRadius: 20,
+ transform: [{ rotate: "-15deg" }],
+ },
+ shield: {
+ position: "absolute",
+ width: 80,
+ height: 100,
+ backgroundColor: Colors.osmoverse[700],
+ borderRadius: 12,
+ top: 50,
+ left: 60,
+ },
+ fingerprint: {
+ position: "absolute",
+ width: 40,
+ height: 40,
+ backgroundColor: Colors.wosmongton[300],
+ borderRadius: 20,
+ top: 80,
+ left: 80,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: "600",
+ color: "white",
+ textAlign: "center",
+ marginBottom: 12,
+ lineHeight: 28,
+ },
+ subtitle: {
+ fontSize: 16,
+ color: Colors.osmoverse[300],
+ textAlign: "center",
+ marginBottom: 48,
+ lineHeight: 24,
+ },
+ buttonContainer: {
+ width: "100%",
+ gap: 12,
+ },
+ button: {
+ width: "100%",
+ backgroundColor: Colors.wosmongton[500],
+ },
+ buttonLabel: {
+ fontSize: 16,
+ fontWeight: "600",
+ },
+ pinButton: {
+ width: "100%",
+ },
+ pinButtonLabel: {
+ fontSize: 16,
+ color: Colors.osmoverse[100],
+ },
+});
diff --git a/packages/mobile/app/onboarding/welcome.tsx b/packages/mobile/app/onboarding/welcome.tsx
index e8b5954115..f83d36fdde 100644
--- a/packages/mobile/app/onboarding/welcome.tsx
+++ b/packages/mobile/app/onboarding/welcome.tsx
@@ -1,12 +1,28 @@
+import MaskedView from "@react-native-masked-view/masked-view";
+import { useCameraPermissions } from "expo-camera";
+import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router";
+import { Dimensions, Image, StyleSheet, Text, View } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
-import { WelcomeScreen } from "../../components/onboarding/welcome-screen";
+import { QrCodeIcon } from "~/components/icons/qr-code";
+import { Colors } from "~/constants/theme-colors";
+
+import { Button } from "../../components/ui/button";
export default function Welcome() {
const router = useRouter();
+ const [permission, requestPermission] = useCameraPermissions();
- const handleLinkDesktop = () => {
- router.push("/onboarding/security");
+ const handleLinkDesktop = async () => {
+ if (permission?.granted) {
+ router.push("/onboarding/camera-scan");
+ } else if (permission?.canAskAgain) {
+ await requestPermission();
+ router.push("/onboarding/camera-scan");
+ } else {
+ router.push("/onboarding/enable-qr-code");
+ }
};
const handleSkip = () => {
@@ -14,6 +30,134 @@ export default function Welcome() {
};
return (
-
+
+
+
+
+ Welcome to the
+
+ Osmosis Mobile App
+
+ }
+ >
+
+
+
+
+ Link your desktop wallet, or skip this step and just browse the
+ assets. Don't worry, you can always link your wallet at any time.
+
+
+
+ }
+ title="Link via desktop"
+ textStyle={styles.buttonLabel}
+ disabled={!permission}
+ />
+
+
+
+
);
}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ content: {
+ flex: 1,
+ alignItems: "center",
+ paddingHorizontal: 24,
+ },
+ tokensContainer: {
+ width: 200,
+ height: 200,
+ position: "relative",
+ marginBottom: 48,
+ },
+ token: {
+ position: "absolute",
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ },
+ tokenCircle: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ },
+ centerToken: {
+ position: "absolute",
+ top: "50%",
+ left: "50%",
+ transform: [{ translateX: -25 }, { translateY: -25 }],
+ width: 50,
+ height: 50,
+ },
+ title: {
+ fontSize: 32,
+ fontWeight: "600",
+ color: "white",
+ },
+ subtitle: {
+ fontSize: 16,
+ color: Colors.osmoverse[300],
+ textAlign: "center",
+ marginBottom: 48,
+ lineHeight: 24,
+ },
+ buttonContainer: {
+ width: "100%",
+ gap: 12,
+ },
+ button: {
+ width: "100%",
+ flexDirection: "row",
+ gap: 8,
+ justifyContent: "center",
+ },
+ buttonLabel: {
+ fontSize: 16,
+ fontWeight: "600",
+ },
+ skipButton: {
+ width: "100%",
+ },
+ skipButtonLabel: {
+ fontSize: 16,
+ color: Colors.osmoverse[100],
+ },
+});
diff --git a/packages/mobile/assets/images/device-security.png b/packages/mobile/assets/images/device-security.png
new file mode 100644
index 0000000000..900fa550f9
Binary files /dev/null and b/packages/mobile/assets/images/device-security.png differ
diff --git a/packages/mobile/assets/images/onboarding-welcome-assets.png b/packages/mobile/assets/images/onboarding-welcome-assets.png
new file mode 100644
index 0000000000..805cf9e1ea
Binary files /dev/null and b/packages/mobile/assets/images/onboarding-welcome-assets.png differ
diff --git a/packages/mobile/assets/images/qr-code-connection.png b/packages/mobile/assets/images/qr-code-connection.png
new file mode 100644
index 0000000000..3d25b16d63
Binary files /dev/null and b/packages/mobile/assets/images/qr-code-connection.png differ
diff --git a/packages/mobile/components/icons/qr-code.tsx b/packages/mobile/components/icons/qr-code.tsx
new file mode 100644
index 0000000000..4e46eacad4
--- /dev/null
+++ b/packages/mobile/components/icons/qr-code.tsx
@@ -0,0 +1,55 @@
+import React from "react";
+import Svg, { Path } from "react-native-svg";
+
+import { Colors } from "~/constants/theme-colors";
+
+export const QrCodeIcon = ({
+ width = 25,
+ height = 24,
+ fill = Colors["osmoverse"][300],
+}) => (
+
+);
diff --git a/packages/mobile/components/lock-screen-modal.tsx b/packages/mobile/components/lock-screen-modal.tsx
index e6e7733aa2..d9e751bb6c 100644
--- a/packages/mobile/components/lock-screen-modal.tsx
+++ b/packages/mobile/components/lock-screen-modal.tsx
@@ -23,15 +23,17 @@ import {
useAuthenticationStore,
useBiometricPrompt,
} from "~/hooks/biometrics";
+import { useWallets } from "~/hooks/use-wallets";
import { useSettingsStore } from "~/stores/settings";
export const LockScreenModal = () => {
const biometricForAppAccess = useSettingsStore(
(state) => state.biometricForAppAccess
);
+ const { currentWallet } = useWallets();
const [showLockScreen, setShowLockScreen] = useState(
- biometricForAppAccess ? true : false
+ biometricForAppAccess && currentWallet ? true : false
);
const { authenticate } = useBiometricPrompt({
@@ -41,7 +43,7 @@ export const LockScreenModal = () => {
});
const triggerBiometricCheck = useCallback(async () => {
- if (biometricForAppAccess) {
+ if (biometricForAppAccess && currentWallet) {
await authenticate();
}
// Run only on mount so it doesn't trigger a biometric check on setting change
@@ -54,7 +56,7 @@ export const LockScreenModal = () => {
const subscription = AppState.addEventListener(
"change",
async (nextAppState) => {
- if (!biometricForAppAccess) {
+ if (!biometricForAppAccess || !currentWallet) {
return;
}
@@ -108,14 +110,16 @@ export const LockScreenModal = () => {
return () => {
subscription.remove();
};
- }, [biometricForAppAccess, triggerBiometricCheck]);
+ }, [biometricForAppAccess, triggerBiometricCheck, currentWallet]);
useLayoutEffect(() => {
- triggerBiometricCheck();
+ if (currentWallet) {
+ triggerBiometricCheck();
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- if (!showLockScreen) {
+ if (!showLockScreen || !currentWallet) {
return null;
}
diff --git a/packages/mobile/components/onboarding/device-security-screen.tsx b/packages/mobile/components/onboarding/device-security-screen.tsx
deleted file mode 100644
index 7b06404589..0000000000
--- a/packages/mobile/components/onboarding/device-security-screen.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import { StyleSheet, Text, View } from "react-native";
-import { SafeAreaView } from "react-native-safe-area-context";
-
-import { Colors } from "~/constants/theme-colors";
-
-import { Button } from "../ui/button";
-
-export const DeviceSecurityScreen = ({
- onUseFaceID,
- onUseDevicePin,
-}: {
- onUseFaceID: () => void;
- onUseDevicePin: () => void;
-}) => {
- return (
-
-
-
- Device Security
-
-
-
- {/* Replace with actual security illustration */}
-
-
-
-
-
-
-
-
- First, let's set up your device security.
-
-
- To ensure your funds are only accessible by you, secure this device
- with FaceID or a passcode.
-
-
-
-
-
-
-
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: Colors.osmoverse[900],
- },
- content: {
- flex: 1,
- alignItems: "center",
- justifyContent: "center",
- paddingHorizontal: 24,
- },
- header: {
- position: "absolute",
- top: 0,
- left: 0,
- right: 0,
- height: 60,
- justifyContent: "center",
- alignItems: "center",
- },
- imageContainer: {
- width: "100%",
- aspectRatio: 1,
- marginBottom: 32,
- alignItems: "center",
- justifyContent: "center",
- },
- illustration: {
- width: 200,
- height: 200,
- position: "relative",
- },
- phone: {
- position: "absolute",
- width: 120,
- height: 200,
- backgroundColor: Colors.wosmongton[500],
- borderRadius: 20,
- transform: [{ rotate: "-15deg" }],
- },
- shield: {
- position: "absolute",
- width: 80,
- height: 100,
- backgroundColor: Colors.osmoverse[700],
- borderRadius: 12,
- top: 50,
- left: 60,
- },
- fingerprint: {
- position: "absolute",
- width: 40,
- height: 40,
- backgroundColor: Colors.wosmongton[300],
- borderRadius: 20,
- top: 80,
- left: 80,
- },
- title: {
- fontSize: 24,
- fontWeight: "600",
- color: "white",
- textAlign: "center",
- marginBottom: 12,
- },
- subtitle: {
- fontSize: 16,
- color: Colors.osmoverse[300],
- textAlign: "center",
- marginBottom: 48,
- },
- buttonContainer: {
- width: "100%",
- gap: 12,
- },
- button: {
- width: "100%",
- backgroundColor: Colors.wosmongton[500],
- },
- buttonLabel: {
- fontSize: 16,
- fontWeight: "600",
- },
- pinButton: {
- width: "100%",
- },
- pinButtonLabel: {
- fontSize: 16,
- color: Colors.osmoverse[100],
- },
-});
diff --git a/packages/mobile/components/onboarding/welcome-screen.tsx b/packages/mobile/components/onboarding/welcome-screen.tsx
deleted file mode 100644
index 8d564b92da..0000000000
--- a/packages/mobile/components/onboarding/welcome-screen.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import { StyleSheet, Text, View } from "react-native";
-import Animated, {
- useAnimatedStyle,
- withRepeat,
- withSpring,
-} from "react-native-reanimated";
-import { SafeAreaView } from "react-native-safe-area-context";
-
-import { Colors } from "~/constants/theme-colors";
-
-import { Button } from "../ui/button";
-
-export const WelcomeScreen = ({
- onLinkDesktop,
- onSkip,
-}: {
- onLinkDesktop: () => void;
- onSkip: () => void;
-}) => {
- const tokens = [
- { id: "eth", angle: 0 },
- { id: "usd", angle: 45 },
- { id: "diamond", angle: 90 },
- { id: "globe", angle: 135 },
- { id: "osmo", angle: 180 },
- { id: "btc", angle: 225 },
- { id: "star", angle: 270 },
- { id: "infinity", angle: 315 },
- ];
-
- const floatingStyle = (angle: number) =>
- useAnimatedStyle(() => {
- return {
- transform: [
- {
- translateX: withRepeat(withSpring(Math.cos(angle) * 10), -1, true),
- },
- {
- translateY: withRepeat(withSpring(Math.sin(angle) * 10), -1, true),
- },
- ],
- };
- });
-
- return (
-
-
-
- {tokens.map((token) => (
-
- {/* Replace with actual token icons */}
-
-
- ))}
-
-
-
-
-
- Welcome to the
-
-
- Osmosis
-
-
- Mobile App
-
-
-
-
- Link your desktop wallet, or skip this step and just browse the
- assets. Don't worry, you can always link your wallet at any time.
-
-
-
-
-
-
-
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: Colors.osmoverse[900],
- },
- content: {
- flex: 1,
- alignItems: "center",
- justifyContent: "center",
- paddingHorizontal: 24,
- },
- tokensContainer: {
- width: 200,
- height: 200,
- position: "relative",
- marginBottom: 48,
- },
- token: {
- position: "absolute",
- width: 40,
- height: 40,
- borderRadius: 20,
- },
- tokenCircle: {
- width: 40,
- height: 40,
- borderRadius: 20,
- },
- centerToken: {
- position: "absolute",
- top: "50%",
- left: "50%",
- transform: [{ translateX: -25 }, { translateY: -25 }],
- width: 50,
- height: 50,
- },
- titleContainer: {
- flexDirection: "row",
- gap: 8,
- marginBottom: 24,
- },
- title: {
- fontSize: 28,
- fontWeight: "600",
- color: "white",
- },
- subtitle: {
- fontSize: 16,
- color: Colors.osmoverse[300],
- textAlign: "center",
- marginBottom: 48,
- },
- buttonContainer: {
- width: "100%",
- gap: 12,
- },
- button: {
- width: "100%",
- backgroundColor: Colors.wosmongton[500],
- },
- buttonLabel: {
- fontSize: 16,
- fontWeight: "600",
- },
- skipButton: {
- width: "100%",
- },
- skipButtonLabel: {
- fontSize: 16,
- color: Colors.osmoverse[100],
- },
-});
diff --git a/packages/mobile/components/ui/button.tsx b/packages/mobile/components/ui/button.tsx
index d0b3959c02..841c43a0ab 100644
--- a/packages/mobile/components/ui/button.tsx
+++ b/packages/mobile/components/ui/button.tsx
@@ -12,10 +12,11 @@ import { Colors } from "~/constants/theme-colors";
interface ButtonProps {
title: string;
onPress: () => void;
- variant?: "primary" | "secondary";
+ variant?: "primary" | "secondary" | "outline";
buttonStyle?: ViewStyle;
textStyle?: TextStyle;
disabled?: boolean;
+ icon?: React.ReactNode;
}
export const Button: React.FC = ({
@@ -25,17 +26,30 @@ export const Button: React.FC = ({
buttonStyle,
textStyle,
disabled,
+ icon,
}) => {
+ let variantStyles = {};
+
+ if (variant === "outline") {
+ variantStyles = styles.outline;
+ } else if (variant === "secondary") {
+ variantStyles = styles.secondary;
+ } else {
+ variantStyles = styles.primary;
+ }
+
return (
+ {icon}
{title}
);
@@ -44,7 +58,7 @@ export const Button: React.FC = ({
const styles = StyleSheet.create({
button: {
paddingHorizontal: 16,
- paddingVertical: 4,
+ paddingVertical: 16,
borderRadius: 255,
alignItems: "center",
},
@@ -54,6 +68,14 @@ const styles = StyleSheet.create({
secondary: {
backgroundColor: Colors["osmoverse"][825],
},
+ outline: {
+ backgroundColor: "transparent",
+ borderWidth: 1,
+ borderColor: Colors["osmoverse"][500],
+ },
+ disabled: {
+ opacity: 0.5,
+ },
text: {
color: "#fff",
fontSize: 16,
diff --git a/packages/mobile/hooks/biometrics.ts b/packages/mobile/hooks/biometrics.ts
index b341a18983..d9f66af664 100644
--- a/packages/mobile/hooks/biometrics.ts
+++ b/packages/mobile/hooks/biometrics.ts
@@ -123,21 +123,25 @@ export const useBiometricPrompt = ({
};
export function useOsBiometricAuthEnabled() {
+ const [isCheckingBiometricAvailability, setIsCheckingBiometricAvailability] =
+ useState(false);
const [isBiometricEnabled, setIsBiometricEnabled] = useState(false);
useEffect(() => {
const checkBiometricAvailability = async () => {
+ setIsCheckingBiometricAvailability(true);
const [hasHardware, isEnrolled] = await Promise.all([
hasHardwareAsync(),
isEnrolledAsync(),
]);
setIsBiometricEnabled(hasHardware && isEnrolled);
+ setIsCheckingBiometricAvailability(false);
};
checkBiometricAvailability();
}, []);
- return { isBiometricEnabled };
+ return { isBiometricEnabled, isCheckingBiometricAvailability };
}
export function useDeviceSupportsBiometricAuth(): {
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index d2dbbec5be..bc04383840 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -26,6 +26,7 @@
"@osmosis-labs/unit": "0.10.24-ibc.go.v7.hot.fix",
"@osmosis-labs/utils": "^1.0.0",
"@react-native-async-storage/async-storage": "^2.1.0",
+ "@react-native-masked-view/masked-view": "0.3.2",
"@react-native-menu/menu": "^1.1.6",
"@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0",
@@ -40,12 +41,14 @@
"debounce": "^1.2.1",
"expo": "~52.0.8",
"expo-blur": "~14.0.1",
+ "expo-camera": "~16.0.10",
"expo-clipboard": "^7.0.0",
"expo-constants": "~17.0.3",
"expo-crypto": "~14.0.1",
"expo-dev-client": "~5.0.4",
"expo-font": "~13.0.1",
"expo-haptics": "~14.0.0",
+ "expo-linear-gradient": "~14.0.1",
"expo-linking": "~7.0.3",
"expo-local-authentication": "^15.0.1",
"expo-router": "~4.0.7",
diff --git a/yarn.lock b/yarn.lock
index b13fc4b9f9..bcb9142df5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5483,7 +5483,29 @@
buffer "^6.0.3"
delay "^4.4.0"
-"@keplr-wallet/cosmos@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/cosmos@0.12.12", "@keplr-wallet/cosmos@0.12.28":
+"@keplr-wallet/common@0.12.12":
+ version "0.12.12"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/common/-/common-0.12.12.tgz#55030d985b729eac582c0d7203190e25ea2cb3ec"
+ integrity sha512-AxpwmXdqs083lMvA8j0/V30oTGyobsefNaCou+lP4rCyDdYuXSEux+x2+1AGL9xB3yZfN+4jvEEKJdMwHYEHcQ==
+ dependencies:
+ "@keplr-wallet/crypto" "0.12.12"
+ "@keplr-wallet/types" "0.12.12"
+ buffer "^6.0.3"
+ delay "^4.4.0"
+ mobx "^6.1.7"
+
+"@keplr-wallet/common@0.12.28":
+ version "0.12.28"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/common/-/common-0.12.28.tgz#1d5d985070aced31a34a6426c9ac4b775081acca"
+ integrity sha512-ESQorPZw8PRiUXhsrxED+E1FEWkAdc6Kwi3Az7ce204gMBQDI2j0XJtTd4uCUp+C24Em9fk0samdHzdoB4caIg==
+ dependencies:
+ "@keplr-wallet/crypto" "0.12.28"
+ "@keplr-wallet/types" "0.12.28"
+ buffer "^6.0.3"
+ delay "^4.4.0"
+ mobx "^6.1.7"
+
+"@keplr-wallet/cosmos@0.10.24-ibc.go.v7.hot.fix":
version "0.10.24-ibc.go.v7.hot.fix"
resolved "https://registry.npmjs.org/@keplr-wallet/cosmos/-/cosmos-0.10.24-ibc.go.v7.hot.fix.tgz"
integrity sha512-/A/wHyYo5gQIW5YkAQYZadEv/12EcAuDclO0KboIb9ti4XFJW6S4VY8LnA16R7DZyBx1cnQknyDm101fUrJfJQ==
@@ -5500,6 +5522,40 @@
long "^4.0.0"
protobufjs "^6.11.2"
+"@keplr-wallet/cosmos@0.12.12":
+ version "0.12.12"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/cosmos/-/cosmos-0.12.12.tgz#72c0505d2327bbf2f5cb51502acaf399b88b4ae3"
+ integrity sha512-9TLsefUIAuDqqf1WHBt9Bk29rPlkezmLM8P1eEsXGUaHBfuqUrO+RwL3eLA3HGcgNvdy9s8e0p/4CMInH/LLLQ==
+ dependencies:
+ "@ethersproject/address" "^5.6.0"
+ "@keplr-wallet/common" "0.12.12"
+ "@keplr-wallet/crypto" "0.12.12"
+ "@keplr-wallet/proto-types" "0.12.12"
+ "@keplr-wallet/simple-fetch" "0.12.12"
+ "@keplr-wallet/types" "0.12.12"
+ "@keplr-wallet/unit" "0.12.12"
+ bech32 "^1.1.4"
+ buffer "^6.0.3"
+ long "^4.0.0"
+ protobufjs "^6.11.2"
+
+"@keplr-wallet/cosmos@0.12.28":
+ version "0.12.28"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/cosmos/-/cosmos-0.12.28.tgz#d56e73468256e7276a66bb41f145449dbf11efa1"
+ integrity sha512-IuqmSBgKgIeWBA0XGQKKs28IXFeFMCrfadCbtiZccNc7qnNr5Y/Cyyk01BPC8Dd1ZyEyAByoICgrxvtGN0GGvA==
+ dependencies:
+ "@ethersproject/address" "^5.6.0"
+ "@keplr-wallet/common" "0.12.28"
+ "@keplr-wallet/crypto" "0.12.28"
+ "@keplr-wallet/proto-types" "0.12.28"
+ "@keplr-wallet/simple-fetch" "0.12.28"
+ "@keplr-wallet/types" "0.12.28"
+ "@keplr-wallet/unit" "0.12.28"
+ bech32 "^1.1.4"
+ buffer "^6.0.3"
+ long "^4.0.0"
+ protobufjs "^6.11.2"
+
"@keplr-wallet/crypto@0.10.24-ibc.go.v7.hot.fix":
version "0.10.24-ibc.go.v7.hot.fix"
resolved "https://registry.npmjs.org/@keplr-wallet/crypto/-/crypto-0.10.24-ibc.go.v7.hot.fix.tgz"
@@ -5572,7 +5628,7 @@
resolved "https://registry.npmjs.org/@keplr-wallet/popup/-/popup-0.10.24-ibc.go.v7.hot.fix.tgz"
integrity sha512-Q/teyV6vdmpH3SySGd1xrNc/mVGK/tCP5vFEG2I3Y4FDCSV1yD7vcVgUy+tN19Z8EM3goR57V2QlarSOidtdjQ==
-"@keplr-wallet/proto-types@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/proto-types@0.12.12":
+"@keplr-wallet/proto-types@0.10.24-ibc.go.v7.hot.fix":
version "0.10.24-ibc.go.v7.hot.fix"
resolved "https://registry.npmjs.org/@keplr-wallet/proto-types/-/proto-types-0.10.24-ibc.go.v7.hot.fix.tgz"
integrity sha512-fLUJEtDadYJIMBzhMSZpEDTvXqk8wW68TwnUCRAcAooEQEtXPwY5gfo3hcekQEiCYtIu8XqzJ9fg01rp2Z4d3w==
@@ -5580,6 +5636,22 @@
long "^4.0.0"
protobufjs "^6.11.2"
+"@keplr-wallet/proto-types@0.12.12":
+ version "0.12.12"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/proto-types/-/proto-types-0.12.12.tgz#24e0530af7604a90f33a397a82fe500865c76154"
+ integrity sha512-iAqqNlJpxu/8j+SwOXEH2ymM4W0anfxn+eNeWuqz2c/0JxGTWeLURioxQmCtewtllfHdDHHcoQ7/S+NmXiaEgQ==
+ dependencies:
+ long "^4.0.0"
+ protobufjs "^6.11.2"
+
+"@keplr-wallet/proto-types@0.12.28":
+ version "0.12.28"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/proto-types/-/proto-types-0.12.28.tgz#2fb2c37749ce7db974f01d07387e966c9b99027d"
+ integrity sha512-ukti/eCTltPUP64jxtk5TjtwJogyfKPqlBIT3KGUCGzBLIPeYMsffL5w5aoHsMjINzOITjYqzXyEF8LTIK/fmw==
+ dependencies:
+ long "^4.0.0"
+ protobufjs "^6.11.2"
+
"@keplr-wallet/provider-extension@^0.12.95":
version "0.12.107"
resolved "https://registry.yarnpkg.com/@keplr-wallet/provider-extension/-/provider-extension-0.12.107.tgz#98a0fb42cb0c54d4e681e60e6b1145429a6e3e23"
@@ -5655,12 +5727,32 @@
deepmerge "^4.2.2"
long "^4.0.0"
-"@keplr-wallet/router@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/router@0.12.12", "@keplr-wallet/router@0.12.96":
+"@keplr-wallet/router@0.10.24-ibc.go.v7.hot.fix":
version "0.10.24-ibc.go.v7.hot.fix"
resolved "https://registry.npmjs.org/@keplr-wallet/router/-/router-0.10.24-ibc.go.v7.hot.fix.tgz"
integrity sha512-bt9weexlbhlh8KsOvbDrvHJ8jtUXrXgB2LX+hEAwjclHQt7PMUhx9a5z0Obd19/ive5G/1M7/ccdPIWxRBpKQw==
-"@keplr-wallet/types@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/types@0.12.107", "@keplr-wallet/types@0.12.12", "@keplr-wallet/types@0.12.96", "@keplr-wallet/types@^0.12.95":
+"@keplr-wallet/router@0.12.12":
+ version "0.12.12"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/router/-/router-0.12.12.tgz#92a2c006aec6945ed313575af6b0801f8e84e315"
+ integrity sha512-Aa1TiVRIEPaqs1t27nCNs5Kz6Ty4CLarVdfqcRWlFQL6zFq33GT46s6K9U4Lz2swVCwdmerSXaq308K/GJHTlw==
+
+"@keplr-wallet/router@0.12.96":
+ version "0.12.96"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/router/-/router-0.12.96.tgz#6a20ed2c90ba3ed4f3fc43ed7513f72d7055482d"
+ integrity sha512-O8izj032ZKQIoTus96BFqem+w6NpYHU3j6NEnSaQBh6Zncj9fgjoOVs0CKK+jsuLYUsOHx2t86BxMSKESsR0Ug==
+
+"@keplr-wallet/simple-fetch@0.12.12":
+ version "0.12.12"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.12.tgz#aacc5c3f22b7ab2804b39e864725294a32f858fd"
+ integrity sha512-lCOsaI8upMpbusfwJqEK8VIEX77+QE8+8MJVRqoCYwjOTqKGdUH7D1ieZWh+pzvzOnVgedM3lxqdmCvdgU91qw==
+
+"@keplr-wallet/simple-fetch@0.12.28":
+ version "0.12.28"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.28.tgz#44225df5b329c823076280df1ec9930a21b1373e"
+ integrity sha512-T2CiKS2B5n0ZA7CWw0CA6qIAH0XYI1siE50MP+i+V0ZniCGBeL+BMcDw64vFJUcEH+1L5X4sDAzV37fQxGwllA==
+
+"@keplr-wallet/types@0.10.24-ibc.go.v7.hot.fix":
version "0.10.24-ibc.go.v7.hot.fix"
resolved "https://registry.npmjs.org/@keplr-wallet/types/-/types-0.10.24-ibc.go.v7.hot.fix.tgz"
integrity sha512-3KUjDMUCscYkvKnC+JsJh9+X0NHlsvBgAghP/uy2p5OGtiULqPBAjWiO+hnBbhis3ZEkzGcCROnnBOoccKd3CQ==
@@ -5671,6 +5763,41 @@
long "^4.0.0"
secretjs "^0.17.0"
+"@keplr-wallet/types@0.12.107":
+ version "0.12.107"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.107.tgz#8d6726d86e17a79131b4b6f4f114052d6384aa58"
+ integrity sha512-jBpjJO+nNL8cgsJLjZYoq84n+7nXHDdztTgRMVnnomFb+Vy0FVIEI8VUl89ImmHDUImDd0562ywsvA496/0yCA==
+ dependencies:
+ long "^4.0.0"
+
+"@keplr-wallet/types@0.12.12":
+ version "0.12.12"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.12.tgz#f4bd9e710d5e53504f6b53330abb45bedd9c20ae"
+ integrity sha512-fo6b8j9EXnJukGvZorifJWEm1BPIrvaTLuu5PqaU5k1ANDasm/FL1NaUuaTBVvhRjINtvVXqYpW/rVUinA9MBA==
+ dependencies:
+ long "^4.0.0"
+
+"@keplr-wallet/types@0.12.28":
+ version "0.12.28"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.28.tgz#eac3c2c9d4560856c5c403a87e67925992a04fbf"
+ integrity sha512-EcM9d46hYDm3AO4lf4GUbTSLRySONtTmhKb7p88q56OQOgJN3MMjRacEo2p9jX9gpPe7gRIjMUalhAfUiFpZoQ==
+ dependencies:
+ long "^4.0.0"
+
+"@keplr-wallet/types@0.12.96":
+ version "0.12.96"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.96.tgz#a7735051b1f7cbcdf9b8c29010b1c3c45d195c19"
+ integrity sha512-tr4tPjMrJCsfRXXhhmqnpb9DqH9auJp3uuj8SvDB3pQTTaYJNxkdonLv1tYmXZZ6J9oWtk9WVEDTVgBQN/wisw==
+ dependencies:
+ long "^4.0.0"
+
+"@keplr-wallet/types@^0.12.95":
+ version "0.12.169"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.169.tgz#1c3d0095fec660f6c28e8790b056f1888a6dab6c"
+ integrity sha512-+m/LnQ6sQKqClyaAeWFKdxmGqgbfqlhOv4Nt10domwsf0c5731W8IEQqkaQJXzol9/iyj6Zh50hM1Irar/TY1w==
+ dependencies:
+ long "^4.0.0"
+
"@keplr-wallet/unit@0.10.24-ibc.go.v7.hot.fix":
version "0.10.24-ibc.go.v7.hot.fix"
resolved "https://registry.npmjs.org/@keplr-wallet/unit/-/unit-0.10.24-ibc.go.v7.hot.fix.tgz"
@@ -5680,6 +5807,24 @@
big-integer "^1.6.48"
utility-types "^3.10.0"
+"@keplr-wallet/unit@0.12.12":
+ version "0.12.12"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/unit/-/unit-0.12.12.tgz#2d7f2e38df4e09c8123dcc0784ffc4b5f4166217"
+ integrity sha512-fayJcfXWKUnbDZiRJHyuA9GMVS9DymjRlCzlpAJ0+xV0c4Kun/f+9FajL9OQAdPPhnJ7A3KevMI4VHZsd9Yw+A==
+ dependencies:
+ "@keplr-wallet/types" "0.12.12"
+ big-integer "^1.6.48"
+ utility-types "^3.10.0"
+
+"@keplr-wallet/unit@0.12.28":
+ version "0.12.28"
+ resolved "https://registry.yarnpkg.com/@keplr-wallet/unit/-/unit-0.12.28.tgz#907c7fa0b49a729cda207fca14fc0a38871cc6c4"
+ integrity sha512-kpXigHDBJGOmhtPkv9hqsQid9zkFo7OQPeKgO2n8GUlOINIXW6kWG5LXYTi/Yg9Uiw1CQF69gFMuZCJ8IzVHlA==
+ dependencies:
+ "@keplr-wallet/types" "0.12.28"
+ big-integer "^1.6.48"
+ utility-types "^3.10.0"
+
"@keplr-wallet/wc-client@^0.12.95":
version "0.12.96"
resolved "https://registry.yarnpkg.com/@keplr-wallet/wc-client/-/wc-client-0.12.96.tgz#a56995172dcdc73d32b24d5a704a954062befc2a"
@@ -7651,6 +7796,11 @@
dependencies:
merge-options "^3.0.4"
+"@react-native-masked-view/masked-view@0.3.2":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/@react-native-masked-view/masked-view/-/masked-view-0.3.2.tgz#7064533a573e3539ec912f59c1f457371bf49dd9"
+ integrity sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ==
+
"@react-native-menu/menu@^1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@react-native-menu/menu/-/menu-1.1.6.tgz#df6b4bf46a8ac5718605203f7fcd6fd3684715f5"
@@ -14561,6 +14711,13 @@ expo-blur@~14.0.1:
resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-14.0.1.tgz#4bdbec43b5015891990e78d31d8ff2f1f5d27464"
integrity sha512-3Q6jFBLbY8n2vwk28ycUC+eIlVhnlqwkXUKk/Lfaj+SGV3AZMQyrixe7OYwJdUfwqETBrnYYMB6uNrJzOSbG+g==
+expo-camera@~16.0.10:
+ version "16.0.10"
+ resolved "https://registry.yarnpkg.com/expo-camera/-/expo-camera-16.0.10.tgz#d3ee17f18984d33425046c83a78e06a533654ddb"
+ integrity sha512-CKvC9dhBuR7VkuwXsBya+Ok2h97z9t7EefkP5sZNdPXZYab8K/zxcisGJZG6/mjCVqzHIftvCb3GxJtuRrDIBQ==
+ dependencies:
+ invariant "^2.2.4"
+
expo-clipboard@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/expo-clipboard/-/expo-clipboard-7.0.0.tgz#066b1a781fdaf05e30f282522d3a58f2e651e4cf"
@@ -14643,6 +14800,11 @@ expo-keep-awake@~14.0.1:
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-14.0.1.tgz#77c38feefa95c494aa167e6df5a6eacd17af2358"
integrity sha512-c5mGCAIk2YM+Vsdy90BlEJ4ZX+KG5Au9EkJUIxXWlpnuKmDAJ3N+5nEZ7EUO1ZTheqoSBeAo4jJ8rTWPU+JXdw==
+expo-linear-gradient@~14.0.1:
+ version "14.0.1"
+ resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-14.0.1.tgz#f93c518014b6eb5ee17ed6562db716640ad27f3e"
+ integrity sha512-apGtUO9AZ52ZWvX9f6K9TamWw8XcUby7jZ0Pcvd5LxUO7pl7tDPx2VlKqpzbhhS4yfCiUwX58wqocwVnE/0ZVg==
+
expo-linking@~7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-7.0.3.tgz#307851288ec65e1c533bcc70b57dfb6372f9679e"