From 4a57e519375fd0e75645a68d38098a3706bced87 Mon Sep 17 00:00:00 2001 From: Sri Kanth Kola Date: Sun, 12 Jun 2022 22:13:59 +0530 Subject: [PATCH 01/12] Added Swipe Gesture & details card --- components/DeviceInfoList.tsx | 6 - components/MessageOverlay.tsx | 4 +- components/NewVcDetails.tsx | 303 +++++++++++++++++++ components/SuccesfullyReceived.tsx | 21 ++ components/TimerBasedMessageOverlay.tsx | 55 ++++ components/UpdatedVcDetails.tsx | 289 ++++++++++++++++++ components/UpdatedVcItem.tsx | 211 +++++++++++++ components/displayVcItem.tsx | 133 ++++++++ components/ui/Modal.tsx | 10 +- components/ui/UpdatedModal.tsx | 54 ++++ machines/request.ts | 3 + machines/request.typegen.ts | 13 +- package-lock.json | 23 +- package.json | 1 + routes/main.ts | 7 + screens/Home/HomeScreen.tsx | 10 + screens/Home/MyVcsTab.tsx | 45 +-- screens/Home/ReceivedVcsTab.tsx | 4 +- screens/Home/ViewVcModal.tsx | 8 +- screens/Profile/ProfileScreen.tsx | 2 +- screens/Request/ReceiveVcModal.tsx | 20 +- screens/Request/RequestScreen.tsx | 1 + screens/Request/RequestScreenController.ts | 4 + screens/Request/TimerBasedReceiveVcModal.tsx | 32 ++ screens/Request/TimerBasedRequestScreen.tsx | 77 +++++ screens/Scan/ScanScreen.tsx | 4 +- screens/Scan/UpdatedSendVcModal.tsx | 116 +++++++ 27 files changed, 1394 insertions(+), 62 deletions(-) create mode 100644 components/NewVcDetails.tsx create mode 100644 components/SuccesfullyReceived.tsx create mode 100644 components/TimerBasedMessageOverlay.tsx create mode 100644 components/UpdatedVcDetails.tsx create mode 100644 components/UpdatedVcItem.tsx create mode 100644 components/displayVcItem.tsx create mode 100644 components/ui/UpdatedModal.tsx create mode 100644 screens/Request/TimerBasedReceiveVcModal.tsx create mode 100644 screens/Request/TimerBasedRequestScreen.tsx create mode 100644 screens/Scan/UpdatedSendVcModal.tsx diff --git a/components/DeviceInfoList.tsx b/components/DeviceInfoList.tsx index 08923c73..2ea52905 100644 --- a/components/DeviceInfoList.tsx +++ b/components/DeviceInfoList.tsx @@ -12,12 +12,6 @@ export const DeviceInfoList: React.FC = (props) => { label={props.of === 'receiver' ? t('requestedBy') : t('sentBy')} text={props.deviceInfo.deviceName} /> - - ); }; diff --git a/components/MessageOverlay.tsx b/components/MessageOverlay.tsx index ccf9de99..1d72edd5 100644 --- a/components/MessageOverlay.tsx +++ b/components/MessageOverlay.tsx @@ -16,7 +16,8 @@ export const MessageOverlay: React.FC = (props) => { + onBackdropPress={props.onBackdropPress} + onShow={props.onShow}> {props.title && ( @@ -38,4 +39,5 @@ interface MessageOverlayProps { message?: string; hasProgress?: boolean; onBackdropPress?: () => void; + onShow?: () => void; } diff --git a/components/NewVcDetails.tsx b/components/NewVcDetails.tsx new file mode 100644 index 00000000..82ba24d2 --- /dev/null +++ b/components/NewVcDetails.tsx @@ -0,0 +1,303 @@ +import { formatDistanceToNow } from 'date-fns'; +import React from 'react'; +import * as DateFnsLocale from '../lib/date-fns/locale'; +import { useTranslation } from 'react-i18next'; +import { Image, StyleSheet } from 'react-native'; +import { Icon, ListItem } from 'react-native-elements'; +import { VC, CredentialSubject } from '../types/vc'; +import { Column, Row, Text } from './ui'; +import { Colors } from './ui/styleUtils'; +import { TextItem } from './ui/TextItem'; +import { useReceiveVcModal } from '../screens/Request/ReceiveVcModalController'; + +const styles = StyleSheet.create({ + successTag: { + backgroundColor: Colors.Green, + height: 43, + flex: 1, + alignItems: 'center', + paddingLeft: 6, + }, + header: { + flex: 1, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingLeft: 18, + margin: 6, + // borderRadius: 0, + // borderWidth: 0, + // marginBottom: 0, + }, + logo: { + height: 48, + width: 40, + marginRight: 10, + }, + container: { + flex: 2, + padding: 10, + }, + detailes: { + width: 290, + marginLeft: 110, + marginTop: 0, + }, + labelPart: { + margin: 0, + borderRadius: 0, + borderWidth: 0, + width: 240, + borderBottomWidth: 0, + backgroundColor: 'transparent', + }, +}); + +const VerifiedIcon: React.FC = () => { + return ( + + ); +}; + +export const NewVcDetails: React.FC = (props) => { + const { t, i18n } = useTranslation('VcDetails'); + const controller = useReceiveVcModal(); + + return ( + + + + + {controller.vcLabel.singular} Received + + + + + + + + {t('fullName')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credentialSubject.fullName + )} + + + + + + + + + {t('photo')} + + + + + + + + + + + {props.vc?.idType} + + + {props.vc?.id} + + + + + {t('generatedOn')} + + + {new Date(props.vc?.generatedOn).toLocaleDateString()} + + + + + {t('status')} + + + + {t('valid')} + + {props.vc?.isVerified && } + + + + + + + {t('gender')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credentialSubject.gender + )} + + + + + + {t('dateOfBirth')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credentialSubject.dateOfBirth + )} + + + + + + {t('phoneNumber')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credentialSubject.phone + )} + + + + + + {t('email')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credentialSubject.email + )} + + + + + + {t('address')} + + + {getFullAddress( + props.vc?.verifiableCredential.credentialSubject + )} + + + + {props.vc?.reason?.length > 0 && ( + + {t('reasonForSharing')} + + )} + {props.vc?.reason?.map((reason, index) => ( + + ))} + + + + + ); +}; + +interface VcDetailsProps { + vc: VC; +} + +interface LocalizedField { + language: string; + value: string; +} + +function getFullAddress(credential: CredentialSubject) { + if (!credential) { + return ''; + } + + const fields = [ + 'addressLine1', + 'addressLine2', + 'addressLine3', + 'city', + 'province', + 'region', + ]; + + return fields + .map((field) => getLocalizedField(credential[field])) + .concat(credential.postalCode) + .filter(Boolean) + .join(', '); +} + +function getLocalizedField(rawField: string | LocalizedField) { + if (typeof rawField === 'string') { + return rawField; + } + try { + const locales: LocalizedField[] = JSON.parse(JSON.stringify(rawField)); + return locales[0].value; + } catch (e) { + return ''; + } +} diff --git a/components/SuccesfullyReceived.tsx b/components/SuccesfullyReceived.tsx new file mode 100644 index 00000000..f260f589 --- /dev/null +++ b/components/SuccesfullyReceived.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Overlay } from 'react-native-elements'; + +export const SuccesfullyReceived: React.FC = (props) => { + return ( + + ); +}; + +interface MessageOverlayProps { + isVisible: boolean; + img?: string; + title?: string; + message?: string; + hasProgress?: boolean; + onBackdropPress?: () => void; + onShow?: () => void; +} diff --git a/components/TimerBasedMessageOverlay.tsx b/components/TimerBasedMessageOverlay.tsx new file mode 100644 index 00000000..236f6b18 --- /dev/null +++ b/components/TimerBasedMessageOverlay.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Dimensions, StyleSheet } from 'react-native'; +import { Overlay, LinearProgress } from 'react-native-elements'; +import { Column, Text } from './ui'; +import { Colors, elevation } from './ui/styleUtils'; +import { Icon } from 'react-native-elements'; + +const styles = StyleSheet.create({ + overlay: { + ...elevation(5), + backgroundColor: Colors.White, + }, +}); + +export const TimerBasedMessageOverlay: React.FC = ( + props +) => { + return ( + + {props.img ? ( + + ) : null} + + + {props.title && ( + + {props.title} + + )} + {props.message && ( + + {props.message} + + )} + {props.hasProgress && ( + + )} + + + ); +}; + +interface MessageOverlayProps { + isVisible: boolean; + img?: string; + title?: string; + message?: string; + hasProgress?: boolean; + onBackdropPress?: () => void; + onShow?: () => void; +} diff --git a/components/UpdatedVcDetails.tsx b/components/UpdatedVcDetails.tsx new file mode 100644 index 00000000..cc4deb74 --- /dev/null +++ b/components/UpdatedVcDetails.tsx @@ -0,0 +1,289 @@ +import { formatDistanceToNow } from 'date-fns'; +import React from 'react'; +import * as DateFnsLocale from '../lib/date-fns/locale'; +import { useTranslation } from 'react-i18next'; +import { Image, StyleSheet, View } from 'react-native'; +import { Icon, ListItem } from 'react-native-elements'; +import { VC, CredentialSubject } from '../types/vc'; +import { Column, Row, Text } from './ui'; +import { Colors } from './ui/styleUtils'; +import { TextItem } from './ui/TextItem'; + +const styles = StyleSheet.create({ + successTag: { + backgroundColor: Colors.Green, + height: 43, + flex: 1, + alignItems: 'center', + paddingLeft: 6, + }, + header: { + flex: 1, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingLeft: 18, + margin: 6, + // borderRadius: 0, + // borderWidth: 0, + // marginBottom: 0, + }, + logo: { + height: 50, + width: 40, + marginRight: 10, + }, + container: { + flex: 2, + padding: 10, + }, + detailes: { + width: 290, + marginLeft: 110, + marginTop: 0, + }, + labelPart: { + margin: 0, + borderRadius: 0, + borderWidth: 0, + width: 240, + borderBottomWidth: 0, + backgroundColor: 'transparent', + }, +}); + +const VerifiedIcon: React.FC = () => { + return ( + + ); +}; + +export const UpdatedVcDetails: React.FC = (props) => { + const { t, i18n } = useTranslation('VcDetails'); + + return ( + + + + + {t('fullName')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credentialSubject.fullName + )} + + + + + + + + + {t('photo')} + + + + + + + + + + + {props.vc?.idType} + + + {props.vc?.id} + + + + + + {t('generatedOn')} + + + {new Date(props.vc?.generatedOn).toLocaleDateString()} + + + + + + {t('status')} + + + + {t('valid')} + + {props.vc?.isVerified && } + + + + + + + {t('gender')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credentialSubject.gender + )} + + + + + + {t('dateOfBirth')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credentialSubject.dateOfBirth + )} + + + + + + {t('phoneNumber')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credentialSubject.phone + )} + + + + + + {t('email')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credentialSubject.email + )} + + + + + + {t('address')} + + + {getFullAddress(props.vc?.verifiableCredential.credentialSubject)} + + + + {props.vc?.reason?.length > 0 && ( + + {t('reasonForSharing')} + + )} + {props.vc?.reason?.map((reason, index) => ( + + ))} + + + + ); +}; + +interface VcDetailsProps { + vc: VC; +} + +interface LocalizedField { + language: string; + value: string; +} + +function getFullAddress(credential: CredentialSubject) { + if (!credential) { + return ''; + } + + const fields = [ + 'addressLine1', + 'addressLine2', + 'addressLine3', + 'city', + 'province', + 'region', + ]; + + return fields + .map((field) => getLocalizedField(credential[field])) + .concat(credential.postalCode) + .filter(Boolean) + .join(', '); +} + +function getLocalizedField(rawField: string | LocalizedField) { + if (typeof rawField === 'string') { + return rawField; + } + try { + const locales: LocalizedField[] = JSON.parse(JSON.stringify(rawField)); + return locales[0].value; + } catch (e) { + return ''; + } +} diff --git a/components/UpdatedVcItem.tsx b/components/UpdatedVcItem.tsx new file mode 100644 index 00000000..831c5e8c --- /dev/null +++ b/components/UpdatedVcItem.tsx @@ -0,0 +1,211 @@ +import React, { useContext, useRef } from 'react'; +import { useInterpret, useSelector } from '@xstate/react'; +import { Pressable, StyleSheet } from 'react-native'; +import { CheckBox, Icon } from 'react-native-elements'; +import { ActorRefFrom } from 'xstate'; +import { + createVcItemMachine, + selectVerifiableCredential, + selectGeneratedOn, + selectTag, + selectId, + vcItemMachine, +} from '../machines/vcItem'; +import { Column, Row, Text } from './ui'; +import { Colors } from './ui/styleUtils'; +import { RotatingIcon } from './RotatingIcon'; +import { GlobalContext } from '../shared/GlobalContext'; +import { useTranslation } from 'react-i18next'; +import { Image } from 'react-native'; + +const styles = StyleSheet.create({ + title: { + color: Colors.Black, + backgroundColor: 'transparent', + }, + loadingTitle: { + color: 'transparent', + backgroundColor: Colors.Grey5, + borderRadius: 4, + }, + subtitle: { + backgroundColor: 'transparent', + }, + loadingSubtitle: { + backgroundColor: Colors.Grey, + borderRadius: 4, + }, + container: { + backgroundColor: Colors.White, + }, + loadingContainer: { + backgroundColor: Colors.Grey6, + borderRadius: 4, + }, + detailsContainer: { + flex: 1, + flexDirection: 'row', + padding: 10, + backgroundColor: '#fef5eb', + }, + bgContainer: { + backgroundColor: '#fef5eb', + borderRadius: 15, + marginTop: 10, + }, + logoContainer: { + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-end', + marginLeft: 300, + }, +}); + +const VerifiedIcon: React.FC = () => { + return ( + + ); +}; + +export const UpdatedVcItem: React.FC = (props) => { + const { appService } = useContext(GlobalContext); + const { t } = useTranslation('VcDetails'); + + const machine = useRef( + createVcItemMachine( + appService.getSnapshot().context.serviceRefs, + props.vcKey + ) + ); + const service = useInterpret(machine.current); + const uin = useSelector(service, selectId); + const tag = useSelector(service, selectTag); + const verifiableCredential = useSelector(service, selectVerifiableCredential); + const generatedOn = useSelector(service, selectGeneratedOn); + + const selectableOrCheck = props.selectable ? ( + } + uncheckedIcon={} + onPress={() => props.onPress(service)} + /> + ) : null; + + return ( + props.onPress(service)} + disabled={!verifiableCredential} + style={styles.bgContainer}> + + {/* + + */} + + + + + + + Full name + + + {!verifiableCredential + ? '' + : getLocalizedField( + verifiableCredential.credentialSubject.fullName + )} + + + + + + UIN + + + {!verifiableCredential ? '' : tag || uin} + + + + + Generated on + + + {!verifiableCredential ? '' : generatedOn} + + + + + {t('status')} + + + + {t('valid')} + + + + + + + {verifiableCredential ? ( + selectableOrCheck + ) : ( + + )} + + + ); +}; + +interface VcItemProps { + vcKey: string; + margin?: string; + selectable?: boolean; + selected?: boolean; + onPress?: (vcRef?: ActorRefFrom) => void; + onShow?: () => void; +} + +function getLocalizedField(rawField: string | LocalizedField) { + if (typeof rawField === 'string') { + return rawField; + } + try { + const locales: LocalizedField[] = JSON.parse(JSON.stringify(rawField)); + return locales[0].value; + } catch (e) { + return ''; + } +} diff --git a/components/displayVcItem.tsx b/components/displayVcItem.tsx new file mode 100644 index 00000000..8b76b5e0 --- /dev/null +++ b/components/displayVcItem.tsx @@ -0,0 +1,133 @@ +import React, { useContext, useRef } from 'react'; +import { useInterpret, useSelector } from '@xstate/react'; +import { Pressable, StyleSheet } from 'react-native'; +import { CheckBox, Icon } from 'react-native-elements'; +import { ActorRefFrom } from 'xstate'; +import { + createVcItemMachine, + selectVerifiableCredential, + selectGeneratedOn, + selectTag, + selectId, + vcItemMachine, +} from '../machines/vcItem'; +import { Column, Row, Text } from './ui'; +import { Colors } from './ui/styleUtils'; +import { RotatingIcon } from './RotatingIcon'; +import { GlobalContext } from '../shared/GlobalContext'; + +const styles = StyleSheet.create({ + title: { + color: Colors.Black, + backgroundColor: 'transparent', + }, + loadingTitle: { + color: 'transparent', + backgroundColor: Colors.Grey5, + borderRadius: 4, + }, + subtitle: { + backgroundColor: 'transparent', + }, + loadingSubtitle: { + backgroundColor: Colors.Grey, + borderRadius: 4, + }, + container: { + backgroundColor: Colors.White, + }, + loadingContainer: { + backgroundColor: Colors.Grey6, + borderRadius: 4, + }, +}); + +export const VcItem: React.FC = (props) => { + const { appService } = useContext(GlobalContext); + const machine = useRef( + createVcItemMachine( + appService.getSnapshot().context.serviceRefs, + props.vcKey + ) + ); + const service = useInterpret(machine.current); + const uin = useSelector(service, selectId); + const tag = useSelector(service, selectTag); + const verifiableCredential = useSelector(service, selectVerifiableCredential); + const generatedOn = useSelector(service, selectGeneratedOn); + + const selectableOrCheck = props.selectable ? ( + } + uncheckedIcon={} + onPress={() => props.onPress(service)} + /> + ) : ( + + ); + + return ( + props.onPress(service)} + disabled={!verifiableCredential}> + + + + {!verifiableCredential ? '' : tag || uin} + + + {!verifiableCredential + ? '' + : getLocalizedField( + verifiableCredential.credentialSubject.fullName + ) + + ' ยท ' + + generatedOn} + + + {verifiableCredential ? ( + selectableOrCheck + ) : ( + + )} + + + ); +}; + +interface VcItemProps { + vcKey: string; + margin?: string; + selectable?: boolean; + selected?: boolean; + onPress?: (vcRef?: ActorRefFrom) => void; +} + +function getLocalizedField(rawField: string | LocalizedField) { + if (typeof rawField === 'string') { + return rawField; + } + try { + const locales: LocalizedField[] = JSON.parse(JSON.stringify(rawField)); + return locales[0].value; + } catch (e) { + return ''; + } +} diff --git a/components/ui/Modal.tsx b/components/ui/Modal.tsx index 246544a8..0c0a2635 100644 --- a/components/ui/Modal.tsx +++ b/components/ui/Modal.tsx @@ -17,6 +17,7 @@ export const Modal: React.FC = (props) => { animationType="slide" style={styles.modal} visible={props.isVisible} + onShow={props.onShow} onRequestClose={props.onDismiss}> @@ -27,16 +28,10 @@ export const Modal: React.FC = (props) => { color={Colors.Orange} /> ) : null} + {props.headerTitle} - {props.headerRight || ( - - )} {props.children} @@ -50,4 +45,5 @@ export interface ModalProps { headerTitle?: string; headerElevation?: ElevationLevel; headerRight?: React.ReactElement; + onShow?: () => void; } diff --git a/components/ui/UpdatedModal.tsx b/components/ui/UpdatedModal.tsx new file mode 100644 index 00000000..60216991 --- /dev/null +++ b/components/ui/UpdatedModal.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Dimensions, Modal as RNModal, StyleSheet } from 'react-native'; +import { Icon } from 'react-native-elements'; +import { Column, Row } from '.'; +import { Colors, ElevationLevel } from './styleUtils'; + +const styles = StyleSheet.create({ + modal: { + width: Dimensions.get('screen').width, + height: Dimensions.get('screen').height, + }, +}); + +export const Modal: React.FC = (props) => { + return ( + + + + {props.headerRight ? ( + + ) : null} + + {/* + {props.headerTitle} + */} + {/* {props.headerRight || ( + + )} */} + + {props.children} + + + ); +}; + +export interface ModalProps { + isVisible: boolean; + onDismiss: () => void; + headerTitle?: string; + headerElevation?: ElevationLevel; + headerRight?: React.ReactElement; +} diff --git a/machines/request.ts b/machines/request.ts index 8ac7eb45..7a00fae2 100644 --- a/machines/request.ts +++ b/machines/request.ts @@ -28,6 +28,7 @@ const model = createModel( REJECT: () => ({}), CANCEL: () => ({}), DISMISS: () => ({}), + GOBACK: () => ({}), VC_RECEIVED: (vc: VC) => ({ vc }), RESPONSE_SENT: () => ({}), CONNECTED: () => ({}), @@ -207,6 +208,7 @@ export const requestMachine = model.createMachine( }, on: { DISMISS: 'navigatingToHome', + GOBACK: '#clearingConnection', }, }, rejected: { @@ -221,6 +223,7 @@ export const requestMachine = model.createMachine( }, }, navigatingToHome: {}, + navigatingToTimeBasedRequest: {}, }, exit: ['disconnect'], }, diff --git a/machines/request.typegen.ts b/machines/request.typegen.ts index a02984fe..25426a6b 100644 --- a/machines/request.typegen.ts +++ b/machines/request.typegen.ts @@ -9,12 +9,17 @@ export interface Typegen0 { removeLoggers: | 'SCREEN_BLUR' | 'xstate.after(CLEAR_DELAY)#clearingConnection' - | 'DISMISS'; + | 'DISMISS' + | 'GOBACK'; disconnect: ''; - registerLoggers: 'xstate.after(CLEAR_DELAY)#clearingConnection' | 'DISMISS'; + registerLoggers: + | 'xstate.after(CLEAR_DELAY)#clearingConnection' + | 'DISMISS' + | 'GOBACK'; generateConnectionParams: | 'xstate.after(CLEAR_DELAY)#clearingConnection' - | 'DISMISS'; + | 'DISMISS' + | 'GOBACK'; requestReceiverInfo: 'CONNECTED'; requestReceivedVcs: 'xstate.init'; requestExistingVc: 'VC_RESPONSE'; @@ -84,6 +89,7 @@ export interface Typegen0 { | 'reviewing.accepted' | 'reviewing.rejected' | 'reviewing.navigatingToHome' + | 'reviewing.navigatingToTimeBasedRequest' | 'disconnected' | { checkingBluetoothService?: 'checking' | 'requesting' | 'enabled'; @@ -93,6 +99,7 @@ export interface Typegen0 { | 'accepted' | 'rejected' | 'navigatingToHome' + | 'navigatingToTimeBasedRequest' | { accepting?: | 'requestingReceivedVcs' diff --git a/package-lock.json b/package-lock.json index 721f35d7..36016da5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "react-native-securerandom": "^1.0.0", "react-native-simple-markdown": "^1.1.0", "react-native-svg": "12.1.1", + "react-native-swipe-gestures": "^1.0.5", "react-native-system-setting": "^1.7.6", "react-native-vector-icons": "^8.1.0", "xstate": "^4.26.0" @@ -10287,9 +10288,9 @@ } }, "node_modules/eventsource": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz", - "integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", + "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", "dev": true, "dependencies": { "original": "^1.0.0" @@ -20458,6 +20459,11 @@ "react-native": ">=0.50.0" } }, + "node_modules/react-native-swipe-gestures": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz", + "integrity": "sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw==" + }, "node_modules/react-native-system-setting": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/react-native-system-setting/-/react-native-system-setting-1.7.6.tgz", @@ -34785,9 +34791,9 @@ "dev": true }, "eventsource": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz", - "integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", + "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", "dev": true, "requires": { "original": "^1.0.0" @@ -42870,6 +42876,11 @@ "css-tree": "^1.0.0-alpha.39" } }, + "react-native-swipe-gestures": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz", + "integrity": "sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw==" + }, "react-native-system-setting": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/react-native-system-setting/-/react-native-system-setting-1.7.6.tgz", diff --git a/package.json b/package.json index ce7b1b04..60c06bfe 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "react-native-securerandom": "^1.0.0", "react-native-simple-markdown": "^1.1.0", "react-native-svg": "12.1.1", + "react-native-swipe-gestures": "^1.0.5", "react-native-system-setting": "^1.7.6", "react-native-vector-icons": "^8.1.0", "xstate": "^4.26.0" diff --git a/routes/main.ts b/routes/main.ts index 3d7f269d..753fdf71 100644 --- a/routes/main.ts +++ b/routes/main.ts @@ -8,6 +8,7 @@ import { ProfileScreen } from '../screens/Profile/ProfileScreen'; import { RequestScreen } from '../screens/Request/RequestScreen'; import { ScanScreen } from '../screens/Scan/ScanScreen'; import { RootStackParamList } from './index'; +import { TimerBasedRequestScreen } from '../screens/Request/TimerBasedRequestScreen'; export const mainRoutes: TabScreen[] = [ { @@ -23,6 +24,11 @@ export const mainRoutes: TabScreen[] = [ headerShown: false, }, }, + { + name: 'TimerBasedRequest', + component: TimerBasedRequestScreen, + icon: 'file-download', + }, { name: 'Request', component: RequestScreen, @@ -41,6 +47,7 @@ export type MainBottomTabParamList = { }; Scan: undefined; Request: undefined; + TimerBasedRequest: undefined; Profile: undefined; }; diff --git a/screens/Home/HomeScreen.tsx b/screens/Home/HomeScreen.tsx index f8aff575..2ec6ef97 100644 --- a/screens/Home/HomeScreen.tsx +++ b/screens/Home/HomeScreen.tsx @@ -11,6 +11,8 @@ import { ViewVcModal } from './ViewVcModal'; import { useHomeScreen } from './HomeScreenController'; import { TabRef } from './HomeScreenMachine'; import { useTranslation } from 'react-i18next'; +import { ActorRefFrom } from 'xstate'; +import { vcItemMachine } from '../../machines/vcItem'; const styles = StyleSheet.create({ tabIndicator: { @@ -45,14 +47,20 @@ export const HomeScreen: React.FC = (props) => { props.navigation.navigate('TimerBasedRequest')} /> props.navigation.navigate('TimerBasedRequest')} /> props.navigation.navigate('TimerBasedRequest')} /> )} @@ -84,4 +92,6 @@ function TabItem(title: string) { export interface HomeScreenTabProps { isVisible: boolean; service: TabRef; + onSwipe: () => void; + vcItemActor: ActorRefFrom; } diff --git a/screens/Home/MyVcsTab.tsx b/screens/Home/MyVcsTab.tsx index b2109b71..56c107c0 100644 --- a/screens/Home/MyVcsTab.tsx +++ b/screens/Home/MyVcsTab.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { Button, Column, Text, Centered } from '../../components/ui'; -import { VcItem } from '../../components/VcItem'; import { Icon } from 'react-native-elements'; import { Colors } from '../../components/ui/styleUtils'; import { RefreshControl } from 'react-native'; @@ -10,6 +9,8 @@ import { AddVcModal } from './MyVcs/AddVcModal'; import { DownloadingVcModal } from './MyVcs/DownloadingVcModal'; import { OnboardingOverlay } from './OnboardingOverlay'; import { useTranslation } from 'react-i18next'; +import GestureRecognizer from 'react-native-swipe-gestures'; +import { UpdatedVcItem } from '../../components/UpdatedVcItem'; export const MyVcsTab: React.FC = (props) => { const { t } = useTranslation('MyVcsTab'); @@ -30,7 +31,7 @@ export const MyVcsTab: React.FC = (props) => { /> }> {controller.vcKeys.map((vcKey) => ( - = (props) => { /> ))} - -