diff --git a/messages/en.json b/messages/en.json
index 6f74e89b9..8534d05ca 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -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"
diff --git a/src/frontend/Navigation/ScreenGroups/AppScreens.tsx b/src/frontend/Navigation/ScreenGroups/AppScreens.tsx
index 9271af9d7..919b29e19 100644
--- a/src/frontend/Navigation/ScreenGroups/AppScreens.tsx
+++ b/src/frontend/Navigation/ScreenGroups/AppScreens.tsx
@@ -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;
@@ -127,6 +128,7 @@ export type AppList = {
ReviewAndInvite: InviteProps;
InviteAccepted: InviteProps;
InviteDeclined: InviteProps;
+ UnableToCancelInvite: InviteProps;
DeviceNameDisplay: undefined;
DeviceNameEdit: undefined;
};
@@ -340,5 +342,10 @@ export const createDefaultScreenGroup = (
component={InviteDeclined}
options={{headerShown: false}}
/>
+
);
diff --git a/src/frontend/contexts/ProjectContext.tsx b/src/frontend/contexts/ProjectContext.tsx
index 4550c6090..09a2d3e63 100644
--- a/src/frontend/contexts/ProjectContext.tsx
+++ b/src/frontend/contexts/ProjectContext.tsx
@@ -67,7 +67,7 @@ export const ActiveProjectProvider = ({
return () => {
cancelled = true;
};
- }, [activeProjectId, setActiveProjectId]);
+ }, [activeProjectId, setActiveProjectId, mapeoApi]);
if (!activeProject) {
return ;
diff --git a/src/frontend/hooks/server/invites.ts b/src/frontend/hooks/server/invites.ts
index 6394a1265..9f73f7d8f 100644
--- a/src/frontend/hooks/server/invites.ts
+++ b/src/frontend/hooks/server/invites.ts
@@ -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';
@@ -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);
},
});
}
@@ -61,3 +73,33 @@ export function useClearAllPendingInvites() {
},
});
}
+
+export function useSendInvite() {
+ const queryClient = useQueryClient();
+ const project = useProject();
+ type InviteParams = Parameters;
+ 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]});
+ },
+ });
+}
diff --git a/src/frontend/hooks/server/projects.ts b/src/frontend/hooks/server/projects.ts
index e54f721f6..7f5c39e90 100644
--- a/src/frontend/hooks/server/projects.ts
+++ b/src/frontend/hooks/server/projects.ts
@@ -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;
@@ -17,7 +19,7 @@ export function useAllProjects() {
return useQuery({
queryFn: async () => await api.listProjects(),
- queryKey: ['projects'],
+ queryKey: [PROJECTS_KEY],
});
}
diff --git a/src/frontend/hooks/useProjectInvite.ts b/src/frontend/hooks/useProjectInvite.ts
index db0c74e20..67da14ec0 100644
--- a/src/frontend/hooks/useProjectInvite.ts
+++ b/src/frontend/hooks/useProjectInvite.ts
@@ -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();
diff --git a/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/UnableToCancelInvite.tsx b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/UnableToCancelInvite.tsx
new file mode 100644
index 000000000..e3531c1ac
--- /dev/null
+++ b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/UnableToCancelInvite.tsx
@@ -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 (
+
+
+
+
+ {formatMessage(m.unableToCancel)}
+
+ {data?.name && (
+
+ {formatMessage(m.deviceHasJoined, {projectName: data.name})}
+
+ )}
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 20,
+ paddingTop: 80,
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ flex: 1,
+ },
+});
diff --git a/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/WaitingForInviteAccept.tsx b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/WaitingForInviteAccept.tsx
index 2db3322af..a8f7745fb 100644
--- a/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/WaitingForInviteAccept.tsx
+++ b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/WaitingForInviteAccept.tsx
@@ -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();
@@ -53,12 +57,7 @@ export const WaitingForInviteAccept = () => {
{t(m.waitingMessage)}
{t(m.timerMessage, {seconds: time})}
- {
- navigation.navigate('YourTeam');
- }}
- />
+
);
};
diff --git a/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/index.tsx b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/index.tsx
index ed0dca8c6..0a4230183 100644
--- a/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/index.tsx
+++ b/src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewAndInvite/index.tsx
@@ -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: {
@@ -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 (
- {inviteStatus === 'reviewing' ? (
+ {sendInviteMutation.isIdle ? (
= ({
role={role}
/>
) : (
-
+
)}