Skip to content

Commit

Permalink
chore: screen to indicate when cancel invite was unsuccessful (#263)
Browse files Browse the repository at this point in the history
* chore: created screen

* chore: translations

* chore:update invite with tanstack

* chore: added navigation to unable to invite screen

* chore: add notes

* chore: added missing useEffect dependency

* chore: switch project on accept of invite

* chore: added screen to navigation

* core:remove unneccessary plural

* chore: translations

* chore: remove unecessary console log

* chore: pr rfixes
  • Loading branch information
ErikSin authored Apr 22, 2024
1 parent c1a9f92 commit 38f73c7
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 37 deletions.
6 changes: 6 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -570,9 +570,15 @@
"screens.Settings.YourTeam.close": {
"message": "Close"
},
"screens.Settings.YourTeam.deviceHasJoined": {
"message": "Device Has Joined {projectName}"
},
"screens.Settings.YourTeam.inviteDeclinedDes": {
"message": "This device has declined your invitation. They have not joined the project."
},
"screens.Settings.YourTeam.unableToCancel": {
"message": "Unable to Cancel Invitation"
},
"screens.Settings.aboutMapeo": {
"description": "Primary text for 'About Mapeo' link (version info)",
"message": "About Mapeo"
Expand Down
7 changes: 7 additions & 0 deletions src/frontend/Navigation/ScreenGroups/AppScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {useLocation} from '../../hooks/useLocation';
import {useLocationProviderStatus} from '../../hooks/useLocationProviderStatus';
import {getLocationStatus} from '../../lib/utils';
import {InviteDeclined} from '../../screens/Settings/ProjectSettings/YourTeam/InviteDeclined';
import {UnableToCancelInvite} from '../../screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/UnableToCancelInvite';

export type HomeTabsList = {
Map: undefined;
Expand Down Expand Up @@ -127,6 +128,7 @@ export type AppList = {
ReviewAndInvite: InviteProps;
InviteAccepted: InviteProps;
InviteDeclined: InviteProps;
UnableToCancelInvite: InviteProps;
DeviceNameDisplay: undefined;
DeviceNameEdit: undefined;
};
Expand Down Expand Up @@ -340,5 +342,10 @@ export const createDefaultScreenGroup = (
component={InviteDeclined}
options={{headerShown: false}}
/>
<RootStack.Screen
name="UnableToCancelInvite"
component={UnableToCancelInvite}
options={{headerShown: false}}
/>
</RootStack.Group>
);
2 changes: 1 addition & 1 deletion src/frontend/contexts/ProjectContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const ActiveProjectProvider = ({
return () => {
cancelled = true;
};
}, [activeProjectId, setActiveProjectId]);
}, [activeProjectId, setActiveProjectId, mapeoApi]);

if (!activeProject) {
return <Loading />;
Expand Down
46 changes: 44 additions & 2 deletions src/frontend/hooks/server/invites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useSuspenseQuery,
} from '@tanstack/react-query';
import {useApi} from '../../contexts/ApiContext';
import {PROJECTS_KEY, useProject, useUpdateActiveProjectId} from './projects';

export const INVITE_KEY = 'pending_invites';

Expand All @@ -17,16 +18,27 @@ export function usePendingInvites() {
});
}

export function useAcceptInvite() {
export function useAcceptInvite(projectId?: string) {
const mapeoApi = useApi();
const queryClient = useQueryClient();
const switchActiveProject = useUpdateActiveProjectId();

return useMutation({
mutationFn: async ({inviteId}: {inviteId: string}) => {
if (!inviteId) return;
mapeoApi.invite.accept({inviteId});
},
onSuccess: () => {
queryClient.invalidateQueries({queryKey: [INVITE_KEY]});
// This is a workaround. There is a race condition where the project in not available when the invite is accepted. This is temporary and is currently being worked on.
setTimeout(() => {
queryClient
.invalidateQueries({queryKey: [INVITE_KEY, PROJECTS_KEY]})
.then(() => {
if (projectId) {
switchActiveProject(projectId);
}
});
}, 5000);
},
});
}
Expand Down Expand Up @@ -61,3 +73,33 @@ export function useClearAllPendingInvites() {
},
});
}

