Skip to content

Commit ec6c2ca

Browse files
authored
SEL-178: Improve haptic feedback library (#535)
* fix dev settings typing * add dev screens file * save haptic feedback progress * change ordedr * fix initial route and add haptic feedback screen to dev settings options
1 parent b1b6c20 commit ec6c2ca

File tree

6 files changed

+200
-27
lines changed

6 files changed

+200
-27
lines changed

app/src/Navigation/dev.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
2+
3+
import DevHapticFeedbackScreen from '../screens/Settings/DevHapticFeedback';
4+
import DevSettingsScreen from '../screens/Settings/DevSettingsScreen';
5+
import { white } from '../utils/colors';
6+
7+
const settingsScreens = {
8+
DevSettings: {
9+
screen: DevSettingsScreen,
10+
options: {
11+
title: 'Developer Settings',
12+
headerStyle: {
13+
backgroundColor: white,
14+
},
15+
} as NativeStackNavigationOptions,
16+
},
17+
DevHapticFeedback: {
18+
screen: DevHapticFeedbackScreen,
19+
options: {
20+
title: 'Haptic Feedback',
21+
} as NativeStackNavigationOptions,
22+
},
23+
};
24+
25+
export default settingsScreens;

app/src/Navigation/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { white } from '../utils/colors';
1616
import { setupUniversalLinkListenerInNavigation } from '../utils/deeplinks';
1717
import accountScreens from './account';
1818
import aesopScreens from './aesop';
19+
import devScreens from './dev';
1920
import homeScreens from './home';
2021
import passportScreens from './passport';
2122
import proveScreens from './prove';
@@ -39,6 +40,7 @@ const AppNavigation = createNativeStackNavigator({
3940
...accountScreens,
4041
...settingsScreens,
4142
...recoveryScreens,
43+
...devScreens,
4244
...aesopScreens,
4345
},
4446
});

app/src/Navigation/settings.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
22

