Skip to content

Commit 35e66d3

Browse files
authored
Add PIN lock screen (#117)
1 parent a38dc9f commit 35e66d3

File tree

5 files changed

+296
-92
lines changed

5 files changed

+296
-92
lines changed

packages/mobile-app/app/_layout.tsx

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ApplicationProvider, IconRegistry } from "@ui-kitten/components";
2121
import { EvaIconsPack } from "@ui-kitten/eva-icons";
2222
import { menuItems } from "./menu";
2323
import { accountSettingsRoutes } from "./account-settings";
24+
import { PinLockScreen } from "@/components/PinLockScreen/PinLockScreen";
2425

2526
const queryClient = new QueryClient({
2627
defaultOptions: {
@@ -82,45 +83,47 @@ export default function Layout() {
8283
<FacadeProvider>
8384
<DatabaseLoader>
8485
<AccountProvider>
85-
<Stack>
86-
<Stack.Screen
87-
name="onboarding"
88-
options={{
89-
headerShown: false,
90-
}}
91-
/>
92-
<Stack.Screen
93-
name="(tabs)"
94-
options={{
95-
headerShown: false,
96-
title: "Account",
97-
}}
98-
/>
99-
<Stack.Screen
100-
name="menu/index"
101-
options={{
102-
title: "Menu",
103-
}}
104-
/>
105-
{menuItems.map((item) => {
106-
return (
107-
<Stack.Screen
108-
key={item.title}
109-
name={item.path}
110-
options={{ title: item.title }}
111-
/>
112-
);
113-
})}
114-
{accountSettingsRoutes.map((item) => {
115-
return (
116-
<Stack.Screen
117-
key={item.title}
118-
name={item.path}
119-
options={{ title: item.title }}
120-
/>
121-
);
122-
})}
123-
</Stack>
86+
<PinLockScreen>
87+
<Stack>
88+
<Stack.Screen
89+
name="onboarding"
90+
options={{
91+
headerShown: false,
92+
}}
93+
/>
94+
<Stack.Screen
95+
name="(tabs)"
96+
options={{
97+
headerShown: false,
98+
title: "Account",
99+
}}
100+
/>
101+
<Stack.Screen
102+
name="menu/index"
103+
options={{
104+
title: "Menu",
105+
}}
106+
/>
107+
{menuItems.map((item) => {
108+
return (
109+
<Stack.Screen
110+
key={item.title}
111+
name={item.path}
112+
options={{ title: item.title }}
113+
/>
114+
);
115+
})}
116+
{accountSettingsRoutes.map((item) => {
117+
return (
118+
<Stack.Screen
119+
key={item.title}
120+
name={item.path}
121+
options={{ title: item.title }}
122+
/>
123+
);
124+
})}
125+
</Stack>
126+
</PinLockScreen>
124127
</AccountProvider>
125128
</DatabaseLoader>
126129
</FacadeProvider>

packages/mobile-app/app/onboarding/create-pin.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ export default function CreatePin() {
218218
</View>
219219
) : (
220220
<PinInputComponent
221-
pinLength={MAX_PIN_LENGTH}
221+
pinLength={step === "pin" ? MAX_PIN_LENGTH : pinValue.length}
222222
onPinChange={(value) => handlePinChange(value, step === "confirm")}
223223
error={error}
224224
setError={setError}
Lines changed: 116 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,15 @@
1-
import { View, Text, TextInput, StyleSheet, Platform } from "react-native";
1+
import { View, Text, StyleSheet } from "react-native";
2+
import { Button, Layout } from "@ui-kitten/components";
23

34
interface PinInputComponentProps {
45
pinLength: number;
56
onPinChange: (pin: string) => void;
67
error?: string | null;
7-
setError: (error: string | null) => void;
8+
setError?: (error: string | null) => void;
89
promptText: string;
910
value: string;
1011
}
1112

12-
const styles = StyleSheet.create({
13-
container: {
14-
alignItems: "center",
15-
gap: 16,
16-
},
17-
promptText: {
18-
fontSize: 16,
19-
color: "#666666",
20-
textAlign: "center",
21-
paddingHorizontal: 16,
22-
},
23-
pinInput: {
24-
width: "80%",
25-
height: 50,
26-
fontSize: 24,
27-
textAlign: "center",
28-
letterSpacing: 16,
29-
backgroundColor: "#F5F5F5",
30-
borderRadius: 8,
31-
...Platform.select({
32-
ios: {
33-
padding: 12,
34-
},
35-
android: {
36-
padding: 8,
37-
},
38-
}),
39-
},
40-
errorText: {
41-
fontSize: 14,
42-
color: "#FF3B30",
43-
textAlign: "center",
44-
},
45-
});
46-
4713
export function PinInputComponent({
4814
pinLength,
4915
onPinChange,
@@ -52,26 +18,125 @@ export function PinInputComponent({
5218
promptText,
5319
value,
5420
}: PinInputComponentProps) {
55-
const handlePinChange = (value: string) => {
56-
// Only allow numbers and limit to pinLength
57-
const numericValue = value.replace(/[^0-9]/g, "").slice(0, pinLength);
58-
setError(null);
59-
onPinChange(numericValue);
21+
const handleNumberPress = (num: string) => {
22+
if (value.length < pinLength) {
23+
const newValue = value + num;
24+
setError?.(null);
25+
onPinChange(newValue);
26+
}
27+
};
28+
29+
const handleDelete = () => {
30+
const newValue = value.slice(0, -1);
31+
setError?.(null);
32+
onPinChange(newValue);
6033
};
6134

35+
const renderNumberButton = (num: string) => (
36+
<Button
37+
appearance="outline"
38+
style={styles.numberButton}
39+
onPress={() => handleNumberPress(num)}
40+
>
41+
{num}
42+
</Button>
43+
);
44+
6245
return (
6346
<View style={styles.container}>
6447
<Text style={styles.promptText}>{promptText}</Text>
65-
<TextInput
66-
style={styles.pinInput}
67-
value={value}
68-
onChangeText={handlePinChange}
69-
keyboardType="number-pad"
70-
maxLength={pinLength}
71-
secureTextEntry={true}
72-
autoFocus={true}
73-
/>
48+
49+
<View style={styles.dotsContainer}>
50+
{Array.from({ length: pinLength }).map((_, i) => (
51+
<View
52+
key={i}
53+
style={[
54+
styles.dot,
55+
i < value.length ? styles.dotFilled : styles.dotEmpty,
56+
]}
57+
/>
58+
))}
59+
</View>
60+
61+
<Layout style={styles.keypadContainer}>
62+
<Layout style={styles.keypadRow}>
63+
{renderNumberButton("1")}
64+
{renderNumberButton("2")}
65+
{renderNumberButton("3")}
66+
</Layout>
67+
<Layout style={styles.keypadRow}>
68+
{renderNumberButton("4")}
69+
{renderNumberButton("5")}
70+
{renderNumberButton("6")}
71+
</Layout>
72+
<Layout style={styles.keypadRow}>
73+
{renderNumberButton("7")}
74+
{renderNumberButton("8")}
75+
{renderNumberButton("9")}
76+
</Layout>
77+
<Layout style={styles.keypadRow}>
78+
<View style={styles.numberButton} />
79+
{renderNumberButton("0")}
80+
<Button
81+
appearance="ghost"
82+
style={styles.numberButton}
83+
onPress={handleDelete}
84+
>
85+
86+
</Button>
87+
</Layout>
88+
</Layout>
7489
{error && <Text style={styles.errorText}>{error}</Text>}
7590
</View>
7691
);
7792
}
93+
94+
const styles = StyleSheet.create({
95+
container: {
96+
alignItems: "center",
97+
width: "100%",
98+
padding: 16,
99+
},
100+
promptText: {
101+
fontSize: 18,
102+
marginBottom: 24,
103+
textAlign: "center",
104+
},
105+
dotsContainer: {
106+
flexDirection: "row",
107+
justifyContent: "center",
108+
marginBottom: 32,
109+
gap: 16,
110+
},
111+
dot: {
112+
width: 16,
113+
height: 16,
114+
borderRadius: 8,
115+
borderWidth: 1,
116+
borderColor: "#000",
117+
},
118+
dotEmpty: {
119+
backgroundColor: "transparent",
120+
},
121+
dotFilled: {
122+
backgroundColor: "#000",
123+
},
124+
keypadContainer: {
125+
gap: 16,
126+
alignItems: "center",
127+
},
128+
keypadRow: {
129+
flexDirection: "row",
130+
gap: 16,
131+
},
132+
numberButton: {
133+
width: 72,
134+
height: 72,
135+
borderRadius: 36,
136+
},
137+
errorText: {
138+
color: "red",
139+
marginTop: 16,
140+
textAlign: "center",
141+
},
142+
});

0 commit comments

Comments
 (0)