Skip to content

Commit

Permalink
Merge branch 'master' into feat/gestures
Browse files Browse the repository at this point in the history
  • Loading branch information
reneaaron authored Sep 25, 2024
2 parents 83ae1e8 + 7936aac commit 7028ce0
Show file tree
Hide file tree
Showing 21 changed files with 508 additions and 46 deletions.
24 changes: 19 additions & 5 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
"expo": {
"name": "Alby Go",
"slug": "alby-mobile",
"version": "1.4.2",
"scheme": ["bitcoin", "lightning", "alby"],
"version": "1.5.0",
"scheme": [
"bitcoin",
"lightning",
"alby"
],
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
Expand All @@ -12,8 +16,16 @@
"resizeMode": "cover",
"backgroundColor": "#0F0C40"
},
"assetBundlePatterns": ["**/*"],
"assetBundlePatterns": [
"**/*"
],
"plugins": [
[
"expo-local-authentication",
{
"faceIDPermission": "Allow Alby Go to use Face ID."
}
],
[
"expo-camera",
{
Expand Down Expand Up @@ -49,7 +61,9 @@
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundImage": "./assets/adaptive-icon-bg.png"
},
"permissions": ["android.permission.CAMERA"]
"permissions": [
"android.permission.CAMERA"
]
},
"extra": {
"eas": {
Expand All @@ -58,4 +72,4 @@
},
"owner": "roland_alby"
}
}
}
29 changes: 25 additions & 4 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ import { toastConfig } from "~/components/ToastConfig";
import * as Font from "expo-font";
import { useInfo } from "~/hooks/useInfo";
import { secureStorage } from "~/lib/secureStorage";
import { hasOnboardedKey } from "~/lib/state/appStore";
import { hasOnboardedKey, useAppStore } from "~/lib/state/appStore";
import { usePathname } from "expo-router";
import { UserInactivityProvider } from "~/context/UserInactivity";
import { PortalHost } from '@rn-primitives/portal';
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { isBiometricSupported } from "~/lib/isBiometricSupported";

