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
This commit is contained in:
Justin Hernandez
2025-04-29 13:43:14 -05:00
committed by GitHub
parent b1b6c201b4
commit ec6c2cae72
6 changed files with 200 additions and 27 deletions

25
app/src/Navigation/dev.ts Normal file
View File

@@ -0,0 +1,25 @@
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import DevHapticFeedbackScreen from '../screens/Settings/DevHapticFeedback';
import DevSettingsScreen from '../screens/Settings/DevSettingsScreen';
import { white } from '../utils/colors';
const settingsScreens = {
DevSettings: {
screen: DevSettingsScreen,
options: {
title: 'Developer Settings',
headerStyle: {
backgroundColor: white,
},
} as NativeStackNavigationOptions,
},
DevHapticFeedback: {
screen: DevHapticFeedbackScreen,
options: {
title: 'Haptic Feedback',
} as NativeStackNavigationOptions,
},
};
export default settingsScreens;

View File

@@ -16,6 +16,7 @@ import { white } from '../utils/colors';
import { setupUniversalLinkListenerInNavigation } from '../utils/deeplinks';
import accountScreens from './account';
import aesopScreens from './aesop';
import devScreens from './dev';
import homeScreens from './home';
import passportScreens from './passport';
import proveScreens from './prove';
@@ -39,6 +40,7 @@ const AppNavigation = createNativeStackNavigator({
...accountScreens,
...settingsScreens,
...recoveryScreens,
...devScreens,
...aesopScreens,
},
});

View File

