Skip to content

Commit

Permalink
Merge pull request #319 from cultuurnet/feature/UIT-205-family-member…
Browse files Browse the repository at this point in the history
…-scan-summary

Family members scan summary
  • Loading branch information
chennara authored Dec 4, 2023
2 parents 79af6c4 + cd7f2ff commit bbba06d
Show file tree
Hide file tree
Showing 22 changed files with 221 additions and 69 deletions.
Binary file added src/_assets/images/check.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/_assets/images/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/_assets/images/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/_assets/images/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
export * from './avatars';
export const Check = require('./check.png');
export const Counter = require('./counter.png');
export const Error = require('./error.png');
export const Family = require('./family.png');
Expand Down
12 changes: 9 additions & 3 deletions src/_components/diagonalSplitView/DiagonalSplitView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FC } from 'react';
import { ScrollView, useWindowDimensions, View } from 'react-native';
import { ScrollView, StyleProp, useWindowDimensions, View, ViewStyle } from 'react-native';

import { ThemeColor } from '../../_styles/theme';
import { getColor } from '../../_utils';
Expand All @@ -9,15 +9,19 @@ import * as Styled from './style';
type TProps = {
backgroundColor?: ThemeColor;
bottomContent?: React.ReactNode;
bottomContentStyle?: StyleProp<ViewStyle>;
diagonalContainerHeight?: number;
isScrollable?: boolean;
lineColor?: ThemeColor;
topContent: React.ReactNode;
topContentStyle?: StyleProp<ViewStyle>;
};