export function useSendInvite() {
const queryClient = useQueryClient();
const project = useProject();
type InviteParams = Parameters<typeof project.$member.invite>;
return useMutation({
mutationFn: ({
deviceId,
role,
}: {
deviceId: InviteParams[0];
role: InviteParams[1];
}) => project.$member.invite(deviceId, role),
onSuccess: () => {
queryClient.invalidateQueries({queryKey: [INVITE_KEY]});
},
});
}

export function useRequestCancelInvite() {
const queryClient = useQueryClient();
const project = useProject();
return useMutation({
mutationFn: (deviceId: string) =>
project.$member.requestCancelInvite(deviceId),
onSuccess: () => {
queryClient.invalidateQueries({queryKey: [INVITE_KEY]});
},
});
}
4 changes: 3 additions & 1 deletion src/frontend/hooks/server/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import {useApi} from '../../contexts/ApiContext';
import {useActiveProjectContext} from '../../contexts/ProjectContext';

export const PROJECTS_KEY = 'all_projects';

export function useUpdateActiveProjectId() {
const projectContext = useActiveProjectContext();
return projectContext.switchProject;
Expand All @@ -17,7 +19,7 @@ export function useAllProjects() {

return useQuery({
queryFn: async () => await api.listProjects(),
queryKey: ['projects'],
queryKey: [PROJECTS_KEY],
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/frontend/hooks/useProjectInvite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function useProjectInvite() {
const invites = usePendingInvites().data;
// this will eventually sort invite by date
const invite = invites[0];
const acceptMutation = useAcceptInvite();
const acceptMutation = useAcceptInvite(invite?.projectPublicId);
const rejectMutation = useRejectInvite();
const clearAllInvites = useClearAllPendingInvites();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import {Button} from '../../../../../sharedComponents/Button';
import ErrorIcon from '../../../../../images/Error.svg';
import {defineMessages, useIntl} from 'react-intl';
import {Text} from '../../../../../sharedComponents/Text';
import {DeviceNameWithIcon} from '../../../../../sharedComponents/DeviceNameWithIcon';
import {RoleWithIcon} from '../../../../../sharedComponents/RoleWithIcon';
import {
COORDINATOR_ROLE_ID,
NativeRootNavigationProps,
} from '../../../../../sharedTypes';
import {useProjectSettings} from '../../../../../hooks/server/projects';

const m = defineMessages({
unableToCancel: {
id: 'screens.Settings.YourTeam.unableToCancel',
defaultMessage: 'Unable to Cancel Invitation',
},
deviceHasJoined: {
id: 'screens.Settings.YourTeam.deviceHasJoined',
defaultMessage: 'Device Has Joined {projectName}',
},
close: {
id: 'screens.Settings.YourTeam.close',
defaultMessage: 'Close',
},
});

export const UnableToCancelInvite = ({
navigation,
route,
}: NativeRootNavigationProps<'UnableToCancelInvite'>) => {
const {formatMessage} = useIntl();
const {role, ...deviceInfo} = route.params;
const {data} = useProjectSettings();

return (
<View style={styles.container}>
<View style={{alignItems: 'center'}}>
<ErrorIcon />
<Text style={{marginTop: 20, fontSize: 20, fontWeight: 'bold'}}>
{formatMessage(m.unableToCancel)}
</Text>
{data?.name && (
<Text style={{marginTop: 10}}>
{formatMessage(m.deviceHasJoined, {projectName: data.name})}
</Text>
)}
<DeviceNameWithIcon {...deviceInfo} style={{marginTop: 10}} />
<RoleWithIcon
style={{marginTop: 20}}
role={role === COORDINATOR_ROLE_ID ? 'coordinator' : 'participant'}
/>
</View>
<Button
style={{marginTop: 10}}
fullWidth
onPress={() => {
navigation.navigate('YourTeam');
}}>
{formatMessage(m.close)}
</Button>
</View>
);
};

const styles = StyleSheet.create({
container: {
padding: 20,
paddingTop: 80,
alignItems: 'center',
justifyContent: 'space-between',
flex: 1,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ const m = defineMessages({
},
});

export const WaitingForInviteAccept = () => {
export const WaitingForInviteAccept = ({
cancelInvite,
}: {
cancelInvite: () => void;
}) => {
const {formatMessage: t} = useIntl();
const [time, setTime] = React.useState(0);
const navigation = useNavigationFromRoot();
Expand Down Expand Up @@ -53,12 +57,7 @@ export const WaitingForInviteAccept = () => {
<InviteSent />
<Text style={{marginTop: 10}}>{t(m.waitingMessage)}</Text>
<Text style={{marginTop: 20}}>{t(m.timerMessage, {seconds: time})}</Text>
<TextButton
title={t(m.cancelInvite)}
onPress={() => {
navigation.navigate('YourTeam');
}}
/>
<TextButton title={t(m.cancelInvite)} onPress={cancelInvite} />
</View>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import * as React from 'react';
import {NativeNavigationComponent} from '../../../../../sharedTypes';
import {defineMessages} from 'react-intl';
import {useBottomSheetModal} from '../../../../../sharedComponents/BottomSheetModal';
import {useQueryClient} from '@tanstack/react-query';
import {useProject} from '../../../../../hooks/server/projects';
import {ErrorModal} from '../../../../../sharedComponents/ErrorModal';
import {ReviewInvitation} from './ReviewInvitation';
import {WaitingForInviteAccept} from './WaitingForInviteAccept';
import {
useRequestCancelInvite,
useSendInvite,
} from '../../../../../hooks/server/invites';

const m = defineMessages({
title: {
Expand All @@ -19,41 +21,55 @@ export const ReviewAndInvite: NativeNavigationComponent<'ReviewAndInvite'> = ({
route,
navigation,
}) => {
const [inviteStatus, setInviteStatus] = React.useState<
'reviewing' | 'waiting'
>('reviewing');
const {role, deviceId, deviceType, name} = route.params;

const {openSheet, sheetRef, closeSheet, isOpen} = useBottomSheetModal({
openOnMount: false,
});
const project = useProject();
const queryClient = useQueryClient();
const sendInviteMutation = useSendInvite();
const requestCancelInviteMutation = useRequestCancelInvite();

function sendInvite() {
setInviteStatus('waiting');
project.$member
.invite(deviceId, {roleId: role})
.then(val => {
if (val === 'ACCEPT') {
queryClient.invalidateQueries({queryKey: ['projectMembers']});
navigation.navigate('InviteAccepted', route.params);
return;
}
sendInviteMutation.mutate(
{deviceId, role: {roleId: role}},
{
onSuccess: val => {
// If user has attempted to cancel an invite, but an invite has already been accepted, let user know their cancellation was unsuccessful
if (val === 'ACCEPT' && requestCancelInviteMutation.isPending) {
navigation.navigate('UnableToCancelInvite', {...route.params});
return;
}
if (val === 'ACCEPT') {
navigation.navigate('InviteAccepted', route.params);
return;
}

if (val === 'REJECT') {
navigation.navigate('InviteDeclined', route.params);
return;
}
})
.catch(() => {
if (val === 'REJECT') {
navigation.navigate('InviteDeclined', route.params);
return;
}
},
onError: () => {
openSheet();
},
},
);
}

function cancelInvite() {
requestCancelInviteMutation.mutate(deviceId, {
onSuccess: () => {
navigation.navigate('YourTeam');
},
onError: () => {
openSheet();
});
},
});
}

return (
<React.Fragment>
{inviteStatus === 'reviewing' ? (
{sendInviteMutation.isIdle ? (
<ReviewInvitation
sendInvite={sendInvite}
deviceId={deviceId}
Expand All @@ -62,7 +78,7 @@ export const ReviewAndInvite: NativeNavigationComponent<'ReviewAndInvite'> = ({
role={role}
/>
) : (
<WaitingForInviteAccept />
<WaitingForInviteAccept cancelInvite={cancelInvite} />
)}
<ErrorModal
sheetRef={sheetRef}
Expand Down

0 comments on commit 38f73c7

Please sign in to comment.