Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile create pin #94

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
12 changes: 12 additions & 0 deletions packages/mobile-app/app/onboarding/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export default function OnboardingLayout() {
title: "Name your account",
}}
/>
<Stack.Screen
name="create-pin"
options={{
title: "Create Your PIN",
}}
/>
<Stack.Screen
name="confirm-pin"
options={{
title: "Confirm Your PIN",
}}
/>
</Stack>
);
}
58 changes: 58 additions & 0 deletions packages/mobile-app/app/onboarding/confirm-pin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { StyleSheet, KeyboardAvoidingView, Platform, View } from "react-native";
import { useHeaderHeight } from "@react-navigation/elements";
import { Button } from "@ironfish/tackle-box";
import { router, useLocalSearchParams } from "expo-router";
import { usePin } from "../../hooks/usePin";
import { PinInputComponent } from "@/components/PinInputComponent";

const styles = StyleSheet.create({
keyboardAvoidingView: {
flex: 1,
backgroundColor: "white",
},
innerContainer: {
flex: 1,
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 32,
},
});

const confirmPinText = "Retype your 4-8 digit PIN to confirm and move forward.";

export default function ConfirmPin() {
const { createPinValue } = useLocalSearchParams<{ createPinValue: string }>();
const { pinValue, setPinValue, error, setError, isPinValid, MAX_PIN_LENGTH } =
usePin();
const headerHeight = useHeaderHeight();

return (
<KeyboardAvoidingView
style={styles.keyboardAvoidingView}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={headerHeight}
>
<View style={styles.innerContainer}>
<PinInputComponent
pinLength={MAX_PIN_LENGTH}
onPinChange={setPinValue}
error={error}
setError={setError}
promptText={confirmPinText}
/>
<Button
disabled={!isPinValid}
title="Continue"
onClick={() => {
if (createPinValue !== pinValue) {
setError("PINs do not match");
} else {
router.push("/onboarding/name-account");
}
}}
/>
</View>
</KeyboardAvoidingView>
);
}
58 changes: 58 additions & 0 deletions packages/mobile-app/app/onboarding/create-pin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { StyleSheet, KeyboardAvoidingView, Platform, View } from "react-native";
import { useHeaderHeight } from "@react-navigation/elements";
import { Button } from "@ironfish/tackle-box";
import { useRouter } from "expo-router";
import { usePin } from "../../hooks/usePin";
import { PinInputComponent } from "@/components/PinInputComponent";

const styles = StyleSheet.create({
keyboardAvoidingView: {
flex: 1,
backgroundColor: "white",
},
innerContainer: {
flex: 1,
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 32,
},
});

const createPinText =
"Set a 4-8 digit PIN to prevent others from accessing your Iron Fish account.";

export default function CreatePin() {
const { pinValue, setPinValue, isPinValid, error, setError, MAX_PIN_LENGTH } =
usePin();
const headerHeight = useHeaderHeight();
const router = useRouter();

return (
<KeyboardAvoidingView
style={styles.keyboardAvoidingView}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={headerHeight}
>
<View style={styles.innerContainer}>
<PinInputComponent
pinLength={MAX_PIN_LENGTH}
onPinChange={setPinValue}
error={error}
setError={setError}
promptText={createPinText}
/>
<Button
disabled={!isPinValid}
title="Continue"
onClick={() =>
router.push({
pathname: "/onboarding/confirm-pin",
params: { createPinValue: pinValue },
})
}
/>
</View>
</KeyboardAvoidingView>
);
}
6 changes: 5 additions & 1 deletion packages/mobile-app/app/onboarding/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ export default function OnboardingCreate() {
<Icon name="chevron-right" />
</HStack>
</LinkButton>
<LinkButton borderRadius={1} variant="ghost" href="/onboarding/pin">
<LinkButton
borderRadius={1}
variant="ghost"
href="/onboarding/create-pin"
>
<HStack justifyContent="space-between" alignItems="center" gap={8}>
<HStack alignItems="center" gap={4}>
<Icon name="number-pad-orchid" />
Expand Down
9 changes: 9 additions & 0 deletions packages/mobile-app/app/onboarding/name-account.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { View, Text } from "react-native";

export default function NameAccountScreen() {
return (
<View>
<Text>Name Account</Text>
</View>
);
}
45 changes: 45 additions & 0 deletions packages/mobile-app/components/PinInputComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { useState } from "react";
import { PinInput, Text, VStack } from "@ironfish/tackle-box";

interface PinInputComponentProps {
pinLength: number;
onPinChange: (pin: string) => void;
error?: string | null;
setError: (error: string | null) => void;
promptText: string;
}

export function PinInputComponent({
pinLength,
onPinChange,
error,
setError,
promptText,
}: PinInputComponentProps) {
const [pinValue, setPinValue] = useState("");

const handlePinChange = (value: string) => {
setError(null);
setPinValue(value);
onPinChange(value);
};

return (
<VStack gap={8}>
<Text size="md" color="textSecondary" textAlign="center">
{promptText}
</Text>
<PinInput
pinValue={pinValue}
onChange={handlePinChange}
pinLength={pinLength}
aria-label="Security PIN input"
/>
{error && (
<Text size="sm" color="textError" textAlign="center">
{error}
</Text>
)}
</VStack>
);
}
30 changes: 30 additions & 0 deletions packages/mobile-app/hooks/usePin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useState } from "react";

const MIN_PIN_LENGTH = 4;
const MAX_PIN_LENGTH = 8;

export function usePin() {
const [pinValue, setPinValue] = useState("");
const [error, setError] = useState<string | null>(null);

// Check if the pin value is valid length and only contains numbers
const isPinValid =
pinValue.length >= MIN_PIN_LENGTH &&
pinValue.length <= MAX_PIN_LENGTH &&
/^[0-9]*$/.test(pinValue);

const handlePinChange = (value: string) => {
setError(null);
setPinValue(value);
};

return {
pinValue,
setPinValue: handlePinChange,
error,
setError,
isPinValid,
MIN_PIN_LENGTH,
MAX_PIN_LENGTH,
};
}
6 changes: 5 additions & 1 deletion packages/tackle-box/lib/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import ArrowReceive from "./svg/arrow-receive.svg?react";
import ArrowSend from "./svg/arrow-send.svg?react";
import ArrowsBridge from "./svg/arrows-bridge.svg?react";
import ChevronRight from "./svg/chevron-right.svg?react";
import Eye from "./svg/eye.svg?react";
import EyeSlash from "./svg/eye-slash.svg?react";
import Gear from "./svg/gear.svg?react";
import HamburgerMenu from "./svg/hamburger-menu.svg?react";
import FaceId from "./svg/face-id.svg?react";
import ChevronRight from "./svg/chevron-right.svg?react";
import NumberPadOrchid from "./svg/number-pad--orchid.svg?react";

const ICONS = {
Expand All @@ -16,6 +18,8 @@ const ICONS = {
gear: Gear,
"hamburger-menu": HamburgerMenu,
"number-pad-orchid": NumberPadOrchid,
eye: Eye,
"eye-slash": EyeSlash,
} as const;

export type IconName = keyof typeof ICONS;
Expand Down
3 changes: 3 additions & 0 deletions packages/tackle-box/lib/components/Icon/svg/eye-slash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/tackle-box/lib/components/Icon/svg/eye.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading