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

Adding Haptics feedback functionality #36

Merged
merged 4 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Make sure your development environment is setup and up-to-date by following this

After you have cloned this repo to start a new mobile app project go through the following steps to customize the template based on your project needs.

### Customizable features

Look for `[CUSTOMIZE]` in the codebase to find places where you need to customize the template.

For example, you can decide to opt in or out of **[haptic feedback](./src/utils/haptics.ts)** or **[store review](<./src/app/(tabs)/_layout.tsx>)**.

### Update app metadata

Update the following fields in the `config/app.config.ts`:
Expand Down
2 changes: 1 addition & 1 deletion app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const expoConfig: ExpoConfig = {
backgroundColor: config.adaptiveIcon.backgroundColor,
},
// Add more Android permissions here
permissions: [],
permissions: ['VIBRATE'],
},
ios: {
bundleIdentifier: appId,
Expand Down
4 changes: 2 additions & 2 deletions config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ export type Config = {
backgroundColor: string;
image: string;
};
appStoreUrl?: string;
playStoreUrl?: string;
appStoreUrl?: string; // You can safely remove this if you do not use the Store Review feature
playStoreUrl?: string; // You can safely remove this if you do not use the Store Review feature
};
11 changes: 10 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@lingui/core": "4.14.0",
"@lingui/react": "4.14.0",
"@react-native-menu/menu": "1.1.6",
"@react-navigation/drawer": "7.0.12",
"@react-navigation/drawer": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@shopify/flash-list": "1.7.1",
"expo": "~52.0.7",
Expand All @@ -56,6 +56,7 @@
"expo-device": "~7.0.1",
"expo-font": "~13.0.1",
"expo-image": "~2.0.1",
"expo-haptics": "~14.0.0",
"expo-linking": "~7.0.3",
"expo-localization": "~16.0.0",
"expo-router": "~4.0.7",
Expand Down
2 changes: 2 additions & 0 deletions src/app/(auth)/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useAuthStore } from '~services/auth';
import { useI18n } from '~services/i18n';
import { styled } from '~styles';
import { announceForAccessibility } from '~utils/a11y';
import { haptics } from '~utils/haptics';

