Skip to content

Commit

Permalink
feat: Delete ticket recipient (#4656)
Browse files Browse the repository at this point in the history
  • Loading branch information
gorandalum authored Aug 13, 2024
1 parent bafc46f commit 82a009a
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 36 deletions.
8 changes: 8 additions & 0 deletions src/api/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,11 @@ export const getPhoneNumberFromId = async (
)
.then((response) => response.data.phone);
};

export const deleteOnBehalfOfAccount = async (accountId: string) => {
await client.patch(
`${profileEndpoint}/on-behalf-of/${accountId}`,
{customerAlias: undefined, phoneNumber: undefined},
{authWithIdToken: true},
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ import {SubmitButton} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScre
import {SaveRecipientToggle} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/components/SaveRecipientToggle.tsx';
import {ExistingRecipientsList} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/components/ExistingRecipientsList.tsx';
import {PhoneAndNameInputSection} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/components/PhoneAndNameInputSection.tsx';
import {SendToOtherButton} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/components/SendToOtherButton.tsx';
import {TitleAndDescription} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/components/TitleAndDescription.tsx';
import {
FETCH_RECIPIENTS_QUERY_KEY,
} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/use-fetch-recipients-query.ts';
import {FETCH_RECIPIENTS_QUERY_KEY} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/use-fetch-recipients-query.ts';
import {useQueryClient} from '@tanstack/react-query';
import {SendToOtherButton} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/components/SendToOtherButton.tsx';

type Props = RootStackScreenProps<'Root_ChooseTicketRecipientScreen'>;
const themeColor: StaticColorByType<'background'> = 'background_accent_0';
Expand Down Expand Up @@ -64,7 +62,7 @@ export const Root_ChooseTicketRecipientScreen = ({
animateNextChange();
dispatch({type: 'SELECT_RECIPIENT', recipient});
}}
onErrorOrEmpty={useCallback(() => {
onEmptyRecipients={useCallback(() => {
animateNextChange();
dispatch({type: 'SELECT_SEND_TO_OTHER'});
}, [dispatch])}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,53 @@ import {
} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/types.ts';
import {dictionary, useTranslation} from '@atb/translations';
import {useFetchRecipientsQuery} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/use-fetch-recipients-query.ts';
import {useEffect} from 'react';
import {useEffect, useLayoutEffect} from 'react';
import {ActivityIndicator} from 'react-native';
import {MessageInfoBox} from '@atb/components/message-info-box';
import {RadioGroupSection} from '@atb/components/sections';
import {StyleSheet, useTheme} from '@atb/theme';
import {getStaticColor, StaticColor} from '@atb/theme/colors.ts';
import OnBehalfOfTexts from "@atb/translations/screens/subscreens/OnBehalfOf.ts";
import OnBehalfOfTexts from '@atb/translations/screens/subscreens/OnBehalfOf.ts';
import {Delete} from '@atb/assets/svg/mono-icons/actions';
import {useDeleteRecipientMutation} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/use-delete-recipient-mutation.ts';
import {animateNextChange} from '@atb/utils/animation.ts';
import {screenReaderPause} from '@atb/components/text';

export const ExistingRecipientsList = ({
state: {recipient},
onSelect,
onErrorOrEmpty,
onEmptyRecipients,
themeColor,
}: {
state: RecipientSelectionState;
onSelect: (r: ExistingRecipientType) => void;
onErrorOrEmpty: () => void;
onSelect: (r?: ExistingRecipientType) => void;
onEmptyRecipients: () => void;
themeColor: StaticColor;
}) => {
const styles = useStyles();
const {t} = useTranslation();
const {themeName} = useTheme();
const {theme, themeName} = useTheme();

const recipientsQuery = useFetchRecipientsQuery();
const {mutation: deleteMutation, activeDeletions} =
useDeleteRecipientMutation();

useLayoutEffect(() => {
if (deleteMutation.status !== 'idle') animateNextChange();
}, [deleteMutation.status]);

useEffect(() => {
const isError = recipientsQuery.status === 'error';
const isEmpty =
recipientsQuery.status === 'success' && !recipientsQuery.data.length;
if (isError || isEmpty) {
onErrorOrEmpty();
if (recipientsQuery.status === 'success' && !recipientsQuery.data.length) {
onEmptyRecipients();
}
}, [recipientsQuery.status, recipientsQuery.data, onEmptyRecipients]);

const onDelete = ({accountId}: ExistingRecipientType) => {
if (recipient?.accountId === accountId) {
onSelect(undefined);
}
}, [recipientsQuery.status, recipientsQuery.data, onErrorOrEmpty]);
deleteMutation.mutate(accountId);
};

return (
<>
Expand All @@ -50,24 +64,45 @@ export const ExistingRecipientsList = ({
{recipientsQuery.status === 'error' && (
<MessageInfoBox
type="error"
message={t(OnBehalfOfTexts.errors.fetchRecipients)}
message={t(OnBehalfOfTexts.errors.fetch_recipients_failed)}
onPressConfig={{
action: recipientsQuery.refetch,
text: t(dictionary.retry),
}}
style={styles.errorMessage}
/>
)}
{deleteMutation.isError && (
<MessageInfoBox
type="error"
message={t(OnBehalfOfTexts.errors.delete_recipient_failed)}
style={styles.errorMessage}
/>
)}
{recipientsQuery.status === 'success' && recipientsQuery.data?.length ? (
<RadioGroupSection
items={recipientsQuery.data}
itemToText={(i) => i.name}
itemToSubtext={(i) => i.phoneNumber}
itemToA11yLabel={(i) =>
i.name +
screenReaderPause +
i.phoneNumber.split('').join(screenReaderPause)
}
keyExtractor={(i) => i.accountId}
selected={recipient}
onSelect={onSelect}
onSelect={(item) =>
!activeDeletions.includes(item.accountId) && onSelect(item)
}
color="interactive_2"
style={styles.recipientList}
itemToRightAction={(item) => ({
icon: (props) => (
<Delete {...props} fill={theme.status.error.primary.background} />
),
onPress: () => onDelete(item),
isLoading: activeDeletions.includes(item.accountId),
})}
/>
) : null}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import {RecipientSelectionState} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/types.ts';
import {OnBehalfOfTexts, useTranslation} from '@atb/translations';
import {TouchableOpacity} from 'react-native';
import {Checkbox} from '@atb/components/checkbox';
import {ThemeText} from '@atb/components/text';
import {StyleSheet} from '@atb/theme';
import {StaticColor} from '@atb/theme/colors.ts';
import {useFetchRecipientsQuery} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/use-fetch-recipients-query.ts';
import {MessageInfoBox} from '@atb/components/message-info-box';
import {PressableOpacity} from '@atb/components/pressable-opacity';

const MAX_RECIPIENTS = 10;

export const SaveRecipientToggle = ({
state: {settingPhone, settingName},
Expand All @@ -17,20 +21,33 @@ export const SaveRecipientToggle = ({
}) => {
const styles = useStyles();
const {t} = useTranslation();
const {data: recipients} = useFetchRecipientsQuery();
if (!settingPhone) return null;

const isAtMaxRecipients = (recipients?.length || 0) >= MAX_RECIPIENTS;

return (
<TouchableOpacity
style={styles.container}
onPress={onPress}
accessibilityRole="checkbox"
accessibilityState={{checked: settingPhone}}
>
<Checkbox checked={settingName} />
<ThemeText color={themeColor}>
{t(OnBehalfOfTexts.saveCheckBoxLabel)}
</ThemeText>
</TouchableOpacity>
<>
{isAtMaxRecipients && (
<MessageInfoBox
type="warning"
message={t(OnBehalfOfTexts.tooManyRecipients)}
style={styles.maxRecipientsWarning}
/>
)}
<PressableOpacity
style={[styles.container, isAtMaxRecipients && {opacity: 0.2}]}
onPress={onPress}
accessibilityRole="checkbox"
accessibilityState={{checked: settingPhone}}
disabled={isAtMaxRecipients}
>
<Checkbox checked={settingName} />
<ThemeText color={themeColor}>
{t(OnBehalfOfTexts.saveCheckBoxLabel)}
</ThemeText>
</PressableOpacity>
</>
);
};

Expand All @@ -40,4 +57,5 @@ const useStyles = StyleSheet.createThemeHook((theme) => ({
flexDirection: 'row',
gap: theme.spacings.medium,
},
maxRecipientsWarning: {marginTop: theme.spacings.medium},
}));
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import {TicketRecipientType} from '@atb/stacks-hierarchy/types.ts';
import {StyleSheet, useTheme} from '@atb/theme';
import {
OnBehalfOfTexts,
PhoneInputTexts,
PurchaseOverviewTexts,
useTranslation,
Expand All @@ -23,7 +24,7 @@ import {useQueryClient} from '@tanstack/react-query';
import {useAuthState} from '@atb/auth';

export const SubmitButton = ({
state: {settingName, recipient, phone, prefix, name, error},
state: {settingPhone, settingName, recipient, phone, prefix, name, error},
onSubmit,
onError,
themeColor,
Expand All @@ -44,11 +45,18 @@ export const SubmitButton = ({
const onPress = async () => {
if (recipient) {
onSubmit(recipient);
return;
}

setIsSubmitting(true);
onError(undefined);

if (!settingPhone) {
setIsSubmitting(false);
onError('no_recipient_selected');
return;
}

if (!phone) {
setIsSubmitting(false);
onError('invalid_phone');
Expand Down Expand Up @@ -127,6 +135,14 @@ export const SubmitButton = ({
/>
)}

{error === 'no_recipient_selected' && !isSubmitting && (
<MessageInfoBox
style={styles.errorMessage}
type="error"
message={t(OnBehalfOfTexts.errors[error])}
/>
)}

{!isSubmitting && (
<Button
interactiveColor="interactive_0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type ExistingRecipientType = Required<TicketRecipientType>;

export type OnBehalfOfErrorCode =
| GetAccountByPhoneErrorCode
| 'no_recipient_selected'
| 'missing_recipient_name'
| 'name_already_exists'
| 'phone_already_exists';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {useMutation, useQueryClient} from '@tanstack/react-query';
import {deleteOnBehalfOfAccount} from '@atb/api/profile.ts';
import {useAuthState} from '@atb/auth';
import {FETCH_RECIPIENTS_QUERY_KEY} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/use-fetch-recipients-query.ts';
import {OnBehalfOfAccountsResponse} from '@atb/api/types/profile.ts';
import {useState} from 'react';

export const useDeleteRecipientMutation = () => {
const {userId} = useAuthState();
const queryClient = useQueryClient();
const {active, addActive, removeActive} = useActiveDeletions();

const mutation = useMutation({
mutationFn: deleteOnBehalfOfAccount,
onMutate: (accountId) => addActive(accountId),
onSuccess: (_, accountId) => {
removeActive(accountId);
queryClient.setQueryData(
[FETCH_RECIPIENTS_QUERY_KEY, userId],
(oldData?: OnBehalfOfAccountsResponse) =>
oldData?.filter((r) => r.sentToAccountId !== accountId),
);
},
onError: (_, accountId) => removeActive(accountId),
});
return {activeDeletions: active, mutation};
};

const useActiveDeletions = () => {
const [active, setActive] = useState<string[]>([]);
const addActive = (accountId: string) =>
setActive((prev) => [...prev, accountId]);
const removeActive = (accountId: string) =>
setActive((prev) => prev.filter((accId) => accId !== accountId));
return {active, addActive, removeActive};
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@atb/stacks-hierarchy/Root_ChooseTicketRecipientScreen/types.ts';

type RecipientSelectionAction =
| {type: 'SELECT_RECIPIENT'; recipient: ExistingRecipientType}
| {type: 'SELECT_RECIPIENT'; recipient?: ExistingRecipientType}
| {type: 'SELECT_SEND_TO_OTHER'}
| {type: 'SET_PREFIX'; prefix: string}
| {type: 'SET_PHONE'; phoneNumber: string}
Expand Down
21 changes: 18 additions & 3 deletions src/translations/screens/subscreens/OnBehalfOf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ const OnBehalfOfTexts = {
'Husk at mottakaren må ha AtB-appen for å få billetten.',
),
},
tooManyRecipients: _(
'Du kan ikke legge til flere mottakere. Om du ønsker å sende til noen andre, må du fjerne en av mottakerne i listen ovenfor.',
"You can't add more recipients. If you wish to send to someone else, then you need to remove one of the recipients in the list above.",
'Du kan ikkje leggje til fleire mottakarar. Om du ønskjer sende til nokon andre, må du fjerne ein av mottakarane i lista ovanfor.',
),
newRecipientLabel: _('Ny mottaker', 'New recipient', 'Ny mottakar'),
nameInputLabel: _('Navn', 'Name', 'Namn'),
nameInputPlaceholder: _('Skriv inn navn', 'Enter name', 'Skriv inn namn'),
Expand All @@ -29,10 +34,20 @@ const OnBehalfOfTexts = {
'Lagre denne mottakaren til seinare',
),
errors: {
fetchRecipients: _(
'Kunne ikke hente mottakere',
fetch_recipients_failed: _(
'Kunne ikke laste mottakere',
"Couldn't retrieve recipients",
'Kunne ikkje hente mottakarar',
'Kunne ikkje laste mottakarar',
),
no_recipient_selected: _(
'Du må velge mottaker',
'You need to choose recipient',
'Du må velje mottakar',
),
delete_recipient_failed: _(
'Klarte ikke å slette mottaker',
"Weren't able to delete recipient",
'Klarte ikkje å slette mottakar',
),
missing_recipient_name: _(
'Du må legge inn navn på mottaker',
Expand Down

0 comments on commit 82a009a

Please sign in to comment.