Skip to content

Commit

Permalink
Add Push Notification Token Management (#8)
Browse files Browse the repository at this point in the history
# Add Notification Permission and Push Token Management

## Description
Implemented notification permission handling using `expo-notifications`.
The app now checks for and requests notification permissions on launch
and when the app becomes active. Additionally, implemented push
notification token handling using Supabase. Tokens are registered on
login and deleted on logout.

## Changes
### Notification Permissions
- Integrated `expo-notifications` to manage notification permissions.
- Added permission check on app launch.
- Re-checked permissions when the app becomes active using `AppState`.
- Configured `LSApplicationQueriesSchemes` for `mailto:` in `app.json`.

### Push Token Management
- Added functions for push token management in Supabase:
  - `fetchAddPushToken`: Adds or retrieves an existing push token.
  - `fetchDeletePushToken`: Deletes a push token.
  - `fetchPushTokens`: Fetches all push tokens for a user.
- Integrated token registration on login and deletion on logout in the
app's layout.

## How to Test
1. **Notification Permissions**:
   - Verify that the app requests notification permissions on launch.
      - Note that user should be signed-in.
- Move the app to the background and back to the foreground to ensure
permissions are re-checked.
2. **Push Token Management**:
   - **Login**:
     - Verify the token is added to the `push_tokens` table in Supabase.
   - **Logout**:
- Verify the token is removed from the `push_tokens` table in Supabase.
  • Loading branch information
hyochan authored Aug 7, 2024
1 parent 23e7f7a commit 3a5f219
Show file tree
Hide file tree
Showing 29 changed files with 1,216 additions and 290 deletions.
17 changes: 14 additions & 3 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ export default ({config}: ConfigContext): ExpoConfig => ({
[
'expo-build-properties',
{
ios: {newArchEnabled: true},
android: {newArchEnabled: true},
// https://github.com/software-mansion/react-native-screens/issues/2219
// ios: {newArchEnabled: true},
// android: {newArchEnabled: true},
},
],
// @ts-ignore
Expand All @@ -59,6 +60,14 @@ export default ({config}: ConfigContext): ExpoConfig => ({
],
},
],
[
'expo-notifications',
{
icon: './assets/notification-icon.png',
color: '#ffffff',
defaultChannel: 'default',
},
],
],
experiments: {
typedRoutes: true,
Expand Down Expand Up @@ -94,6 +103,7 @@ export default ({config}: ConfigContext): ExpoConfig => ({
entitlements: {
'com.apple.developer.applesignin': ['Default'],
},
googleServicesFile: process.env.GOOGLE_SERVICES_IOS,
infoPlist: {
LSApplicationQueriesSchemes: ['mailto'],
CFBundleAllowMixedLocalizations: true,
Expand All @@ -107,7 +117,7 @@ export default ({config}: ConfigContext): ExpoConfig => ({
},
},
android: {
googleServicesFile: process.env.GOOGLE_SERVICES_JSON,
googleServicesFile: process.env.GOOGLE_SERVICES_ANDROID,
userInterfaceStyle: 'automatic',
permissions: [
'RECEIVE_BOOT_COMPLETED',
Expand All @@ -117,6 +127,7 @@ export default ({config}: ConfigContext): ExpoConfig => ({
'WRITE_EXTERNAL_STORAGE',
'NOTIFICATIONS',
'USER_FACING_NOTIFICATIONS',
'SCHEDULE_EXACT_ALARM',
],
adaptiveIcon: {
foregroundImage: './assets/adaptive_icon.png',
Expand Down
24 changes: 21 additions & 3 deletions app/(app)/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {Pressable, View} from 'react-native';
import {Icon, useDooboo} from 'dooboo-ui';
import {Link, Redirect, Tabs, useRouter} from 'expo-router';
import {useRecoilValue} from 'recoil';
import {useRecoilState} from 'recoil';

import {authRecoilState} from '../../../src/recoil/atoms';
import {t} from '../../../src/STRINGS';
import {useEffect, useRef} from 'react';
import * as Notifications from 'expo-notifications';

function SettingsMenu(): JSX.Element {
const {theme} = useDooboo();
const {push} = useRouter();

return (
<Link asChild href="/settings">
<Pressable onPress={() => push('settings')}>
<Pressable onPress={() => push('/settings')}>
{({pressed}) => (
<Icon
color={theme.text.basic}
Expand All @@ -28,7 +30,23 @@ function SettingsMenu(): JSX.Element {

export default function TabLayout(): JSX.Element {
const {theme} = useDooboo();
const {authId, user} = useRecoilValue(authRecoilState);
const [{authId, user}, setAuth] = useRecoilState(authRecoilState);
const notificationResponseListener = useRef<Notifications.Subscription>();

useEffect(() => {
if (!authId) return;
notificationResponseListener.current =
Notifications.addNotificationResponseReceivedListener((response) => {
console.log(JSON.stringify(response.notification.request));
});

return () => {
notificationResponseListener.current &&
Notifications.removeNotificationSubscription(
notificationResponseListener.current,
);
};
}, [authId, setAuth]);

if (!authId) {
return <Redirect href="/sign-in" />;
Expand Down
61 changes: 36 additions & 25 deletions app/(app)/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import styled from '@emotion/native';
import {Stack} from 'expo-router';
import {Icon, Typography} from 'dooboo-ui';
import {Icon, Typography, useDooboo} from 'dooboo-ui';
import {t} from '../../../src/STRINGS';
import {useRecoilValue} from 'recoil';
import {authRecoilState} from '../../../src/recoil/atoms';
Expand Down Expand Up @@ -81,20 +81,21 @@ const TagContainer = styled.View`
`;

const Tag = styled.View`
background-color: ${({theme}) => theme.role.accent};
background-color: ${({theme}) => theme.role.link};
border-radius: 20px;
padding: 6px 12px;
margin-right: 8px;
margin-bottom: 4px;
`;

const TagText = styled.Text`
color: ${({theme}) => theme.text.basic};
color: ${({theme}) => theme.text.contrast};
font-size: 14px;
`;

export default function Profile(): JSX.Element {
const {user, tags} = useRecoilValue(authRecoilState);
const {theme} = useDooboo();

return (
<Container>
Expand All @@ -109,7 +110,7 @@ export default function Profile(): JSX.Element {
source={user?.avatar_url ? {uri: user?.avatar_url} : IC_ICON}
/>
<UserName>{user?.display_name || ''}</UserName>
<UserBio>{user?.introduction || ''}</UserBio>
{user?.introduction ? <UserBio>{user?.introduction}</UserBio> : null}
</ProfileHeader>
<Content>
<InfoCard>
Expand All @@ -122,7 +123,7 @@ export default function Profile(): JSX.Element {
gap: 4px;
`}
>
<Icon name="GithubLogo" size={16} color="#333" />
<Icon name="GithubLogo" size={16} color={theme.role.link} />
<InfoValue>{user?.github_id || ''}</InfoValue>
</View>
</InfoItem>
Expand All @@ -131,26 +132,36 @@ export default function Profile(): JSX.Element {
<InfoValue>{user?.affiliation || ''}</InfoValue>
</InfoItem>
</InfoCard>
<InfoCard>
<InfoItem>
<InfoLabel>{t('onboarding.desiredConnection')}</InfoLabel>
<InfoValue>{user?.desired_connection || ''}</InfoValue>
</InfoItem>
<InfoItem>
<InfoLabel>{t('onboarding.futureExpectations')}</InfoLabel>
<InfoValue>{user?.future_expectations || ''}</InfoValue>
</InfoItem>
</InfoCard>
<InfoCard>
<InfoLabel>{t('onboarding.userTags')}:</InfoLabel>
<TagContainer>
{tags?.map((tag, index) => (
<Tag key={index}>
<TagText>{tag}</TagText>
</Tag>
))}
</TagContainer>
</InfoCard>

{user?.desired_connection || user?.future_expectations ? (
<InfoCard>
{user?.desired_connection ? (
<InfoItem>
<InfoLabel>{t('onboarding.desiredConnection')}</InfoLabel>
<InfoValue>{user?.desired_connection || ''}</InfoValue>
</InfoItem>
) : null}
{user?.future_expectations ? (
<InfoItem>
<InfoLabel>{t('onboarding.futureExpectations')}</InfoLabel>
<InfoValue>{user?.future_expectations || ''}</InfoValue>
</InfoItem>
) : null}
</InfoCard>
) : null}

{tags?.length ? (
<InfoCard>
<InfoLabel>{t('onboarding.userTags')}</InfoLabel>
<TagContainer>
{tags.map((tag, index) => (
<Tag key={index}>
<TagText>{tag}</TagText>
</Tag>
))}
</TagContainer>
</InfoCard>
) : null}
</Content>
</CustomScrollView>
</Container>
Expand Down
40 changes: 21 additions & 19 deletions app/(app)/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import * as yup from 'yup';
import {Controller, SubmitHandler, useForm} from 'react-hook-form';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
Pressable,
Expand All @@ -30,6 +29,7 @@ import {useRecoilState} from 'recoil';
import {authRecoilState} from '../../src/recoil/atoms';
import {ImageInsertArgs} from '../../src/types';
import FallbackComponent from '../../src/components/uis/FallbackComponent';
import {showAlert} from '../../src/utils/alert';

const Container = styled.SafeAreaView`
flex: 1;
Expand All @@ -55,11 +55,11 @@ const Content = styled.View`
`;

const schema = yup.object().shape({
display_name: yup.string().required(t('common.requiredField')),
display_name: yup.string().required(t('common.requiredField')).min(2).max(20),
github_id: yup.string().required(t('common.requiredField')),
affiliation: yup.string().required(t('common.requiredField')),
avatar_url: yup.string(),
meetup_id: yup.string(),
affiliation: yup.string(),
github_id: yup.string(),
other_sns_urls: yup.array().of(yup.string()),
tags: yup.array().of(yup.string()),
desired_connection: yup.string(),
Expand Down Expand Up @@ -115,7 +115,7 @@ export default function Onboarding(): JSX.Element {

let image: ImageInsertArgs | undefined = {};

if (profileImg) {
if (profileImg && !profileImg.startsWith('http')) {
const destPath = `users/${authId}`;

image = await uploadFileToSupabase({
Expand Down Expand Up @@ -148,7 +148,7 @@ export default function Onboarding(): JSX.Element {
return;
}

Alert.alert((error as Error)?.message || '');
showAlert((error as Error)?.message || '');
}
};

Expand Down Expand Up @@ -295,7 +295,7 @@ export default function Onboarding(): JSX.Element {
displayNameError
? displayNameError
: errors.display_name
? errors.display_name.message
? t('error.displayNameInvalid')
: ''
}
/>
Expand All @@ -304,9 +304,10 @@ export default function Onboarding(): JSX.Element {
/>
<Controller
control={control}
name="meetup_id"
name="github_id"
render={({field: {onChange, value}}) => (
<EditText
required
styles={{
label: css`
font-size: 14px;
Expand All @@ -316,21 +317,22 @@ export default function Onboarding(): JSX.Element {
`,
}}
colors={{focused: theme.role.primary}}
label={t('onboarding.meetupId')}
label={t('onboarding.githubId')}
onChangeText={onChange}
placeholder={t('onboarding.meetupIdPlaceholder')}
placeholder={t('onboarding.githubIdPlaceholder')}
value={value}
decoration="boxed"
error={errors.meetup_id ? errors.meetup_id.message : ''}
error={errors.github_id ? errors.github_id.message : ''}
/>
)}
rules={{validate: (value) => !!value}}
/>
<Controller
control={control}
name="github_id"
name="affiliation"
render={({field: {onChange, value}}) => (
<EditText
required
styles={{
label: css`
font-size: 14px;
Expand All @@ -340,19 +342,19 @@ export default function Onboarding(): JSX.Element {
`,
}}
colors={{focused: theme.role.primary}}
label={t('onboarding.githubId')}
label={t('onboarding.affiliation')}
onChangeText={onChange}
placeholder={t('onboarding.githubIdPlaceholder')}
placeholder={t('onboarding.affiliationPlaceholder')}
value={value}
decoration="boxed"
error={errors.github_id ? errors.github_id.message : ''}
error={errors.affiliation ? errors.affiliation.message : ''}
/>
)}
rules={{validate: (value) => !!value}}
/>
<Controller
control={control}
name="affiliation"
name="meetup_id"
render={({field: {onChange, value}}) => (
<EditText
styles={{
Expand All @@ -364,12 +366,12 @@ export default function Onboarding(): JSX.Element {
`,
}}
colors={{focused: theme.role.primary}}
label={t('onboarding.affiliation')}
label={t('onboarding.meetupId')}
onChangeText={onChange}
placeholder={t('onboarding.affiliationPlaceholder')}
placeholder={t('onboarding.meetupIdPlaceholder')}
value={value}
decoration="boxed"
error={errors.affiliation ? errors.affiliation.message : ''}
error={errors.meetup_id ? errors.meetup_id.message : ''}
/>
)}
rules={{validate: (value) => !!value}}
Expand Down
9 changes: 8 additions & 1 deletion app/(app)/picture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ export default function Picture(): JSX.Element {
const [loading, setLoading] = useState(true);

if (!imageUrl || typeof imageUrl !== 'string') {
return <CustomLoadingIndicator />;
return (
<>
<Stack.Screen
options={{headerShown: false, title: t('common.picture')}}
/>
<CustomLoadingIndicator />
</>
);
}

return (
Expand Down
8 changes: 7 additions & 1 deletion app/(app)/post/[id]/update.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
KeyboardAvoidingView,
Platform,
Pressable,
View,
} from 'react-native';
import ErrorFallback from '../../../../src/components/uis/FallbackComponent';
import {useRecoilValue} from 'recoil';
Expand Down Expand Up @@ -156,7 +157,7 @@ export default function PostUpdate(): JSX.Element {
return (
<KeyboardAvoidingView
behavior={Platform.select({ios: 'padding', default: undefined})}
keyboardVerticalOffset={Platform.select({ios: 116, default: 88})}
keyboardVerticalOffset={Platform.select({ios: 56})}
style={css`
background-color: ${theme.bg.basic};
`}
Expand Down Expand Up @@ -261,6 +262,11 @@ export default function PostUpdate(): JSX.Element {
`,
}}
/>
<View
style={css`
height: 48px;
`}
/>
</Content>
</CustomScrollView>
</KeyboardAvoidingView>
Expand Down
Loading

0 comments on commit 3a5f219

Please sign in to comment.