type Credentials = {
email: string;
Expand All @@ -25,6 +26,7 @@ export default function Login() {
announceForAccessibility({
message: _(msg`Logged in successfully, entering the app`),
});
haptics.notificationSuccess();
} catch (error) {
console.log('> Failed to login', error);
showToast({ title: _(msg`Failed to login`), type: 'error' });
Expand Down
2 changes: 2 additions & 0 deletions src/app/(auth)/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useAuthStore } from '~services/auth';
import { useI18n } from '~services/i18n';
import { styled } from '~styles/styled';
import { announceForAccessibility } from '~utils/a11y';
import { haptics } from '~utils/haptics';

type Credentials = {
email: string;
Expand Down Expand Up @@ -41,6 +42,7 @@ export default function Signup() {
announceForAccessibility({
message: _(msg`Signed up successfully, entering the app`),
});
haptics.notificationSuccess();
} catch (error) {
console.log('> Failed to signup', error);
showToast({ title: _(msg`Failed to signup`), type: 'error' });
Expand Down
15 changes: 14 additions & 1 deletion src/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export type TabList = {
}[];

/**
* _[CUSTOMIZE]_
*
* Determines whether to use a fully customizable bottom tab bar (`CustomBottomBar`)
* or the default tab layout with minimal customization (`DefaultBottomBar`).
*
Expand All @@ -29,6 +31,17 @@ export type TabList = {
*/
const USE_CUSTOM_TABS = true;

/**
* _[CUSTOMIZE]_
*
* Determines whether to show the store review modal.
*
* The review modal is used to get users feedback about the app.
*
* The idea behind is to get negative feedback before sent to us via email and positive feedback directly in the store.
*/
const USE_STORE_REVIEW = true;

export default function TabsLayout() {
const { _ } = useI18n();

Expand Down Expand Up @@ -69,7 +82,7 @@ export default function TabsLayout() {
) : (
<DefaultBottomBar tabs={tabs} theme={theme} />
)}
<StoreReview />
{USE_STORE_REVIEW && <StoreReview />}
</>
);
}
Expand Down
10 changes: 0 additions & 10 deletions src/app/(tabs)/home.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import { Trans } from '@lingui/macro';
import { useEffect, useState } from 'react';

import { Text } from '~components/uikit';
import { styled } from '~styles';

export default function Home() {
const [firstName] = useState('Taylor');
const [lastName] = useState('Swift');

// 🔴 Avoid: redundant state and unnecessary Effect
const [_, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

return (
<Wrapper testID="homeScreen">
<Text variant="body">
Expand Down
3 changes: 3 additions & 0 deletions src/app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useAuthStore } from '~services/auth';
import { useI18n } from '~services/i18n';
import { styled } from '~styles';
import { announceForAccessibility } from '~utils/a11y';
import { haptics } from '~utils/haptics';

export default function Settings() {
useHeaderPlaygroundButton();
Expand All @@ -17,12 +18,14 @@ export default function Settings() {

function onLogout() {
logout();
haptics.notificationSuccess();
announceForAccessibility({
message: _(msg`Logged out successfully, back to the landing page`),
});
}

function handleLogout() {
haptics.notificationWarning();
alert(_(msg`Are you sure you want to logout?`), '', [
{ text: _(msg`Cancel`), style: 'cancel', onPress: () => {} },
{ text: _(msg`I am sure`), onPress: onLogout },
Expand Down
1 change: 1 addition & 0 deletions src/app/playground/buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ function IconButtonExamples({
color={color}
loading={loading}
disabled={disabled}
onPress={handlePress}
/>
))}
</Stack>
Expand Down
2 changes: 2 additions & 0 deletions src/app/playground/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { IconName } from '~components/uikit/Icon';
import * as icons from '~design-system/icons';
import { useI18n } from '~services/i18n';
import { styled } from '~styles';
import { haptics } from '~utils/haptics';

export default function Icons() {
const { _ } = useI18n();
Expand All @@ -23,6 +24,7 @@ export default function Icons() {
<Pressable
key={name}
onLongPress={async () => {
haptics.notificationSuccess();
await setStringAsync(name);
showToast({
title: _(msg`Copied to clipboard`),
Expand Down
2 changes: 2 additions & 0 deletions src/components/common/MenuList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Icon, Stack, Text } from '~components/uikit';
import { useI18n } from '~services/i18n';
import { styled } from '~styles';
import { haptics } from '~utils/haptics';

type Item = {
id: string;
Expand All @@ -14,7 +15,7 @@
checked?: boolean;
leftSlot?: ReactNode;
rightSlot?: ReactNode;
target?: FunctionComponent<any>; // | keyof ParamList;

Check warning on line 18 in src/components/common/MenuList.tsx

View workflow job for this annotation

GitHub Actions / Lint, typecheck, and test

Unexpected any. Specify a different type
targetName?: string;
onPress?: () => void;
platform?: 'ios' | 'android';
Expand Down Expand Up @@ -43,6 +44,7 @@
router.navigate(item.target);
}

haptics.selection();
item.onPress?.();
}

Expand All @@ -65,7 +67,7 @@
accessibilityRole={isPressable ? 'button' : 'text'}
accessibilityLabel={`${_(msg`Item`)} ${item.label}${item.currentValue ? `, ${_(msg`Selected value`)}: ${item.currentValue}` : ''}`} // eslint-disable-line lingui/no-unlocalized-strings
accessibilityHint={
isPressable ? _(msg`Double tap to select ${item.label}`) : ''

Check warning on line 70 in src/components/common/MenuList.tsx

View workflow job for this annotation

GitHub Actions / Lint, typecheck, and test

Should be ${variable}, not ${object.property} or ${myFunction()}
}
>
<ContentWrapper axis="x" spacing="small">
Expand Down
17 changes: 17 additions & 0 deletions src/components/common/Toaster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Icon, IconButton, Stack, Text } from '~components/uikit';
import { type IconName } from '~components/uikit/Icon';
import { styled, useTheme, type Color } from '~styles/styled';
import { announceForAccessibility } from '~utils/a11y';
import { haptics } from '~utils/haptics';

type Variant = 'info' | 'success' | 'warn' | 'error';

Expand Down Expand Up @@ -68,6 +69,7 @@ export function showToast({
icon?: IconName;
type: Variant;
}) {
getHaptic(type)();
ToastContainer.show({
text1: title,
text2: subtitle,
Expand All @@ -80,6 +82,21 @@ export function showToast({
});
}

function getHaptic(type: Variant) {
switch (type) {
case 'info':
return haptics.impactLight;
case 'warn':
return haptics.notificationWarning;
case 'error':
return haptics.notificationError;
case 'success':
return haptics.notificationSuccess;
default:
return haptics.impactLight;
}
}

function Toast({
title,
subtitle,
Expand Down
2 changes: 2 additions & 0 deletions src/components/common/custom-bottom-bar/BottomBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { type TabList } from '~app/(tabs)/_layout';
import { styled } from '~styles';
import { haptics } from '~utils/haptics';

import { TabBarButton } from './Tab';

Expand Down Expand Up @@ -42,6 +43,7 @@ export function BottomBar({
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
haptics.selection();
navigation.navigate(route.name, route.params);
}
};
Expand Down
2 changes: 2 additions & 0 deletions src/components/settings/SystemInfoMenuTarget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { showToast } from '~components/common/Toaster';
import { Text } from '~components/uikit';
import config from '~constants/config';
import { useI18n } from '~services/i18n';
import { haptics } from '~utils/haptics';

export function SystemInfoMenuTarget() {
const { _ } = useI18n();
Expand Down Expand Up @@ -68,6 +69,7 @@ export function SystemInfoMenuTarget() {
<TouchableOpacity
accessibilityRole="button"
onLongPress={async () => {
haptics.notificationSuccess();
await setStringAsync(updateId);
showToast({
title: _(msg`Copied to clipboard`),
Expand Down
8 changes: 7 additions & 1 deletion src/components/uikit/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Collapsible, { type CollapsibleProps } from 'react-native-collapsible';

import { useI18n } from '~services/i18n';
import { styled, type Color } from '~styles';
import { haptics } from '~utils/haptics';

import { Icon, type IconName } from './Icon';
import { Text } from './Text';
Expand All @@ -29,10 +30,15 @@ export function Accordion({
const { _ } = useI18n();
const [collapsed, setCollapsed] = useState(!initialOpen);

function onPress() {
haptics.selection();
setCollapsed((p) => !p);
}

return (
<Stack axis="y" spacing="small">
<TouchableOpacity
onPress={() => setCollapsed((p) => !p)}
onPress={onPress}
accessibilityRole="header"
accessibilityLabel={title}
accessibilityState={{ expanded: !collapsed }}
Expand Down
2 changes: 2 additions & 0 deletions src/components/uikit/SegmentedControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Animated, {

import { useI18n } from '~services/i18n';
import { styled } from '~styles';
import { haptics } from '~utils/haptics';

import { Text } from './Text';

Expand Down Expand Up @@ -56,6 +57,7 @@ function Segments<T>({
function handleSegmentChange(index: number) {
offset.value = segmentSize * index;
onSelect(segments[index].value);
haptics.selection();
}

return (
Expand Down
14 changes: 11 additions & 3 deletions src/components/uikit/buttons/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ActivityIndicator } from 'react-native';
import { ActivityIndicator, type GestureResponderEvent } from 'react-native';

import { type Typography, styled, useTheme } from '~styles';
import { styled, useTheme, type Typography } from '~styles';
import { haptics } from '~utils/haptics';

import { Icon } from '../Icon';
import { Text } from '../Text';
Expand Down Expand Up @@ -39,12 +40,19 @@ export function Button({
<Icon name={icon} color={textColor} size={iconSize} />
);

function _onPress(e: GestureResponderEvent) {
if (!disabled && onPress) {
haptics.selection();
onPress(e);
}
}

return (
<Wrapper
size={size}
disabled={disabled}
style={[wrapperStyle, style]}
onPress={!disabled ? onPress : undefined}
onPress={_onPress}
accessibilityRole={accessibilityRole ?? 'button'}
accessibilityState={{ disabled, busy: loading }}
{...rest}
Expand Down
Loading
Loading