Skip to content

Commit e6158e0

Browse files
authored
feat: Add MFA (#962)
* feat: add mfa * chore: add i18n * chore: minor fixes * chore: android/ios v4.2.1 * chore: change chatwoot min version * chore: add the support for hexa decimal backup code
1 parent b8b43f8 commit e6158e0

File tree

21 files changed

+608
-13
lines changed

21 files changed

+608
-13
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
EXPO_PUBLIC_CHATWOOT_WEBSITE_TOKEN=
22
EXPO_PUBLIC_CHATWOOT_BASE_URL=https://app.chatwoot.com
33
EXPO_PUBLIC_JUNE_SDK_KEY=
4-
EXPO_PUBLIC_MINIMUM_CHATWOOT_VERSION=3.13.0
4+
EXPO_PUBLIC_MINIMUM_CHATWOOT_VERSION=4.1.0
55
EXPO_PUBLIC_SENTRY_DSN=
66
EXPO_PUBLIC_PROJECT_ID=
77
EXPO_PUBLIC_APP_SLUG=

app.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default ({ config }: ConfigContext): ExpoConfig => {
44
return {
55
name: 'Chatwoot',
66
slug: process.env.EXPO_PUBLIC_APP_SLUG || 'chatwoot-mobile',
7-
version: '4.2.0',
7+
version: '4.2.3',
88
orientation: 'portrait',
99
icon: './assets/icon.png',
1010
userInterfaceStyle: 'light',

jest.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ module.exports = {
44
moduleNameMapper: {
55
'^@/(.*)$': '<rootDir>/src/$1',
66
},
7+
transformIgnorePatterns: [
8+
'node_modules/(?!(jest-)?@?react-native|@react-native-community|@react-navigation|@reduxjs|immer)',
9+
],
710
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@chatwoot/mobile-app",
3-
"version": "4.2.0",
3+
"version": "4.2.3",
44
"scripts": {
55
"start": "expo start --dev-client",
66
"start:production": "expo start --no-dev --minify",

src/components-next/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './sheet-components';
66
export * from './spinner';
77
export * from './action-tabs';
88
export * from './no-network';
9+
export * from './verification-code';
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { StyleSheet } from 'react-native';
2+
import type { SharedValue } from 'react-native-reanimated';
3+
import Animated, {
4+
Easing,
5+
FadeIn,
6+
FadeOut,
7+
FlipInXDown,
8+
FlipOutXDown,
9+
useAnimatedStyle,
10+
withTiming,
11+
} from 'react-native-reanimated';
12+
13+
export type StatusType = 'inProgress' | 'correct' | 'wrong';
14+
15+
export type AnimatedCodeNumberProps = {
16+
code?: string;
17+
highlighted: boolean;
18+
status: SharedValue<StatusType>;
19+
};
20+
21+
export const AnimatedCodeNumber: React.FC<AnimatedCodeNumberProps> = ({
22+
code,
23+
highlighted,
24+
status,
25+
}) => {
26+
const correctColor = 'hsl(151, 40.2%, 54.1%)'; // green-600
27+
const defaultColor = 'hsl(0, 0%, 89.5%)'; // gray-300
28+
29+
const rBoxStyle = useAnimatedStyle(() => {
30+
return {
31+
// Only show green border for correct status, default border for all other states
32+
borderColor: withTiming(status.value === 'correct' ? correctColor : defaultColor),
33+
};
34+
}, [correctColor, defaultColor]);
35+
36+
return (
37+
<Animated.View style={[styles.container, rBoxStyle]}>
38+
{code != null && (
39+
<Animated.View entering={FadeIn.duration(250)} exiting={FadeOut.duration(250)}>
40+
<Animated.Text
41+
entering={FlipInXDown.duration(500)
42+
// Go to this website and you'll see the curve I used:
43+
// https://cubic-bezier.com/#0,0.75,0.5,0.9
44+
// Basically, I want the animation to start slow, then accelerate at the end
45+
// Do we really need to use a curve? Every detail matters :)
46+
.easing(Easing.bezier(0, 0.75, 0.5, 0.9).factory())
47+
.build()}
48+
exiting={FlipOutXDown.duration(500)
49+
// https://cubic-bezier.com/#0.6,0.1,0.4,0.8
50+
// I want the animation to start fast, then decelerate at the end (opposite of the previous one)
51+
.easing(Easing.bezier(0.6, 0.1, 0.4, 0.8).factory())
52+
.build()}
53+
style={[styles.text, { color: 'hsl(0, 0%, 39.3%)' }]}>
54+
{code}
55+
</Animated.Text>
56+
</Animated.View>
57+
)}
58+
</Animated.View>
59+
);
60+
};
61+
62+
const styles = StyleSheet.create({
63+
container: {
64+
height: '90%',
65+
width: '80%',
66+
borderWidth: 2,
67+
borderRadius: 12,
68+
borderCurve: 'continuous',
69+
justifyContent: 'center',
70+
alignItems: 'center',
71+
},
72+
text: {
73+
fontSize: 40,
74+
fontFamily: 'Inter-500-24',
75+
},
76+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useCallback } from 'react';
2+
import {
3+
cancelAnimation,
4+
Easing,
5+
useAnimatedStyle,
6+
useSharedValue,
7+
withRepeat,
8+
withTiming,
9+
} from 'react-native-reanimated';
10+
11+
// Custom hook for creating an animated shaking effect
12+
const useAnimatedShake = () => {
13+
// Shared value to track the horizontal translation for shaking
14+
const shakeTranslateX = useSharedValue(0);
15+
16+
// Callback function to trigger the shake animation
17+
const shake = useCallback(() => {
18+
// Cancel any ongoing animations on shakeTranslateX
19+
cancelAnimation(shakeTranslateX);
20+
21+
// Reset the translation value to 0 before starting the new animation
22+
shakeTranslateX.value = 0;
23+
24+
// Apply a repeating animation to create a shaking effect
25+
shakeTranslateX.value = withRepeat(
26+
withTiming(10, {
27+
duration: 120,
28+
easing: Easing.bezier(0.35, 0.7, 0.5, 0.7),
29+
}),
30+
6, // Repeat the animation 6 times
31+
true, // Infinite loop (true indicates indefinite repetition)
32+
);
33+
}, [shakeTranslateX]); // Dependency array to ensure proper re-rendering
34+
35+
// Define the animated style based on the current translation value
36+
const rShakeStyle = useAnimatedStyle(() => {
37+
return {
38+
transform: [{ translateX: shakeTranslateX.value }],
39+
};
40+
}, []);
41+
42+
// Return the shake callback function and the animated style for external use
43+
return { shake, rShakeStyle };
44+
};
45+
46+
// Export the custom hook for use in other components
47+
export { useAnimatedShake };
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { StyleSheet, View } from 'react-native';
2+
import { useSharedValue } from 'react-native-reanimated';
3+
4+
import type { AnimatedCodeNumberProps } from './animated-code-number';
5+
import { AnimatedCodeNumber } from './animated-code-number';
6+
7+
export type StatusType = 'inProgress' | 'correct' | 'wrong';
8+
9+
type VerificationCodeProps = {
10+
code: string[];
11+
maxLength?: number;
12+
isCodeWrong: boolean;
13+
} & Pick<AnimatedCodeNumberProps, 'status'>;
14+
15+
export const VerificationCode: React.FC<VerificationCodeProps> = ({
16+
code,
17+
maxLength = 5,
18+
status,
19+
isCodeWrong,
20+
}) => {
21+
const wrongStatus = useSharedValue<StatusType>('wrong');
22+
23+
return (
24+
<View style={styles.container}>
25+
{new Array(maxLength).fill(0).map((_, index) => (
26+
<View key={index} style={styles.codeContainer}>
27+
<AnimatedCodeNumber
28+
code={code[index]}
29+
highlighted={index === code.length}
30+
status={isCodeWrong ? wrongStatus : status}
31+
/>
32+
</View>
33+
))}
34+
</View>
35+
);
36+
};
37+
38+
const styles = StyleSheet.create({
39+
container: {
40+
flex: 1,
41+
width: '100%',
42+
flexDirection: 'row',
43+
},
44+
codeContainer: {
45+
aspectRatio: 0.95,
46+
flex: 1,
47+
justifyContent: 'center',
48+
alignItems: 'center',
49+
},
50+
});

src/i18n/en.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@
5050
"SUB_TITLE": "Don't worry! We got your back",
5151
"API_SUCCESS": "Password reset link has been sent to your email address"
5252
},
53+
"MFA": {
54+
"TITLE": "Two-Factor Authentication",
55+
"TABS": {
56+
"AUTHENTICATOR_APP": "Authenticator App",
57+
"BACKUP_CODE": "Backup Code"
58+
},
59+
"INSTRUCTIONS": {
60+
"AUTHENTICATOR": "Enter 6-digit code from your authenticator app",
61+
"BACKUP": "Enter your one of your backup code"
62+
},
63+
"PLACEHOLDERS": {
64+
"BACKUP_CODE": "Enter backup code"
65+
},
66+
"BUTTONS": {
67+
"VERIFY": "Verify",
68+
"VERIFYING": "Verifying..."
69+
}
70+
},
5371
"CONVERSATION": {
5472
"HEADER": {
5573
"TITLE": "Conversations",

src/navigation/stack/AuthStack.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
44
import ConfigInstallationURL from '@/screens/auth/ConfigURLScreen';
55
import Login from '@/screens/auth/LoginScreen';
66
import ForgotPassword from '@/screens/auth/ForgotPassword';
7+
import MFAScreen from '@/screens/auth/MFAScreen';
78

89
export type AuthStackParamList = {
910
Login: undefined;
1011
ResetPassword: undefined;
1112
ConfigureURL: undefined;
13+
MFAScreen: undefined;
1214
};
1315

1416
const Stack = createNativeStackNavigator<AuthStackParamList>();
@@ -45,6 +47,17 @@ export const AuthStack = () => {
4547
name="ConfigureURL"
4648
component={ConfigInstallationURL}
4749
/>
50+
<Stack.Screen
51+
options={{
52+
headerShown: true,
53+
headerBackTitle: 'Back',
54+
headerBackVisible: true,
55+
headerShadowVisible: false,
56+
title: '',
57+
}}
58+
name="MFAScreen"
59+
component={MFAScreen}
60+
/>
4861
</Stack.Navigator>
4962
);
5063
};

0 commit comments

Comments
 (0)