33
import CloudBackupScreen from '../screens/Settings/CloudBackupScreen';
4-
import DevSettingsScreen from '../screens/Settings/DevSettingsScreen';
54
import PassportDataInfoScreen from '../screens/Settings/PassportDataInfoScreen';
65
import ShowRecoveryPhraseScreen from '../screens/Settings/ShowRecoveryPhraseScreen';
76
import SettingsScreen from '../screens/SettingsScreen';
@@ -43,15 +42,6 @@ const settingsScreens = {
4342
},
4443
} as NativeStackNavigationOptions,
4544
},
46-
DevSettings: {
47-
screen: DevSettingsScreen,
48-
options: {
49-
title: 'Developer Settings',
50-
headerStyle: {
51-
backgroundColor: white,
52-
},
53-
} as NativeStackNavigationOptions,
54-
},
5545
CloudBackupSettings: {
5646
screen: CloudBackupScreen,
5747
options: {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React from 'react';
2+
import { StyleSheet } from 'react-native';
3+
import { Button, ScrollView, styled } from 'tamagui';
4+
5+
import {
6+
feedbackProgress,
7+
feedbackSuccess,
8+
feedbackUnsuccessful,
9+
impactLight,
10+
impactMedium,
11+
notificationError,
12+
notificationSuccess,
13+
notificationWarning,
14+
selectionChange,
15+
} from '../../utils/haptic';
16+
17+
const StyledButton = styled(Button, {
18+
width: '75%',
19+
marginHorizontal: 'auto',
20+
padding: 10,
21+
backgroundColor: '#007BFF',
22+
borderRadius: 10,
23+
marginVertical: 10,
24+
color: '#fff',
25+
fontSize: 16,
26+
fontWeight: 'bold',
27+
});
28+
29+
const DevHapticFeedback = () => {
30+
return (
31+
<ScrollView style={styles.container}>
32+
<StyledButton onPress={feedbackUnsuccessful}>
33+
Feedback Unsuccessful
34+
</StyledButton>
35+
<StyledButton onPress={feedbackSuccess}>Feedback Success</StyledButton>
36+
<StyledButton onPress={feedbackProgress}>Feedback Progress</StyledButton>
37+
<StyledButton onPress={notificationError}>
38+
Notification Error
39+
</StyledButton>
40+
<StyledButton onPress={notificationSuccess}>
41+
Notification Success
42+
</StyledButton>
43+
<StyledButton onPress={notificationWarning}>
44+
Notification Warning
45+
</StyledButton>
46+
<StyledButton onPress={impactLight}>Impact Light</StyledButton>
47+
<StyledButton onPress={impactMedium}>Impact Medium</StyledButton>
48+
<StyledButton onPress={selectionChange}>Selection Change</StyledButton>
49+
</ScrollView>
50+
);
51+
};
52+
53+
const styles = StyleSheet.create({
54+
container: {
55+
flex: 1,
56+
paddingVertical: 50,
57+
backgroundColor: '#fff',
58+
},
59+
});
60+
61+
export default DevHapticFeedback;

app/src/screens/Settings/DevSettingsScreen.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
VenetianMask,
88
} from '@tamagui/lucide-icons';
99
import React, { PropsWithChildren, useEffect, useState } from 'react';
10-
import { Platform, TextInput } from 'react-native';
10+
import { Platform, StyleProp, TextInput } from 'react-native';
1111
import {
1212
Adapt,
1313
Button,
@@ -31,9 +31,22 @@ import {
3131
} from '../../stores/passportDataProvider';
3232
import { borderColor, textBlack } from '../../utils/colors';
3333

34-
interface DevSettingsScreenProps {}
34+
interface DevSettingsScreenProps extends PropsWithChildren {
35+
color?: string;
36+
width?: number;
37+
justifyContent?:
38+
| 'center'
39+
| 'unset'
40+
| 'flex-start'
41+
| 'flex-end'
42+
| 'space-between'
43+
| 'space-around'
44+
| 'space-evenly';
45+
userSelect?: 'all' | 'text' | 'none' | 'contain';
46+
style?: StyleProp<any>;
47+
}
3548

36-
function SelectableText({ children, ...props }: PropsWithChildren) {
49+
function SelectableText({ children, ...props }: DevSettingsScreenProps) {
3750
if (Platform.OS === 'ios') {
3851
return (
3952
<TextInput multiline editable={false} {...props}>
@@ -51,6 +64,7 @@ function SelectableText({ children, ...props }: PropsWithChildren) {
5164

5265
const items = [
5366
'DevSettings',
67+
'DevHapticFeedback',
5468
'Splash',
5569
'Launch',
5670
'PassportOnboarding',
@@ -80,8 +94,7 @@ const ScreenSelector = ({}) => {
8094
const navigation = useNavigation();
8195
return (
8296
<Select
83-
onValueChange={screen => {
84-
// @ts-expect-error - weird typing?
97+
onValueChange={(screen: any) => {
8598
navigation.navigate(screen);
8699
}}
87100
disablePreventBodyScroll

app/src/utils/haptic.ts

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ export type HapticType =
1313
export type HapticOptions = {
1414
enableVibrateFallback?: boolean;
1515
ignoreAndroidSystemSettings?: boolean;
16-
androidPattern?: number[];
16+
pattern?: number[];
17+
increaseIosIntensity?: boolean;
1718
};
1819

1920
const defaultOptions: HapticOptions = {
2021
enableVibrateFallback: true,
2122
ignoreAndroidSystemSettings: false,
22-
androidPattern: [50, 100, 50],
23+
pattern: [50, 100, 50],
24+
increaseIosIntensity: true,
2325
};
2426

2527
/**
@@ -35,31 +37,111 @@ export const buttonTap = impactLight;
3537
export const cancelTap = selectionChange;
3638
export const confirmTap = impactMedium;
3739

40+
// Custom feedback events
41+
// consistent light feedback at a steady interval
42+
export const feedbackProgress = () => {
43+
if (Platform.OS === 'android') {
44+
triggerFeedback('custom', {
45+
pattern: [0, 50, 450, 50, 450, 50],
46+
});
47+
return;
48+
}
49+
50+
setTimeout(() => {
51+
triggerFeedback('impactLight', {
52+
increaseIosIntensity: false,
53+
});
54+
}, 500);
55+
setTimeout(() => {
56+
triggerFeedback('impactLight', {
57+
increaseIosIntensity: false,
58+
});
59+
}, 1000);
60+
setTimeout(() => {
61+
triggerFeedback('impactLight', {
62+
increaseIosIntensity: false,
63+
});
64+
}, 1500);
65+
};
66+
67+
// light -> medium -> heavy intensity in sequence
68+
export const feedbackSuccess = () => {
69+
if (Platform.OS === 'android') {
70+
triggerFeedback('custom', {
71+
pattern: [500, 50, 200, 100, 150, 150],
72+
});
73+
return;
74+
}
75+
76+
setTimeout(() => {
77+
triggerFeedback('impactLight', {
78+
increaseIosIntensity: false,
79+
});
80+
}, 500);
81+
setTimeout(() => {
82+
triggerFeedback('impactMedium', {
83+
increaseIosIntensity: false,
84+
});
85+
}, 750);
86+
setTimeout(() => {
87+
triggerFeedback('impactHeavy', {
88+
increaseIosIntensity: false,
89+
});
90+
}, 1000);
91+
};
92+
93+
// heavy -> medium -> light intensity in sequence
94+
export const feedbackUnsuccessful = () => {
95+
if (Platform.OS === 'android') {
96+
triggerFeedback('custom', {
97+
pattern: [500, 150, 100, 100, 150, 50],
98+
});
99+
return;
100+
}
101+
102+
setTimeout(() => {
103+
triggerFeedback('impactHeavy', {
104+
increaseIosIntensity: false,
105+
});
106+
}, 500);
107+
setTimeout(() => {
108+
triggerFeedback('impactMedium', {
109+
increaseIosIntensity: false,
110+
});
111+
}, 750);
112+
setTimeout(() => {
113+
triggerFeedback('impactLight', {
114+
increaseIosIntensity: false,
115+
});
116+
}, 1000);
117+
};
118+
38119
/**
39120
* Triggers haptic feedback or vibration based on platform.
40121
* @param type - The haptic feedback type.
41122
* @param options - Custom options (optional).
42123
*/
43124
export const triggerFeedback = (
44-
type: HapticType,
125+
type: HapticType | 'custom',
45126
options: HapticOptions = {},
46127
) => {
47128
const mergedOptions = { ...defaultOptions, ...options };
48-
49-
if (Platform.OS === 'ios') {
50-
// increase feedback intensity for iOS
51-
if (type === 'impactLight') {
52-
type = 'impactMedium';
53-
} else if (type === 'impactMedium') {
54-
type = 'impactHeavy';
129+
if (Platform.OS === 'ios' && type !== 'custom') {
130+
if (mergedOptions.increaseIosIntensity) {
131+
if (type === 'impactLight') {
132+
type = 'impactMedium';
133+
} else if (type === 'impactMedium') {
134+
type = 'impactHeavy';
135+
}
55136
}
137+
56138
ReactNativeHapticFeedback.trigger(type, {
57139
enableVibrateFallback: mergedOptions.enableVibrateFallback,
58140
ignoreAndroidSystemSettings: mergedOptions.ignoreAndroidSystemSettings,
59141
});
60142
} else {
61-
if (mergedOptions.androidPattern) {
62-
Vibration.vibrate(mergedOptions.androidPattern, false);
143+
if (mergedOptions.pattern) {
144+
Vibration.vibrate(mergedOptions.pattern, false);
63145
} else {
64146
Vibration.vibrate(100);
65147
}

0 commit comments

Comments
 (0)