const LIGHT_THEME: Theme = {
dark: false,
Expand All @@ -50,6 +53,8 @@ export default function RootLayout() {
const { isDarkColorScheme } = useColorScheme();
const [fontsLoaded, setFontsLoaded] = React.useState(false);
const [checkedOnboarding, setCheckedOnboarding] = React.useState(false);
const isUnlocked = useAppStore((store) => store.unlocked);
const pathname = usePathname();
useConnectionChecker();

const rootNavigationState = useRootNavigationState();
Expand All @@ -65,7 +70,6 @@ export default function RootLayout() {
};

async function loadFonts() {

await Font.loadAsync({
OpenRunde: require("./../assets/fonts/OpenRunde-Regular.otf"),
"OpenRunde-Medium": require("./../assets/fonts/OpenRunde-Medium.otf"),
Expand All @@ -76,12 +80,20 @@ export default function RootLayout() {
setFontsLoaded(true);
}

async function checkBiometricStatus() {
const isSupported = await isBiometricSupported()
if (!isSupported) {
useAppStore.getState().setSecurityEnabled(false);
}
}

React.useEffect(() => {
const init = async () => {
try {
await Promise.all([
checkOnboardingStatus(),
loadFonts(),
checkBiometricStatus(),
]);
}
finally {
Expand All @@ -90,9 +102,16 @@ export default function RootLayout() {
};

init();

}, [hasNavigationState]);

React.useEffect(() => {
if (hasNavigationState && !isUnlocked) {
if (pathname !== "/unlock") {
router.push("/unlock");
}
}
}, [isUnlocked, hasNavigationState]);

if (!fontsLoaded || !checkedOnboarding) {
return null;
}
Expand All @@ -104,7 +123,9 @@ export default function RootLayout() {
<PolyfillCrypto />
<SafeAreaView className="w-full h-full bg-background">
<GestureHandlerRootView>
<Stack />
<UserInactivityProvider>
<Stack />
</UserInactivityProvider>
<Toast config={toastConfig} position="bottom" bottomOffset={140} topOffset={140} />
<PortalHost />
</GestureHandlerRootView>
Expand Down
5 changes: 5 additions & 0 deletions app/settings/security.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Security } from "../../pages/settings/Security";

export default function Page() {
return <Security />;
}
5 changes: 5 additions & 0 deletions app/unlock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Unlock } from "../pages/Unlock";

export default function Page() {
return <Unlock />;
}
3 changes: 3 additions & 0 deletions components/DualCurrencyInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ type DualCurrencyInputProps = {
amount: string;
setAmount(amount: string): void;
autoFocus?: boolean;
readOnly?: boolean;
};

export function DualCurrencyInput({
amount,
setAmount,
autoFocus = false,
readOnly = false,
}: DualCurrencyInputProps) {
const getFiatAmount = useGetFiatAmount();
const getSatsAmount = useGetSatsAmount();
Expand Down Expand Up @@ -58,6 +60,7 @@ export function DualCurrencyInput({
style={styles.amountInput}
autoFocus={autoFocus}
returnKeyType="done"
readOnly={readOnly}
// aria-errormessage="inputError"
/>
<Pressable onPress={toggleInputMode}>
Expand Down
6 changes: 6 additions & 0 deletions components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ import {
CameraOff,
Palette,
Egg,
Fingerprint,
HelpCircle,
CircleCheck,
TriangleAlert,
} from "lucide-react-native";
import { cssInterop } from "nativewind";

Expand Down Expand Up @@ -85,8 +87,10 @@ interopIcon(Power);
interopIcon(CameraOff);
interopIcon(Palette);
interopIcon(Egg);
interopIcon(Fingerprint);
interopIcon(HelpCircle);
interopIcon(CircleCheck);
interopIcon(TriangleAlert);

export {
AlertCircle,
Expand Down Expand Up @@ -123,6 +127,8 @@ export {
Power,
Palette,
Egg,
Fingerprint,
HelpCircle,
CircleCheck,
TriangleAlert,
};
96 changes: 96 additions & 0 deletions components/ui/switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as SwitchPrimitives from '@rn-primitives/switch';
import * as React from 'react';
import { Platform } from 'react-native';
import Animated, {
interpolateColor,
useAnimatedStyle,
useDerivedValue,
withTiming,
} from 'react-native-reanimated';
import { useColorScheme } from '~/lib/useColorScheme';
import { cn } from '~/lib/utils';

const SwitchWeb = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer flex-row h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed',
props.checked ? 'bg-amber-300' : 'bg-input',
props.disabled && 'opacity-50',
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-md shadow-foreground/5 ring-0 transition-transform',
props.checked ? 'translate-x-5' : 'translate-x-0'
)}
/>
</SwitchPrimitives.Root>
));

SwitchWeb.displayName = 'SwitchWeb';

const RGB_COLORS = {
light: {
primary: 'rgb(255, 224, 112)',
input: 'rgb(228, 228, 231)',
},
dark: {
primary: 'rgb(255, 224, 112)',
input: 'rgb(228, 228, 231)',
},
} as const;

const SwitchNative = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => {
const { colorScheme } = useColorScheme();
const translateX = useDerivedValue(() => (props.checked ? 18 : 0));
const animatedRootStyle = useAnimatedStyle(() => {
return {
backgroundColor: interpolateColor(
Number(props.checked),
[0, 1],
[RGB_COLORS[colorScheme].input, RGB_COLORS[colorScheme].primary]
),
};
});
const animatedThumbStyle = useAnimatedStyle(() => ({
transform: [{ translateX: withTiming(translateX.value, { duration: 200 }) }],
}));
return (
<Animated.View
style={animatedRootStyle}
className={cn('h-8 w-[46px] rounded-full', props.disabled && 'opacity-50')}
>
<SwitchPrimitives.Root
className={cn(
'flex-row h-8 w-[46px] shrink-0 items-center rounded-full border-2 border-transparent',
className
)}
{...props}
ref={ref}
>
<Animated.View style={animatedThumbStyle}>
<SwitchPrimitives.Thumb
className={'h-7 w-7 rounded-full bg-background shadow-md shadow-foreground/25 ring-0'}
/>
</Animated.View>
</SwitchPrimitives.Root>
</Animated.View>
);
});
SwitchNative.displayName = 'SwitchNative';

const Switch = Platform.select({
web: SwitchWeb,
default: SwitchNative,
});

export { Switch };
42 changes: 42 additions & 0 deletions context/UserInactivity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as React from "react";
import { AppState, AppStateStatus, NativeEventSubscription } from 'react-native';
import { secureStorage } from "~/lib/secureStorage";
import { INACTIVITY_THRESHOLD } from "~/lib/constants";
import { lastActiveTimeKey, useAppStore } from "~/lib/state/appStore";

export const UserInactivityProvider = ({ children }: any) => {
const [appState, setAppState] = React.useState<AppStateStatus>(AppState.currentState);
const isSecurityEnabled = useAppStore((store) => store.isSecurityEnabled);

const handleAppStateChange = async (nextState: AppStateStatus) => {
if (appState === "active" && nextState.match(/inactive|background/)) {
const now = Date.now();
secureStorage.setItem(lastActiveTimeKey, now.toString());
} else if (appState.match(/inactive|background/) && nextState === "active") {
const lastActiveTime = secureStorage.getItem(lastActiveTimeKey);
if (lastActiveTime) {
const timeElapsed = Date.now() - parseInt(lastActiveTime, 10);
if (timeElapsed >= INACTIVITY_THRESHOLD) {
useAppStore.getState().setUnlocked(false)
}
}
await secureStorage.removeItem(lastActiveTimeKey);
}
setAppState(nextState);
};

React.useEffect(() => {
let subscription: NativeEventSubscription
if (isSecurityEnabled) {
subscription = AppState.addEventListener("change", handleAppStateChange);
}

return () => {
if (subscription) {
subscription.remove();
}
};
}, [appState, isSecurityEnabled]);

return children;
}
2 changes: 2 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const NAV_THEME = {
},
};

export const INACTIVITY_THRESHOLD = 5 * 60 * 1000;

export const CURSOR_COLOR = "hsl(47 100% 72%)";

export const TRANSACTIONS_PAGE_SIZE = 20;
Expand Down
7 changes: 7 additions & 0 deletions lib/isBiometricSupported.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as LocalAuthentication from "expo-local-authentication";

export async function isBiometricSupported() {
const compatible = await LocalAuthentication.hasHardwareAsync();
const securityLevel = await LocalAuthentication.getEnrolledLevelAsync();
return compatible && securityLevel > 0
}
Loading

0 comments on commit 7028ce0

Please sign in to comment.