const DiagonalSplitView: FC<TProps> = ({
topContent,
bottomContent,
topContentStyle,
bottomContentStyle,
backgroundColor = 'secondary.600',
lineColor = 'secondary.700',
isScrollable,
Expand All @@ -29,7 +33,9 @@ const DiagonalSplitView: FC<TProps> = ({
<>
<Styled.TopSafeAreaViewContainer backgroundColor={backgroundColor} edges={['top']} isScrollable={false} />
<Styled.ViewContainer backgroundColor={backgroundColor} edges={['bottom']} isScrollable={false}>
<Styled.TopContainer backgroundColor={backgroundColor}>{topContent}</Styled.TopContainer>
<Styled.TopContainer backgroundColor={backgroundColor} style={topContentStyle}>
{topContent}
</Styled.TopContainer>

<Styled.DiagonalContainer diagonalContainerHeight={diagonalContainerHeight} lineColor={lineColor}>
<Styled.Triangle
Expand All @@ -40,7 +46,7 @@ const DiagonalSplitView: FC<TProps> = ({
<Styled.TriangleDark diagonalContainerHeight={diagonalContainerHeight} screenWidth={width} />
</Styled.DiagonalContainer>

<Styled.BottomContainer as={isScrollable ? ScrollView : View}>
<Styled.BottomContainer as={isScrollable ? ScrollView : View} style={bottomContentStyle}>
{isScrollable ? <Styled.BottomContainerContent>{bottomContent}</Styled.BottomContainerContent> : bottomContent}
</Styled.BottomContainer>
</Styled.ViewContainer>
Expand Down
42 changes: 0 additions & 42 deletions src/_components/family/familyMemberPoints/FamilyMemberPoints.tsx

This file was deleted.

56 changes: 56 additions & 0 deletions src/_components/family/familyMembersPoints/FamilyMembersPoints.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { FlatList, StyleProp, View, ViewStyle } from 'react-native';

import { getAvatarByNameOrDefault } from '../../../_utils';
import { TFamilyMember } from '../../../profile/_models';
import Typography from '../../typography/Typography';
import * as Styled from './style';

type TProps<T extends { member: TFamilyMember }> = {
ItemRightComponent?: ({ item }: { item: T }) => ReactElement;
ItemSubtitle?: ({ item }: { item: T }) => ReactElement;
members: Array<T>;
style?: StyleProp<ViewStyle>;
};

export const FamilyMembersPoints = <T extends { member: TFamilyMember }>({
members,
ItemRightComponent,
ItemSubtitle,
style,
}: TProps<T>) => {
const { t } = useTranslation();

return (
<FlatList
ItemSeparatorComponent={Styled.Divider}
contentContainerStyle={style}
data={members}
keyExtractor={item => item.member.uitpasNumber}
renderItem={({ item }) => (
<Styled.Item>
<Styled.Avatar resizeMode="contain" source={getAvatarByNameOrDefault(item.member.icon)} />
<Styled.ItemBody>
<Typography fontStyle="bold" numberOfLines={1}>
{item.member.passholder.firstName}
{item.member.mainFamilyMember ? ` ${t('SHOP_DETAIL.WHO_CAN_REDEEM.YOU')}` : ''}
</Typography>
{ItemSubtitle ? (
<ItemSubtitle item={item} />
) : (
<Typography color="primary.700" fontStyle="semibold" numberOfLines={1} size="small">
{t('SHOP_DETAIL.WHO_CAN_REDEEM.POINTS', { count: item.member.passholder.points })}
</Typography>
)}
</Styled.ItemBody>
{ItemRightComponent && (
<View>
<ItemRightComponent item={item} />
</View>
)}
</Styled.Item>
)}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export const Avatar = styled.Image`

export const ItemBody = styled.View`
flex: 1;
height: 44px;
justify-content: space-around;
margin-left: 8px;
margin-right: 12px;
Expand Down
1 change: 1 addition & 0 deletions src/_components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { default as DatePicker } from './datePicker/DatePicker';
export { default as DiagonalSplitView } from './diagonalSplitView/DiagonalSplitView';
export { default as EnlargedHeader } from './enlargedHeader/EnlargedHeader';
export { default as ExternalLink } from './externalLink/ExternalLink';
export * from './family/familyMembersPoints/FamilyMembersPoints';
export { default as FakeTextInput } from './textInput/fakeTextInput/FakeTextInput';
export { default as FocusAwareStatusBar } from './statusBar/FocusAwareStatusBar';
export { default as HtmlRenderer } from './htmlRenderer/HtmlRenderer';
Expand Down
8 changes: 7 additions & 1 deletion src/_routing/_components/TRootStackParamList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { TFamilyMember } from '../../profile/_models';
import { TRedeemedReward } from '../../redeemedRewards/_models/redeemedReward';
import { TCheckInResponse } from '../../scan/_models';
import { TFamilyScanResponse } from '../../scan/familyCheckinSummary/_models';
import { TFilterRewardCategory, TFilterRewardSection } from '../../shop/_hooks/useRewardFilters';
import { TReward, TRewardType } from '../../shop/_models/reward';
import { TSearchFilters } from '../../shop/_models/searchFilters';
Expand Down Expand Up @@ -39,7 +40,12 @@ export type TRootStackParamList = {
};
FamiliesOverview: undefined;
FamilyCheckin: { checkinCode: string };
FamilyCheckinSummary: { familyMemberResponses: { member: TFamilyMember; response: TCheckInResponse }[] };
FamilyCheckinSummary: {
memberResponses: {
member: TFamilyMember;
response: TFamilyScanResponse;
}[];
};
FamilyInformation: undefined;
FamilyOnboarding: undefined;
FamilyOverview: undefined;
Expand Down
1 change: 1 addition & 0 deletions src/_translations/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@
"TITLE": "Met wie ben je vandaag op stap?",
"CHECKIN": "Spaar!",
"SUMMARY": {
"DESCRIPTION": "Je hebt gespaard voor",
"CLOSE": "Sluiten"
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/onboarding/family/FamilyOnboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { Button, SafeAreaView, Spinner, Trans } from '../../_components';
import { useOnboarding } from '../../_context';
import { StorageKey } from '../../_models';
import { TMainNavigationProp } from '../../_routing';
import { openExternalURL } from '../../_utils';
import { storage } from '../../storage';
import { useHasFamilyMembers } from './_queries';
import * as Styled from './style';
import { openExternalURL } from '../../_utils';

const BULLET_ITEMS = [
{ textKey: 'ONBOARDING.FAMILY.BULLET_1_TEXT' },
Expand Down
10 changes: 6 additions & 4 deletions src/scan/familyCheckin/FamilyCheckin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useGetFamilyMembers } from '../../onboarding/family/_queries';
import { TFamilyMember } from '../../profile/_models';
import { TCheckInResponse } from '../_models';
import { useCheckin } from '../_queries/useCheckin';
import { TFamilyScanResponse } from '../familyCheckinSummary/_models';
import * as Styled from './style';

type TProps = {
Expand Down Expand Up @@ -42,8 +43,8 @@ const FamilyCheckin: FC = ({ navigation, route }: TProps) => {
return checkin({ body: { checkinCode }, path: `/passholders/${member.passholder.id}/checkins` });
});
const responses = await Promise.allSettled(promises);
const familyMemberResponses = mapFamilyMembersToResponses(checkedFamilyMembers, responses);
navigation.navigate('FamilyCheckinSummary', { familyMemberResponses });
const memberResponses = mapFamilyMembersToResponses(checkedFamilyMembers, responses);
navigation.navigate('FamilyCheckinSummary', { memberResponses });
};

return (
Expand Down Expand Up @@ -85,12 +86,13 @@ const FamilyCheckin: FC = ({ navigation, route }: TProps) => {
const mapFamilyMembersToResponses = (
members: TFamilyMember[],
responses: PromiseSettledResult<TCheckInResponse>[],
): { member: TFamilyMember; response: TCheckInResponse }[] => {
): { member: TFamilyMember; response: TFamilyScanResponse }[] => {
return members.map((member, index) => {
const response = responses[index];
return {
member,
response: response.status === 'fulfilled' ? response.value : response.reason,
response:
response.status === 'fulfilled' ? { type: 'success', value: response.value } : { error: response.reason, type: 'error' },
};
});
};
Expand Down
73 changes: 64 additions & 9 deletions src/scan/familyCheckinSummary/FamilyCheckinSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,78 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { SafeAreaView } from '../../_components';
import { TRootStackNavigationProp } from '../../_routing';
import { Check } from '../../_assets/images';
import { DiagonalSplitView, FamilyMembersPoints, Typography } from '../../_components';
import { TRootStackNavigationProp, TRootStackRouteProp } from '../../_routing';
import { TFamilyMember } from '../../profile/_models';
import { TFamilyScanResponse } from './_models';
import { CheckinErrorIcon, CheckinSuccessIcon } from './icons';
import * as Styled from './style';

type TProps = {
navigation: TRootStackNavigationProp<'FamilyCheckinSummary'>;
route: TRootStackRouteProp<'FamilyCheckinSummary'>;
};

export const FamilyCheckinSummary = ({ navigation }: TProps) => {
type FamilyMembersSummaryItem = { item: { member: TFamilyMember; response: TFamilyScanResponse } };

export const FamilyCheckinSummary = ({ navigation, route }: TProps) => {
const { memberResponses } = route.params;

const { t } = useTranslation();
const { top } = useSafeAreaInsets();

const FamilyMembersSummary = useCallback(() => {
const renderIcon = ({ item: { response } }: FamilyMembersSummaryItem) => {
if (response.type === 'success') {
return <CheckinSuccessIcon numberOfPoints={response.value.addedPoints} />;
}
return <CheckinErrorIcon />;
};

const renderErrorDescription = ({ item: { member, response } }: FamilyMembersSummaryItem) => {
if (response.type === 'error') {
return <Typography size="small">{response.error.endUserMessage.nl}</Typography>;
}
return (
<Typography color="primary.700" fontStyle="semibold" numberOfLines={1} size="small">
{t('SHOP_DETAIL.WHO_CAN_REDEEM.POINTS', { count: member.passholder.points })}
</Typography>
);
};

return (
<FamilyMembersPoints
ItemRightComponent={renderIcon}
ItemSubtitle={renderErrorDescription}
members={memberResponses}
style={{ paddingHorizontal: 16 }}
/>
);
}, [memberResponses, t]);

return (
<SafeAreaView backgroundColor="neutral.0" edges={['bottom']} isScrollable={false}>
<Styled.Body />
<Styled.CloseButton
label={t('SCAN.FAMILY_MEMBERS.SUMMARY.CLOSE')}
onPress={() => navigation.reset({ index: 0, routes: [{ name: 'MainNavigator', params: { screen: 'Profile' } }] })}
<>
<DiagonalSplitView
bottomContent={
<Styled.Body>
<FamilyMembersSummary />
<Styled.CloseButton
label={t('SCAN.FAMILY_MEMBERS.SUMMARY.CLOSE')}
onPress={() => navigation.reset({ index: 0, routes: [{ name: 'MainNavigator', params: { screen: 'Profile' } }] })}
/>
</Styled.Body>
}
bottomContentStyle={{ paddingLeft: 0, paddingRight: 0 }}
topContent={null}
/>
</SafeAreaView>
<Styled.Header style={{ top: top + 16 }}>
<Styled.HeaderImage source={Check} />
<Styled.HeaderTitle color="primary.700" fontStyle="bold" size="large">
{t('SCAN.FAMILY_MEMBERS.SUMMARY.DESCRIPTION')}
</Styled.HeaderTitle>
</Styled.Header>
</>
);
};
4 changes: 4 additions & 0 deletions src/scan/familyCheckinSummary/_models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { TApiError } from '../../../_http';
import { TCheckInResponse } from '../../_models';

export type TFamilyScanResponse = { type: 'success'; value: TCheckInResponse } | { error: TApiError; type: 'error' };
12 changes: 12 additions & 0 deletions src/scan/familyCheckinSummary/icons/CheckinErrorIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Typography } from '../../../_components';
import * as Styled from './style';

export const CheckinErrorIcon = () => {
return (
<Styled.Container type="error">
<Typography color="error.500" fontStyle="bold" size="large">
!
</Typography>
</Styled.Container>
);
};
16 changes: 16 additions & 0 deletions src/scan/familyCheckinSummary/icons/CheckinSuccessIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Typography } from '../../../_components';
import * as Styled from './style';

type TProps = {
numberOfPoints: number;
};

export const CheckinSuccessIcon = ({ numberOfPoints }: TProps) => {
return (
<Styled.Container type="success">
<Typography color="primary.700" fontStyle="bold" size="small">
+{numberOfPoints}
</Typography>
</Styled.Container>
);
};
2 changes: 2 additions & 0 deletions src/scan/familyCheckinSummary/icons/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './CheckinErrorIcon';
export * from './CheckinSuccessIcon';
10 changes: 10 additions & 0 deletions src/scan/familyCheckinSummary/icons/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styled from 'styled-components/native';

export const Container = styled.View<{ type: 'success' | 'error' }>`
width: 40px;
height: 40px;
border-radius: 20px;
border: ${({ type, theme }) => `4px solid ${type === 'success' ? theme.palette.primary['200'] : theme.palette.error['200']}`};
justify-content: center;
align-items: center;
`;
Loading

0 comments on commit bbba06d

Please sign in to comment.