@@ -1,7 +1,6 @@
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import CloudBackupScreen from '../screens/Settings/CloudBackupScreen';
import DevSettingsScreen from '../screens/Settings/DevSettingsScreen';
import PassportDataInfoScreen from '../screens/Settings/PassportDataInfoScreen';
import ShowRecoveryPhraseScreen from '../screens/Settings/ShowRecoveryPhraseScreen';
import SettingsScreen from '../screens/SettingsScreen';
@@ -43,15 +42,6 @@ const settingsScreens = {
},
} as NativeStackNavigationOptions,
},
DevSettings: {
screen: DevSettingsScreen,
options: {
title: 'Developer Settings',
headerStyle: {
backgroundColor: white,
},
} as NativeStackNavigationOptions,
},
CloudBackupSettings: {
screen: CloudBackupScreen,
options: {

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import { Button, ScrollView, styled } from 'tamagui';
import {
feedbackProgress,
feedbackSuccess,
feedbackUnsuccessful,
impactLight,
impactMedium,
notificationError,
notificationSuccess,
notificationWarning,
selectionChange,
} from '../../utils/haptic';
const StyledButton = styled(Button, {
width: '75%',
marginHorizontal: 'auto',
padding: 10,
backgroundColor: '#007BFF',
borderRadius: 10,
marginVertical: 10,
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
});
const DevHapticFeedback = () => {
return (
<ScrollView style={styles.container}>
<StyledButton onPress={feedbackUnsuccessful}>
Feedback Unsuccessful
</StyledButton>
<StyledButton onPress={feedbackSuccess}>Feedback Success</StyledButton>
<StyledButton onPress={feedbackProgress}>Feedback Progress</StyledButton>
<StyledButton onPress={notificationError}>
Notification Error
</StyledButton>
<StyledButton onPress={notificationSuccess}>
Notification Success
</StyledButton>
<StyledButton onPress={notificationWarning}>
Notification Warning
</StyledButton>
<StyledButton onPress={impactLight}>Impact Light</StyledButton>
<StyledButton onPress={impactMedium}>Impact Medium</StyledButton>
<StyledButton onPress={selectionChange}>Selection Change</StyledButton>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingVertical: 50,
backgroundColor: '#fff',
},
});
export default DevHapticFeedback;

View File

@@ -7,7 +7,7 @@ import {
VenetianMask,
} from '@tamagui/lucide-icons';
import React, { PropsWithChildren, useEffect, useState } from 'react';
import { Platform, TextInput } from 'react-native';
import { Platform, StyleProp, TextInput } from 'react-native';
import {
Adapt,
Button,
@@ -31,9 +31,22 @@ import {
} from '../../stores/passportDataProvider';
import { borderColor, textBlack } from '../../utils/colors';
interface DevSettingsScreenProps {}
interface DevSettingsScreenProps extends PropsWithChildren {
color?: string;
width?: number;
justifyContent?:
| 'center'
| 'unset'
| 'flex-start'
| 'flex-end'
| 'space-between'
| 'space-around'
| 'space-evenly';
userSelect?: 'all' | 'text' | 'none' | 'contain';
style?: StyleProp<any>;
}
function SelectableText({ children, ...props }: PropsWithChildren) {
function SelectableText({ children, ...props }: DevSettingsScreenProps) {
if (Platform.OS === 'ios') {
return (
<TextInput multiline editable={false} {...props}>
@@ -51,6 +64,7 @@ function SelectableText({ children, ...props }: PropsWithChildren) {
const items = [
'DevSettings',
'DevHapticFeedback',
'Splash',
'Launch',
'PassportOnboarding',
@@ -80,8 +94,7 @@ const ScreenSelector = ({}) => {
const navigation = useNavigation();
return (
<Select
onValueChange={screen => {
// @ts-expect-error - weird typing?
onValueChange={(screen: any) => {
navigation.navigate(screen);
}}
disablePreventBodyScroll

View File

@@ -13,13 +13,15 @@ export type HapticType =
export type HapticOptions = {
enableVibrateFallback?: boolean;
ignoreAndroidSystemSettings?: boolean;
androidPattern?: number[];
pattern?: number[];
increaseIosIntensity?: boolean;
};
const defaultOptions: HapticOptions = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false,
androidPattern: [50, 100, 50],
pattern: [50, 100, 50],
increaseIosIntensity: true,
};
/**
@@ -35,31 +37,111 @@ export const buttonTap = impactLight;
export const cancelTap = selectionChange;
export const confirmTap = impactMedium;
// Custom feedback events
// consistent light feedback at a steady interval
export const feedbackProgress = () => {
if (Platform.OS === 'android') {
triggerFeedback('custom', {
pattern: [0, 50, 450, 50, 450, 50],
});
return;
}
setTimeout(() => {
triggerFeedback('impactLight', {
increaseIosIntensity: false,
});
}, 500);
setTimeout(() => {
triggerFeedback('impactLight', {
increaseIosIntensity: false,
});
}, 1000);
setTimeout(() => {
triggerFeedback('impactLight', {
increaseIosIntensity: false,
});
}, 1500);
};
// light -> medium -> heavy intensity in sequence
export const feedbackSuccess = () => {
if (Platform.OS === 'android') {
triggerFeedback('custom', {
pattern: [500, 50, 200, 100, 150, 150],
});
return;
}
setTimeout(() => {
triggerFeedback('impactLight', {
increaseIosIntensity: false,
});
}, 500);
setTimeout(() => {
triggerFeedback('impactMedium', {
increaseIosIntensity: false,
});
}, 750);
setTimeout(() => {
triggerFeedback('impactHeavy', {
increaseIosIntensity: false,
});
}, 1000);
};
// heavy -> medium -> light intensity in sequence
export const feedbackUnsuccessful = () => {
if (Platform.OS === 'android') {
triggerFeedback('custom', {
pattern: [500, 150, 100, 100, 150, 50],
});
return;
}
setTimeout(() => {
triggerFeedback('impactHeavy', {
increaseIosIntensity: false,
});
}, 500);
setTimeout(() => {
triggerFeedback('impactMedium', {
increaseIosIntensity: false,
});
}, 750);
setTimeout(() => {
triggerFeedback('impactLight', {
increaseIosIntensity: false,
});
}, 1000);
};
/**
* Triggers haptic feedback or vibration based on platform.
* @param type - The haptic feedback type.
* @param options - Custom options (optional).
*/
export const triggerFeedback = (
type: HapticType,
type: HapticType | 'custom',
options: HapticOptions = {},
) => {
const mergedOptions = { ...defaultOptions, ...options };
if (Platform.OS === 'ios') {
// increase feedback intensity for iOS
if (type === 'impactLight') {
type = 'impactMedium';
} else if (type === 'impactMedium') {
type = 'impactHeavy';
if (Platform.OS === 'ios' && type !== 'custom') {
if (mergedOptions.increaseIosIntensity) {
if (type === 'impactLight') {
type = 'impactMedium';
} else if (type === 'impactMedium') {
type = 'impactHeavy';
}
}
ReactNativeHapticFeedback.trigger(type, {
enableVibrateFallback: mergedOptions.enableVibrateFallback,
ignoreAndroidSystemSettings: mergedOptions.ignoreAndroidSystemSettings,
});
} else {
if (mergedOptions.androidPattern) {
Vibration.vibrate(mergedOptions.androidPattern, false);
if (mergedOptions.pattern) {
Vibration.vibrate(mergedOptions.pattern, false);
} else {
Vibration.vibrate(100);
}